Fix guest demo UX and enforce guest limits

This commit is contained in:
Codex Agent
2026-01-21 21:35:40 +01:00
parent a01a7ec399
commit 80dd12bb92
28 changed files with 812 additions and 118 deletions

View 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>
);
}

View File

@@ -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 />