import React from 'react';
import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text';
import { Button } from '@tamagui/button';
import { Award, BarChart2, Camera, Flame, Sparkles, Trophy, Users } from 'lucide-react';
import AppShell from '../components/AppShell';
import PullToRefresh from '@/shared/guest/components/PullToRefresh';
import { useEventData } from '../context/EventDataContext';
import { useOptionalGuestIdentity } from '../context/GuestIdentityContext';
import {
fetchAchievements,
type AchievementBadge,
type AchievementsPayload,
type FeedEntry,
type LeaderboardEntry,
type TimelinePoint,
type TopPhotoHighlight,
type TrendingEmotionHighlight,
} from '../services/achievementsApi';
import { useTranslation } from '@/shared/guest/i18n/useTranslation';
import { useLocale } from '@/shared/guest/i18n/LocaleContext';
import { useGuestThemeVariant } from '../lib/guestTheme';
import { getBentoSurfaceTokens } from '../lib/bento';
import { localizeTaskLabel } from '@/shared/guest/lib/localizeTaskLabel';
import { buildEventPath } from '../lib/routes';
import { useNavigate } from 'react-router-dom';
const GENERIC_ERROR = 'GENERIC_ERROR';
type TabKey = 'personal' | 'event' | 'feed';
type BentoCardProps = {
children: React.ReactNode;
isDark: boolean;
padding?: number;
gradient?: string;
};
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');
}
function BentoCard({ children, isDark, padding = 16, gradient }: BentoCardProps) {
const surface = getBentoSurfaceTokens(isDark);
return (
{children}
);
}
type LeaderboardProps = {
title: string;
description: string;
icon: React.ElementType;
entries: LeaderboardEntry[];
emptyCopy: string;
formatNumber: (value: number) => string;
guestFallback: string;
isDark: boolean;
};
function Leaderboard({ title, description, icon: Icon, entries, emptyCopy, formatNumber, guestFallback, isDark }: LeaderboardProps) {
return (
{title}
{description}
{entries.length === 0 ? (
{emptyCopy}
) : (
{entries.map((entry, index) => (
#{index + 1}
{entry.guest || guestFallback}
{formatNumber(entry.photos)}
{formatNumber(entry.likes)}
))}
)}
);
}
type BadgesGridProps = {
badges: AchievementBadge[];
emptyCopy: string;
completeCopy: string;
isDark: boolean;
};
function BadgesGrid({ badges, emptyCopy, completeCopy, isDark }: BadgesGridProps) {
if (badges.length === 0) {
return (
{emptyCopy}
);
}
return (
{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}%
{badge.earned ? completeCopy : `${progress}/${target}`}
);
})}
);
}
type TimelineProps = {
points: TimelinePoint[];
formatNumber: (value: number) => string;
emptyCopy: string;
isDark: boolean;
};
function Timeline({ points, formatNumber, emptyCopy, isDark }: TimelineProps) {
if (points.length === 0) {
return (
{emptyCopy}
);
}
return (
{points.map((point) => (
{point.date}
{formatNumber(point.photos)} photos · {formatNumber(point.guests)} guests
))}
);
}
type HighlightsProps = {
topPhoto: TopPhotoHighlight | null;
trendingEmotion: TrendingEmotionHighlight | null;
formatRelativeTime: (value: string) => string;
locale: string;
formatNumber: (value: number) => string;
emptyCopy: string;
topPhotoTitle: string;
topPhotoNoPreview: string;
topPhotoFallbackGuest: string;
trendingTitle: string;
trendingCountLabel: string;
isDark: boolean;
};
function Highlights({
topPhoto,
trendingEmotion,
formatRelativeTime,
locale,
formatNumber,
emptyCopy,
topPhotoTitle,
topPhotoNoPreview,
topPhotoFallbackGuest,
trendingTitle,
trendingCountLabel,
isDark,
}: HighlightsProps) {
if (!topPhoto && !trendingEmotion) {
return (
{emptyCopy}
);
}
return (
{topPhoto ? (
{topPhotoTitle}
{topPhoto.thumbnail ? (
) : (
{topPhotoNoPreview}
)}
{topPhoto.guest || topPhotoFallbackGuest}
{formatNumber(topPhoto.likes)} likes · {formatRelativeTime(topPhoto.createdAt)}
{topPhoto.task ? (
{localizeTaskLabel(topPhoto.task ?? null, locale) ?? topPhoto.task}
) : null}
) : null}
{trendingEmotion ? (
{trendingTitle}
{trendingEmotion.name}
{trendingCountLabel.replace('{count}', formatNumber(trendingEmotion.count))}
) : null}
);
}
type FeedProps = {
feed: FeedEntry[];
formatRelativeTime: (value: string) => string;
locale: string;
formatNumber: (value: number) => string;
emptyCopy: string;
guestFallback: string;
likesLabel: string;
isDark: boolean;
};
function Feed({ feed, formatRelativeTime, locale, formatNumber, emptyCopy, guestFallback, likesLabel, isDark }: FeedProps) {
if (feed.length === 0) {
return (
{emptyCopy}
);
}
return (
{feed.map((item) => {
const taskLabel = localizeTaskLabel(item.task ?? null, locale);
return (
{item.thumbnail ? (
) : (
)}
{item.guest || guestFallback}
{taskLabel ? (
{taskLabel}
) : null}
{formatRelativeTime(item.createdAt)}
{likesLabel.replace('{count}', formatNumber(item.likes))}
);
})}
);
}
export default function AchievementsScreen() {
const { token, tasksEnabled } = useEventData();
const identity = useOptionalGuestIdentity();
const { t } = useTranslation();
const { locale } = useLocale();
const { isDark } = useGuestThemeVariant();
const navigate = useNavigate();
const mutedButton = isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(15, 23, 42, 0.06)';
const mutedButtonBorder = isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(15, 23, 42, 0.12)';
const [payload, setPayload] = React.useState(null);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
const [activeTab, setActiveTab] = React.useState('personal');
const numberFormatter = React.useMemo(() => new Intl.NumberFormat(locale), [locale]);
const formatNumber = (value: number) => numberFormatter.format(value);
const relativeFormatter = React.useMemo(() => new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }), [locale]);
const formatRelative = (value: string) => formatRelativeTimestamp(value, relativeFormatter);
const loadAchievements = React.useCallback(async (forceRefresh = false) => {
if (!token) return;
setLoading(true);
setError(null);
try {
const data = await fetchAchievements(token, {
guestName: identity?.name ?? undefined,
locale,
forceRefresh,
});
setPayload(data);
setActiveTab(data.personal ? 'personal' : 'event');
} catch (err) {
console.error('Failed to load achievements', err);
setError(err instanceof Error ? err.message : GENERIC_ERROR);
} finally {
setLoading(false);
}
}, [identity?.name, locale, token]);
React.useEffect(() => {
loadAchievements();
}, [loadAchievements]);
if (!token) {
return null;
}
const hasPersonal = Boolean(payload?.personal);
const summary = payload?.summary;
const personal = payload?.personal ?? null;
const leaderboards = payload?.leaderboards ?? null;
const highlights = payload?.highlights ?? null;
const feed = payload?.feed ?? [];
const headerGradient = isDark
? 'radial-gradient(120% 120% at 20% 20%, rgba(79, 209, 255, 0.16), transparent 60%), radial-gradient(120% 120% at 80% 0%, rgba(244, 63, 94, 0.18), transparent 60%)'
: 'radial-gradient(140% 140% at 20% 10%, color-mix(in oklab, var(--guest-primary, #0EA5E9) 22%, white), transparent 55%), radial-gradient(120% 120% at 80% 0%, color-mix(in oklab, var(--guest-secondary, #F43F5E) 22%, white), transparent 60%)';
const content = (
{t('achievements.page.title', 'Achievements')}
{t('achievements.page.subtitle', 'Track your milestones and highlight streaks.')}
{summary ? (
{[
{ label: t('achievements.summary.photosShared', 'Photos shared'), value: summary.totalPhotos },
...(tasksEnabled
? [{ label: t('achievements.summary.tasksCompleted', 'Tasks completed'), value: summary.tasksSolved }]
: []),
{ label: t('achievements.summary.likesCollected', 'Likes collected'), value: summary.likesTotal },
{ label: t('achievements.summary.uniqueGuests', 'Guests involved'), value: summary.uniqueGuests },
].map((item) => (
{formatNumber(item.value)}
{item.label}
))}
) : null}
{([
{ key: 'personal', label: t('achievements.page.buttons.personal', 'Personal'), icon: Sparkles, disabled: !hasPersonal },
{ key: 'event', label: t('achievements.page.buttons.event', 'Event'), icon: Users, disabled: false },
{ key: 'feed', label: t('achievements.page.buttons.feed', 'Feed'), icon: BarChart2, disabled: false },
] as const).map((tab) => (
))}
{loading ? (
{Array.from({ length: 3 }).map((_, index) => (
))}
) : error ? (
{error === GENERIC_ERROR
? t('achievements.page.loadError', 'Achievements could not be loaded.')
: error}
) : payload ? (
{activeTab === 'personal' ? (
{t('achievements.badges.title', 'Badges')}
{t('achievements.badges.description', 'Unlock achievements and keep your streak going.')}
) : null}
{activeTab === 'event' ? (
{t('achievements.timeline.title', 'Timeline')}
{t('achievements.timeline.description', 'Daily momentum snapshot.')}
) : null}
{activeTab === 'feed' ? (
) : null}
) : null}
);
return (
loadAchievements(true)}
pullLabel={t('common.pullToRefresh')}
releaseLabel={t('common.releaseToRefresh')}
refreshingLabel={t('common.refreshing')}
>
{content}
);
}