import {useEffect, useState} from 'react'; import {Link, useParams} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {CircleCheck, Clock, Gamepad2, Library, Percent, Star, Trophy, UserRoundX} from 'lucide-react'; import {Card} from '@/components/ui/card'; import {Skeleton} from '@/components/ui/skeleton'; import GameCover from '../components/GameCover.jsx'; import GameStatusBadge, {STATUS_META} from '../components/GameStatusBadge.jsx'; import {GAME_STATUSES} from '../utils/backlog.js'; import {formatPlaytime} from '../utils/games.js'; import PageContainer from '../components/PageContainer.jsx'; import PileStats from '../components/stats/PileStats.jsx'; import TrendStats from '../components/stats/TrendStats.jsx'; import TopGamesStats from '../components/stats/TopGamesStats.jsx'; import TasteStats from '../components/stats/TasteStats.jsx'; import HabitsStats from '../components/stats/HabitsStats.jsx'; import FinishedStats from '../components/stats/FinishedStats.jsx'; /** * Public, no login. Auth returns a short-lived ticket for a public profile; Core returns the data for it. */ export default function PublicProfilePage() { const {t, i18n} = useTranslation(); const {name} = useParams(); const [profile, setProfile] = useState(null); const [library, setLibrary] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setProfile(null); setLibrary(null); setError(null); (async () => { try { const profileResponse = await fetch( `${import.meta.env.VITE_API_URL}/api/public/profiles/${encodeURIComponent(name)}`); if (cancelled) return; if (profileResponse.status === 404) { setError(t('PROFILE.NOT_FOUND')); return; } if (!profileResponse.ok) { setError(t('PROFILE.ERROR_LOAD')); return; } const found = await profileResponse.json(); const libraryResponse = await fetch(`${import.meta.env.VITE_CORE_API_URL}/api/public/profiles/library`, { headers: {'X-Profile-Ticket': found.ticket}, }); if (cancelled) return; if (!libraryResponse.ok) { setError(t('PROFILE.ERROR_LOAD')); return; } const data = await libraryResponse.json(); if (!cancelled) { setProfile(found); setLibrary(data); } } catch (err) { console.error('Public profile load error:', err); if (!cancelled) setError(t('INFO.ERROR.NETWORK_FAILURE')); } })(); return () => { cancelled = true; }; }, [name, t]); if (error) { return (

{error}

{t('PROFILE.TO_START')}
); } if (!library) { return ( ); } const finishedCount = (library.byStatus.FINISHED ?? 0) + (library.byStatus.COMPLETED ?? 0); const stats = [ [Library, library.totalGames.toLocaleString(i18n.language), t('PROFILE.GAMES')], [Clock, formatPlaytime(library.totalPlaytimeMinutes), t('PROFILE.PLAYTIME')], [CircleCheck, finishedCount.toLocaleString(i18n.language), t('PROFILE.FINISHED')], ]; if (library.averageCompletion != null) { stats.push( [Trophy, (library.perfectGames ?? 0).toLocaleString(i18n.language), t('PROFILE.PERFECT_GAMES')], [Percent, `${library.averageCompletion.toLocaleString(i18n.language)} %`, t('PROFILE.AVERAGE_COMPLETION')], ); } const memberSince = profile.memberSince ? new Date(`${profile.memberSince}T12:00:00`).toLocaleDateString(i18n.language, {year: 'numeric', month: 'long'}) : null; return (

{t('PROFILE.TITLE')}

{profile.displayName}

{memberSince && (

{t('PROFILE.MEMBER_SINCE', {date: memberSince})}

)}
3 ? 'grid-cols-2 sm:grid-cols-5' : 'grid-cols-3'}`}> {stats.map(([Icon, value, label]) => (
{value}
{label}
))}
{library.totalGames > 0 && (
{GAME_STATUSES.map((s) => (
))}
    {GAME_STATUSES.map((s) => (
  • {t(`BACKLOG.STATUS.${s}`)} {library.byStatus[s] ?? 0}
  • ))}
)} {library.playing.length > 0 && (

{t('PROFILE.PLAYING')}

    {library.playing.map((g) => (
  • {g.name ?? `App ${g.appid}`}
    {formatPlaytime(g.playtimeMinutes)}
  • ))}
)} {library.finished.length > 0 && (

{t('PROFILE.FINISHED_GAMES')}

    {library.finished.map((g) => (
  • {g.name ?? `App ${g.appid}`} {g.rating && ( {Array.from({length: g.rating}, (_, i) => ( ))} )}
  • ))}
)} {library.stats && } ); } // Only the groups the user shares come back; the game rows do not link, the library pages are private. function SharedStats({stats}) { return (
{stats.pile && } {stats.trend && } {stats.topGames && } {stats.taste && } {stats.habits && } {stats.finished && }
); }