Public Access
Feed links go to the game and to the profile view of that game. Add /following page, nav item and an opt-in Following tile. Games not in your library show a catalog view with an add button.
64 lines
2.6 KiB
React
64 lines
2.6 KiB
React
import {useEffect, useState} from 'react';
|
|
import {useTranslation} from 'react-i18next';
|
|
import {Trophy} from 'lucide-react';
|
|
|
|
import {Card} from '@/components/ui/card';
|
|
import FitText from './FitText.jsx';
|
|
import GameCover from './GameCover.jsx';
|
|
import GameStatusBadge from './GameStatusBadge.jsx';
|
|
import {formatPlaytime} from '../utils/games.js';
|
|
import {fetchProfileGame} from '../utils/people.js';
|
|
|
|
/**
|
|
* One game of a public profile, above its tiles. Shows what the profile shares of it; a game the user hides or does
|
|
* not have gets one message.
|
|
*/
|
|
export default function ProfileGameCard({ticket, appid}) {
|
|
const {t, i18n} = useTranslation();
|
|
const [game, setGame] = useState(null);
|
|
const [missing, setMissing] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setGame(null);
|
|
setMissing(false);
|
|
(async () => {
|
|
const {ok, data} = await fetchProfileGame(ticket, appid);
|
|
if (cancelled) return;
|
|
if (ok) setGame(data);
|
|
else setMissing(true);
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [ticket, appid]);
|
|
|
|
if (missing) {
|
|
return <Card className="p-5 text-sm text-muted-foreground">{t('PROFILE.GAME_NOT_SHOWN')}</Card>;
|
|
}
|
|
if (!game) return null;
|
|
|
|
return (
|
|
<Card className="gap-4 p-5 sm:flex-row sm:items-center">
|
|
<GameCover appid={game.appid} imageUrl={game.imageUrl} className="w-full sm:w-56 sm:shrink-0"/>
|
|
<div className="min-w-0 flex-1 space-y-2">
|
|
<div className="@container text-lg font-semibold"><FitText text={game.name ?? `App ${game.appid}`}/></div>
|
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
|
{game.status && <GameStatusBadge status={game.status}/>}
|
|
{game.playtimeMinutes != null && <span className="tabular-nums">{formatPlaytime(game.playtimeMinutes)}</span>}
|
|
{game.achievementsTotal != null && (
|
|
<span className="flex items-center gap-1 tabular-nums">
|
|
<Trophy className="size-4 text-primary"/>
|
|
{t('PROFILE.GAME_ACHIEVEMENTS', {
|
|
unlocked: game.achievementsUnlocked,
|
|
total: game.achievementsTotal,
|
|
percent: game.completionPercent.toLocaleString(i18n.language),
|
|
})}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|