import React, { useEffect, useMemo, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Separator } from '@/components/ui/separator'; import { AnimatePresence, motion } from 'framer-motion'; import { AchievementBadge, AchievementsPayload, FeedEntry, LeaderboardEntry, TimelinePoint, TopPhotoHighlight, TrendingEmotionHighlight, fetchAchievements, } from '../services/achievementApi'; import { useGuestIdentity } from '../context/GuestIdentityContext'; import { Sparkles, Award, Trophy, Camera, Users, BarChart2, Flame } from 'lucide-react'; import { useTranslation, type TranslateFn } from '../i18n/useTranslation'; import type { LocaleCode } from '../i18n/messages'; import { localizeTaskLabel } from '../lib/localizeTaskLabel'; import { useEventData } from '../hooks/useEventData'; import { isTaskModeEnabled } from '../lib/engagement'; import { FADE_SCALE, FADE_UP, STAGGER_FAST, getMotionContainerProps, getMotionItemProps, prefersReducedMotion } from '../lib/motion'; import PullToRefresh from '../components/PullToRefresh'; const GENERIC_ERROR = 'GENERIC_ERROR'; function formatRelativeTimestamp(input: string, formatter: Intl.RelativeTimeFormat): string { const date = new Date(input); if (Number.isNaN(date.getTime())) return ''; const diff = date.getTime() - Date.now(); const minute = 60_000; const hour = 60 * minute; const day = 24 * hour; const abs = Math.abs(diff); if (abs < minute) return formatter.format(0, 'second'); if (abs < hour) return formatter.format(Math.round(diff / minute), 'minute'); if (abs < day) return formatter.format(Math.round(diff / hour), 'hour'); return formatter.format(Math.round(diff / day), 'day'); } type LeaderboardProps = { title: string; description: string; icon: React.ElementType; entries: LeaderboardEntry[]; emptyCopy: string; formatNumber: (value: number) => string; t: TranslateFn; }; function Leaderboard({ title, description, icon: Icon, entries, emptyCopy, formatNumber, t }: LeaderboardProps) { return (
{title} {description}
{entries.length === 0 ? (

{emptyCopy}

) : (
    {entries.map((entry, index) => (
  1. #{index + 1} {entry.guest || t('achievements.leaderboard.guestFallback')}
    {t('achievements.leaderboard.item.photos', { count: formatNumber(entry.photos) })} {t('achievements.leaderboard.item.likes', { count: formatNumber(entry.likes) })}
  2. ))}
)}
); } type BadgesGridProps = { badges: AchievementBadge[]; t: TranslateFn; }; export function BadgesGrid({ badges, t }: BadgesGridProps) { if (badges.length === 0) { return ( {t('achievements.badges.title')} {t('achievements.badges.description')}

{t('achievements.badges.empty')}

); } return ( {t('achievements.badges.title')} {t('achievements.badges.description')} {badges.map((badge) => { const target = badge.target ?? 0; const progress = badge.progress ?? 0; const ratio = target > 0 ? Math.min(1, progress / target) : 0; const percentage = Math.round((badge.earned ? 1 : ratio) * 100); return (

{badge.title}

{badge.description}

{percentage}%
); })} ); } type TimelineProps = { points: TimelinePoint[]; t: TranslateFn; formatNumber: (value: number) => string; }; function Timeline({ points, t, formatNumber }: TimelineProps) { return ( {t('achievements.timeline.title')} {t('achievements.timeline.description')} {points.map((point) => (
{point.date} {t('achievements.timeline.row', { photos: formatNumber(point.photos), guests: formatNumber(point.guests) })}
))}
); } type FeedProps = { feed: FeedEntry[]; t: TranslateFn; formatRelativeTime: (value: string) => string; locale: LocaleCode; formatNumber: (value: number) => string; }; function Feed({ feed, t, formatRelativeTime, locale, formatNumber }: FeedProps) { if (feed.length === 0) { return ( {t('achievements.feed.title')} {t('achievements.feed.description')}

{t('achievements.feed.empty')}

); } return ( {t('achievements.feed.title')} {t('achievements.feed.description')} {feed.map((item) => { const taskLabel = localizeTaskLabel(item.task ?? null, locale); return (
{item.thumbnail ? ( {t('achievements.feed.thumbnailAlt')} ) : (
)}

{item.guest || t('achievements.leaderboard.guestFallback')}

{taskLabel &&

{t('achievements.feed.taskLabel', { task: taskLabel })}

}
{formatRelativeTime(item.createdAt)} {t('achievements.feed.likesLabel', { count: formatNumber(item.likes) })}
); })}
); } type HighlightsProps = { topPhoto: TopPhotoHighlight | null; trendingEmotion: TrendingEmotionHighlight | null; t: TranslateFn; formatRelativeTime: (value: string) => string; locale: LocaleCode; formatNumber: (value: number) => string; }; function Highlights({ topPhoto, trendingEmotion, t, formatRelativeTime, locale, formatNumber }: HighlightsProps) { if (!topPhoto && !trendingEmotion) { return null; } const renderTopPhoto = () => { if (!topPhoto) return null; const localizedTask = localizeTaskLabel(topPhoto.task ?? null, locale); return (
{t('achievements.highlights.topTitle')} {t('achievements.highlights.topDescription')}
{topPhoto.thumbnail ? ( {t('achievements.highlights.topTitle')} ) : (
{t('achievements.highlights.noPreview')}
)}

{topPhoto.guest || t('achievements.leaderboard.guestFallback')} {` – ${t('achievements.highlights.likesAmount', { count: formatNumber(topPhoto.likes) })}`}

{localizedTask && (

{t('achievements.highlights.taskLabel', { task: localizedTask })}

)}

{formatRelativeTime(topPhoto.createdAt)}

); }; const renderTrendingEmotion = () => { if (!trendingEmotion) return null; return (
{t('achievements.highlights.trendingTitle')} {t('achievements.highlights.trendingDescription')}

{trendingEmotion.name}

{t('achievements.highlights.trendingCount', { count: formatNumber(trendingEmotion.count) })}

); }; return (
{renderTopPhoto()} {renderTrendingEmotion()}
); } type PersonalActionsProps = { token: string; t: TranslateFn; tasksEnabled: boolean; }; function PersonalActions({ token, t, tasksEnabled }: PersonalActionsProps) { return (
{tasksEnabled ? ( ) : null}
); } export default function AchievementsPage() { const { token } = useParams<{ token: string }>(); const identity = useGuestIdentity(); const { t, locale } = useTranslation(); const { event } = useEventData(); const tasksEnabled = isTaskModeEnabled(event); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState<'personal' | 'event' | 'feed'>('personal'); const numberFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]); const formatNumber = (value: number) => numberFormatter.format(value); const relativeFormatter = useMemo(() => new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }), [locale]); const formatRelative = (value: string) => formatRelativeTimestamp(value, relativeFormatter); const personalName = identity.hydrated && identity.name ? identity.name : undefined; const loadAchievements = React.useCallback(async (signal?: AbortSignal) => { if (!token) return; setLoading(true); setError(null); try { const payload = await fetchAchievements(token, { guestName: personalName, locale, signal, }); setData(payload); if (!payload.personal) { setActiveTab('event'); } } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return; console.error('Failed to load achievements', err); setError(err instanceof Error ? err.message : GENERIC_ERROR); } finally { if (!signal?.aborted) { setLoading(false); } } }, [locale, personalName, token]); useEffect(() => { const controller = new AbortController(); void loadAchievements(controller.signal); return () => controller.abort(); }, [loadAchievements]); const hasPersonal = Boolean(data?.personal); const motionEnabled = !prefersReducedMotion(); const containerMotion = getMotionContainerProps(motionEnabled, STAGGER_FAST); const fadeUpMotion = getMotionItemProps(motionEnabled, FADE_UP); const fadeScaleMotion = getMotionItemProps(motionEnabled, FADE_SCALE); const tabMotion = motionEnabled ? { variants: FADE_UP, initial: 'hidden', animate: 'show', exit: 'hidden' as const } : {}; const handleRefresh = React.useCallback(async () => { await loadAchievements(); }, [loadAchievements]); if (!token) { return null; } const tabContent = ( <> {activeTab === 'personal' && hasPersonal && data?.personal && (
{t('achievements.personal.greeting', { name: data.personal.guestName || identity.name || t('achievements.leaderboard.guestFallback') })} {t('achievements.personal.stats', { photos: formatNumber(data.personal.photos), tasks: formatNumber(data.personal.tasks), likes: formatNumber(data.personal.likes), })}
)} {activeTab === 'event' && data && (
)} {activeTab === 'feed' && data && ( )} ); return (

{t('achievements.page.title')}

{t('achievements.page.subtitle')}

{loading && ( )} {!loading && error && ( {error === GENERIC_ERROR ? t('achievements.page.loadError') : error} )} {!loading && !error && data && ( <> {motionEnabled ? ( {tabContent} ) : ( {tabContent} )} )}
); }