import React, { useEffect, useMemo, useState } from 'react';
import { Link, useNavigationType, 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, getMotionContainerPropsForNavigation, 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) => (
-
#{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) })}
))}
)}
);
}
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 ? (

) : (
)}
{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.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 navigationType = useNavigationType();
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 = getMotionContainerPropsForNavigation(motionEnabled, STAGGER_FAST, navigationType);
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}
)}
>
)}
);
}