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.
This commit is contained in:
1 parent
720764bf68
commit
5be0d42bca
31 files changed
+692
-67
No files matched your search
@@ -0,0 +1,63 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user