Fix guest demo UX and enforce guest limits
This commit is contained in:
57
resources/js/guest/components/DemoReadOnlyNotice.tsx
Normal file
57
resources/js/guest/components/DemoReadOnlyNotice.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { motion, type HTMLMotionProps } from 'framer-motion';
|
||||
import { ZapOff } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export type DemoReadOnlyNoticeProps = {
|
||||
title: string;
|
||||
copy: string;
|
||||
hint?: string;
|
||||
ctaLabel?: string;
|
||||
onCta?: () => void;
|
||||
radius?: number;
|
||||
bodyFont?: string;
|
||||
motionProps?: HTMLMotionProps<'div'>;
|
||||
};
|
||||
|
||||
export default function DemoReadOnlyNotice({
|
||||
title,
|
||||
copy,
|
||||
hint,
|
||||
ctaLabel,
|
||||
onCta,
|
||||
radius,
|
||||
bodyFont,
|
||||
motionProps,
|
||||
}: DemoReadOnlyNoticeProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className="rounded-[28px] border border-white/15 bg-black/70 p-5 text-white shadow-2xl backdrop-blur"
|
||||
style={{ borderRadius: radius, fontFamily: bodyFont }}
|
||||
{...motionProps}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-white/10">
|
||||
<ZapOff className="h-5 w-5 text-amber-200" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-semibold">{title}</p>
|
||||
<p className="text-xs text-white/80">{copy}</p>
|
||||
{hint ? <p className="text-[11px] text-white/60">{hint}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
{ctaLabel && onCta ? (
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full bg-white/90 text-slate-900 hover:bg-white"
|
||||
onClick={onCta}
|
||||
>
|
||||
{ctaLabel}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -40,10 +40,10 @@ const EVENT_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: st
|
||||
|
||||
type LogoSize = 's' | 'm' | 'l';
|
||||
|
||||
const LOGO_SIZE_CLASSES: Record<LogoSize, { container: string; image: string; emoji: string; icon: string }> = {
|
||||
s: { container: 'h-8 w-8', image: 'h-7 w-7', emoji: 'text-lg', icon: 'h-4 w-4' },
|
||||
m: { container: 'h-10 w-10', image: 'h-9 w-9', emoji: 'text-xl', icon: 'h-5 w-5' },
|
||||
l: { container: 'h-12 w-12', image: 'h-11 w-11', emoji: 'text-2xl', icon: 'h-6 w-6' },
|
||||
const LOGO_SIZE_CLASSES: Record<LogoSize, { container: string; image: string; emoji: string; icon: string; initials: string }> = {
|
||||
s: { container: 'h-8 w-8', image: 'h-7 w-7', emoji: 'text-lg', icon: 'h-4 w-4', initials: 'text-[11px]' },
|
||||
m: { container: 'h-10 w-10', image: 'h-9 w-9', emoji: 'text-xl', icon: 'h-5 w-5', initials: 'text-sm' },
|
||||
l: { container: 'h-12 w-12', image: 'h-11 w-11', emoji: 'text-2xl', icon: 'h-6 w-6', initials: 'text-base' },
|
||||
};
|
||||
|
||||
function getLogoClasses(size?: LogoSize) {
|
||||
@@ -81,18 +81,36 @@ function getInitials(name: string): string {
|
||||
return name.substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function renderEventAvatar(
|
||||
name: string,
|
||||
icon: unknown,
|
||||
accentColor: string,
|
||||
textColor: string,
|
||||
logo?: { mode: 'emoticon' | 'upload'; value: string | null; size?: LogoSize }
|
||||
) {
|
||||
function EventAvatar({
|
||||
name,
|
||||
icon,
|
||||
accentColor,
|
||||
textColor,
|
||||
logo,
|
||||
}: {
|
||||
name: string;
|
||||
icon: unknown;
|
||||
accentColor: string;
|
||||
textColor: string;
|
||||
logo?: { mode: 'emoticon' | 'upload'; value: string | null; size?: LogoSize };
|
||||
}) {
|
||||
const logoValue = logo?.mode === 'upload' ? (logo.value?.trim() || null) : null;
|
||||
const [logoFailed, setLogoFailed] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLogoFailed(false);
|
||||
}, [logoValue]);
|
||||
|
||||
const sizes = getLogoClasses(logo?.size);
|
||||
if (logo?.mode === 'upload' && logo.value) {
|
||||
if (logo?.mode === 'upload' && logoValue && !logoFailed) {
|
||||
return (
|
||||
<div className={`flex items-center justify-center rounded-full bg-white shadow-sm ${sizes.container}`}>
|
||||
<img src={logo.value} alt={name} className={`rounded-full object-contain ${sizes.image}`} />
|
||||
<img
|
||||
src={logoValue}
|
||||
alt={name}
|
||||
className={`rounded-full object-contain ${sizes.image}`}
|
||||
onError={() => setLogoFailed(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -140,7 +158,7 @@ function renderEventAvatar(
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full font-semibold text-sm shadow-sm"
|
||||
className={`flex items-center justify-center rounded-full font-semibold shadow-sm ${sizes.container} ${sizes.initials}`}
|
||||
style={{ backgroundColor: accentColor, color: textColor }}
|
||||
>
|
||||
{getInitials(name)}
|
||||
@@ -248,20 +266,26 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
: 'flex items-center gap-3'
|
||||
}
|
||||
>
|
||||
{renderEventAvatar(event.name, event.type?.icon, accentColor, headerTextColor, branding.logo)}
|
||||
<EventAvatar
|
||||
name={event.name}
|
||||
icon={event.type?.icon}
|
||||
accentColor={accentColor}
|
||||
textColor={headerTextColor}
|
||||
logo={branding.logo}
|
||||
/>
|
||||
<div
|
||||
className={`flex flex-col${logoPosition === 'center' ? ' items-center text-center' : ''}`}
|
||||
style={headerFont ? { fontFamily: headerFont } : undefined}
|
||||
>
|
||||
<div className="font-semibold text-lg">{event.name}</div>
|
||||
<div className="flex items-center gap-2 text-xs text-white/70" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||
<div className="flex items-center gap-2 text-xs opacity-70" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||
{stats && tasksEnabled && (
|
||||
<>
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="h-3 w-3" />
|
||||
<span>{`${stats.onlineGuests} ${t('header.stats.online')}`}</span>
|
||||
</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span className="opacity-50">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="font-medium">{stats.tasksSolved}</span>{' '}
|
||||
{t('header.stats.tasksSolved')}
|
||||
@@ -356,7 +380,7 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, butt
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="relative rounded-full bg-white/15 p-2 text-white transition hover:bg-white/30"
|
||||
className="relative rounded-full bg-white/15 p-2 text-current transition hover:bg-white/30"
|
||||
aria-label={open ? t('header.notifications.close', 'Benachrichtigungen schließen') : t('header.notifications.open', 'Benachrichtigungen anzeigen')}
|
||||
>
|
||||
<Bell className="h-5 w-5" aria-hidden />
|
||||
|
||||
Reference in New Issue
Block a user