enhancements of the homepage in the guest pwa
This commit is contained in:
@@ -110,51 +110,54 @@ export default function EmotionPicker({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-3 pb-2',
|
||||
variant === 'standalone' ? 'grid-cols-2 sm:grid-cols-3' : 'grid-cols-2 sm:grid-cols-3'
|
||||
)}
|
||||
aria-label="Emotions"
|
||||
>
|
||||
{emotions.map((emotion) => {
|
||||
// Localize name and description if they are JSON
|
||||
const localize = (value: string | object, defaultValue: string = ''): string => {
|
||||
if (typeof value === 'string' && value.startsWith('{')) {
|
||||
try {
|
||||
const data = JSON.parse(value as string);
|
||||
return data.de || data.en || defaultValue || '';
|
||||
} catch {
|
||||
return value as string;
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'grid grid-rows-2 grid-flow-col auto-cols-[170px] sm:auto-cols-[190px] gap-3 overflow-x-auto pb-2 pr-12',
|
||||
'scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent'
|
||||
)}
|
||||
aria-label="Emotions"
|
||||
>
|
||||
{emotions.map((emotion) => {
|
||||
// Localize name and description if they are JSON
|
||||
const localize = (value: string | object, defaultValue: string = ''): string => {
|
||||
if (typeof value === 'string' && value.startsWith('{')) {
|
||||
try {
|
||||
const data = JSON.parse(value as string);
|
||||
return data.de || data.en || defaultValue || '';
|
||||
} catch {
|
||||
return value as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
return value as string;
|
||||
};
|
||||
return value as string;
|
||||
};
|
||||
|
||||
const localizedName = localize(emotion.name, emotion.name);
|
||||
const localizedDescription = localize(emotion.description || '', '');
|
||||
return (
|
||||
<button
|
||||
key={emotion.id}
|
||||
type="button"
|
||||
onClick={() => handleEmotionSelect(emotion)}
|
||||
className="group flex min-w-[180px] flex-col gap-2 rounded-2xl border border-white/40 bg-white/80 px-4 py-3 text-left shadow-sm transition hover:-translate-y-0.5 hover:border-pink-200 hover:shadow-md dark:border-gray-700/60 dark:bg-gray-900/70"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl" aria-hidden>
|
||||
{emotion.emoji}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-foreground line-clamp-1">{localizedName}</div>
|
||||
{localizedDescription && (
|
||||
<div className="text-xs text-muted-foreground line-clamp-1">{localizedDescription}</div>
|
||||
)}
|
||||
const localizedName = localize(emotion.name, emotion.name);
|
||||
const localizedDescription = localize(emotion.description || '', '');
|
||||
return (
|
||||
<button
|
||||
key={emotion.id}
|
||||
type="button"
|
||||
onClick={() => handleEmotionSelect(emotion)}
|
||||
className="group flex flex-col gap-2 rounded-2xl border border-muted/40 bg-white/80 px-4 py-3 text-left shadow-sm transition hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md dark:border-gray-700/60 dark:bg-gray-900/70"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl" aria-hidden>
|
||||
{emotion.emoji}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-foreground">{localizedName}</div>
|
||||
{localizedDescription && (
|
||||
<div className="text-xs text-muted-foreground line-clamp-2">{localizedDescription}</div>
|
||||
)}
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground opacity-0 transition group-hover:opacity-100" />
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground opacity-0 transition group-hover:opacity-100" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 w-10 bg-gradient-to-l from-[var(--guest-background)] via-[var(--guest-background)]/90 to-transparent dark:from-black dark:via-black/80" aria-hidden />
|
||||
</div>
|
||||
|
||||
{/* Skip option */}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getDeviceId } from '../lib/device';
|
||||
import { usePollGalleryDelta } from '../polling/usePollGalleryDelta';
|
||||
import { Heart } from 'lucide-react';
|
||||
import { useTranslation } from '../i18n/useTranslation';
|
||||
import { useEventBranding } from '../context/EventBrandingContext';
|
||||
|
||||
type Props = { token: string };
|
||||
|
||||
@@ -27,10 +28,15 @@ type PreviewPhoto = {
|
||||
|
||||
export default function GalleryPreview({ token }: Props) {
|
||||
const { locale } = useTranslation();
|
||||
const { branding } = useEventBranding();
|
||||
const { photos, loading } = usePollGalleryDelta(token, locale);
|
||||
const [mode, setMode] = React.useState<PreviewFilter>('latest');
|
||||
const typedPhotos = React.useMemo(() => photos as PreviewPhoto[], [photos]);
|
||||
const hasPhotobooth = React.useMemo(() => typedPhotos.some((p) => p.ingest_source === 'photobooth'), [typedPhotos]);
|
||||
const radius = branding.buttons?.radius ?? 12;
|
||||
const linkColor = branding.buttons?.linkColor ?? branding.secondaryColor;
|
||||
const headingFont = branding.typography?.heading ?? branding.fontFamily ?? undefined;
|
||||
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
let arr = typedPhotos.slice();
|
||||
@@ -84,64 +90,82 @@ export default function GalleryPreview({ token }: Props) {
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-muted-foreground">Live-Galerie</p>
|
||||
<h3 className="text-lg font-semibold text-foreground">Alle Uploads auf einen Blick</h3>
|
||||
<Card className="border border-muted/30 shadow-sm" style={{ borderRadius: radius, background: 'var(--guest-surface)', fontFamily: bodyFont }}>
|
||||
<CardContent className="space-y-3 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-muted-foreground" style={headingFont ? { fontFamily: headingFont } : undefined}>Live-Galerie</p>
|
||||
<h3 className="text-lg font-semibold text-foreground" style={headingFont ? { fontFamily: headingFont } : undefined}>Alle Uploads auf einen Blick</h3>
|
||||
</div>
|
||||
<Link
|
||||
to={`/e/${encodeURIComponent(token)}/gallery?mode=${mode}`}
|
||||
className="text-sm font-semibold transition"
|
||||
style={{ color: linkColor }}
|
||||
>
|
||||
Alle ansehen →
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
to={`/e/${encodeURIComponent(token)}/gallery?mode=${mode}`}
|
||||
className="text-sm font-semibold text-pink-600 hover:text-pink-700"
|
||||
>
|
||||
Alle ansehen →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 overflow-x-auto pb-1 text-sm font-medium [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||
<div className="flex gap-2 overflow-x-auto pb-1 text-sm font-medium [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||
{filters.map((filter) => (
|
||||
<button
|
||||
key={filter.value}
|
||||
type="button"
|
||||
onClick={() => setMode(filter.value)}
|
||||
className={`rounded-full border px-4 py-1 transition ${
|
||||
mode === filter.value
|
||||
? 'border-pink-500 bg-pink-500 text-white shadow'
|
||||
: 'border-transparent bg-white/70 text-muted-foreground hover:border-pink-200'
|
||||
}`}
|
||||
style={{
|
||||
borderRadius: radius,
|
||||
border: mode === filter.value ? `1px solid ${branding.primaryColor}` : `1px solid ${branding.primaryColor}22`,
|
||||
background: mode === filter.value ? branding.primaryColor : 'var(--guest-surface)',
|
||||
color: mode === filter.value ? '#ffffff' : 'var(--foreground)',
|
||||
boxShadow: mode === filter.value ? `0 8px 18px ${branding.primaryColor}33` : 'none',
|
||||
}}
|
||||
className="px-4 py-1 transition"
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Lädt…</p>}
|
||||
{!loading && items.length === 0 && (
|
||||
<Card>
|
||||
<CardContent className="p-3 text-sm text-muted-foreground">
|
||||
{loading && <p className="text-sm text-muted-foreground">Lädt…</p>}
|
||||
{!loading && items.length === 0 && (
|
||||
<div className="flex items-center gap-3 rounded-xl border border-muted/30 bg-[var(--guest-surface)] p-3 text-sm text-muted-foreground">
|
||||
<Heart className="h-4 w-4" style={{ color: branding.secondaryColor }} aria-hidden />
|
||||
Noch keine Fotos. Starte mit deinem ersten Upload!
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{items.map((p: PreviewPhoto) => (
|
||||
<Link
|
||||
key={p.id}
|
||||
to={`/e/${encodeURIComponent(token)}/gallery?photoId=${p.id}`}
|
||||
className="group relative block overflow-hidden rounded-3xl border border-white/30 bg-gray-900 text-white shadow-lg"
|
||||
className="group relative block overflow-hidden text-foreground"
|
||||
style={{
|
||||
borderRadius: radius,
|
||||
border: `1px solid ${branding.primaryColor}22`,
|
||||
background: 'var(--guest-surface)',
|
||||
boxShadow: `0 12px 26px ${branding.primaryColor}22`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={p.thumbnail_path || p.file_path}
|
||||
alt={p.title || 'Foto'}
|
||||
className="h-48 w-full object-cover transition duration-300 group-hover:scale-105"
|
||||
className="h-40 w-full object-cover transition duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/10 to-transparent" aria-hidden />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||
<p className="text-sm font-semibold leading-tight line-clamp-2">{p.title || getPhotoTitle(p)}</p>
|
||||
<div className="mt-2 flex items-center gap-1 text-xs text-white/80">
|
||||
<Heart className="h-4 w-4 fill-current" aria-hidden />
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: `linear-gradient(180deg, transparent 50%, ${branding.primaryColor}33 100%)`,
|
||||
}}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="absolute bottom-0 left-0 right-0 space-y-1 p-3">
|
||||
<p className="text-sm font-semibold leading-tight line-clamp-2" style={headingFont ? { fontFamily: headingFont } : undefined}>
|
||||
{p.title || getPhotoTitle(p)}
|
||||
</p>
|
||||
<div className="flex items-center gap-1 text-xs text-foreground/80">
|
||||
<Heart className="h-4 w-4" style={{ color: branding.primaryColor }} aria-hidden />
|
||||
{p.likes_count ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,12 +173,13 @@ export default function GalleryPreview({ token }: Props) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Lust auf mehr?{' '}
|
||||
<Link to={`/e/${encodeURIComponent(token)}/gallery`} className="font-semibold text-pink-600 hover:text-pink-700">
|
||||
Zur Galerie →
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Lust auf mehr?{' '}
|
||||
<Link to={`/e/${encodeURIComponent(token)}/gallery`} className="font-semibold transition" style={{ color: linkColor }}>
|
||||
Zur Galerie →
|
||||
</Link>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,19 +172,13 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
}, [notificationsOpen]);
|
||||
|
||||
if (!eventToken) {
|
||||
const guestName = identity?.name && identity?.hydrated ? identity.name : null;
|
||||
return (
|
||||
<div
|
||||
className="guest-header sticky top-0 z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 backdrop-blur dark:bg-black/40"
|
||||
className="guest-header z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 dark:bg-black/40"
|
||||
style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="font-semibold">{title}</div>
|
||||
{guestName && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{`${t('common.hi')} ${guestName}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<AppearanceToggleDropdown />
|
||||
@@ -194,20 +188,20 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
);
|
||||
}
|
||||
|
||||
const guestName =
|
||||
identity && identity.eventKey === eventToken && identity.hydrated && identity.name ? identity.name : null;
|
||||
const headerFont = branding.typography?.heading ?? branding.fontFamily ?? undefined;
|
||||
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
background: `linear-gradient(135deg, ${branding.primaryColor}, ${branding.secondaryColor})`,
|
||||
color: headerTextColor,
|
||||
fontFamily: branding.fontFamily ?? undefined,
|
||||
fontFamily: headerFont,
|
||||
};
|
||||
|
||||
const accentColor = branding.secondaryColor;
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="guest-header sticky top-0 z-20 flex items-center justify-between border-b border-white/10 px-4 py-2 text-white shadow-sm backdrop-blur" style={headerStyle}>
|
||||
<div className="guest-header z-20 flex items-center justify-between border-b border-white/10 px-4 py-2 text-white shadow-sm backdrop-blur" style={headerStyle}>
|
||||
<div className="font-semibold" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>{t('header.loading')}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<AppearanceToggleDropdown />
|
||||
@@ -225,19 +219,14 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
||||
statsContext && statsContext.eventKey === eventToken ? statsContext : undefined;
|
||||
return (
|
||||
<div
|
||||
className="guest-header sticky top-0 z-20 flex items-center justify-between border-b border-white/10 px-4 py-3 text-white shadow-sm backdrop-blur"
|
||||
className="guest-header z-20 flex items-center justify-between border-b border-white/10 px-4 py-3 text-white shadow-sm backdrop-blur"
|
||||
style={headerStyle}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{renderEventAvatar(event.name, event.type?.icon, accentColor, headerTextColor, branding.logo)}
|
||||
<div className="flex flex-col" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>
|
||||
<div className="font-semibold text-base">{event.name}</div>
|
||||
{guestName && (
|
||||
<span className="text-xs text-white/80">
|
||||
{`${t('common.hi')} ${guestName}`}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-xs text-white/70">
|
||||
<div className="flex flex-col" 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}>
|
||||
{stats && (
|
||||
<>
|
||||
<span className="flex items-center gap-1">
|
||||
|
||||
Reference in New Issue
Block a user