Public Access
The Dashboard loads the top and playing tiles from GET /api/library/dashboard-games in one call. The game page shows the stored game first (refresh=false) and calls again only when achievementsStale is true. The second answer merges the achievement fields; it takes status and finishedHint only when the user did not change the status. The points check runs again after the refresh. Refs #47
50 lines
2.5 KiB
React
50 lines
2.5 KiB
React
import {useEffect, useRef, useState} from 'react';
|
|
import {useTranslation} from 'react-i18next';
|
|
import usePointsCheck from '../hooks/usePointsCheck.js';
|
|
import {formatDate, formatDay} from '../utils/format.js';
|
|
import {STATUS_NOT_REACHED} from '../utils/rewards.js';
|
|
|
|
const WAITING = {NO_PROOF: 'POINTS.GAME.WAITING', NO_PLAY_AFTER_START: 'POINTS.GAME.WAITING_START'};
|
|
// The rest of an early finish waits for Steam play, with or without playtime on record.
|
|
const WAITING_TOPUP = ['NO_PROOF', 'NO_PLAYTIME'];
|
|
|
|
const waitingKey = (kind, reason) => (kind === 'FINISHED_TOPUP' ? WAITING_TOPUP.includes(reason) && 'POINTS.GAME.WAITING_TOPUP' : WAITING[reason]);
|
|
|
|
/**
|
|
* What the game earned: one line per paid row and one per kind that is missing, with the reason from Core. Loading it
|
|
* settles the points; a status change after the first load also shows the points toast. `refreshes` loads it again
|
|
* after the Steam refresh of the game page, which may have stored an unlock that settles points.
|
|
*/
|
|
export default function GamePointsLine({game, refreshes = 0}) {
|
|
const {t, i18n} = useTranslation();
|
|
const check = usePointsCheck();
|
|
const [points, setPoints] = useState(null);
|
|
const lastStatus = useRef(null);
|
|
|
|
useEffect(() => {
|
|
const changed = lastStatus.current !== null && lastStatus.current !== game.status;
|
|
lastStatus.current = game.status;
|
|
let stale = false;
|
|
check(game.appid, {status: game.status, tell: changed}).then((data) => {
|
|
if (!stale && data) setPoints(data);
|
|
});
|
|
return () => {
|
|
stale = true;
|
|
};
|
|
}, [check, game.appid, game.status, refreshes]);
|
|
|
|
if (!points) return null;
|
|
const earlySinceText = points.earlySince && formatDay(points.earlySince, i18n.language, {dateStyle: 'medium'});
|
|
const kindText = (kind) => t(`POINTS.KINDS.${kind}`);
|
|
const lines = [
|
|
...points.paid.map((row) => t('POINTS.GAME.PAID', {
|
|
kind: kindText(row.kind), points: row.points,
|
|
date: formatDate(row.paidAt, i18n.language, {dateStyle: 'medium'}),
|
|
}) + (row.appid === game.appid ? '' : ` ${t('POINTS.GAME.OTHER_EDITION')}`)),
|
|
...Object.entries(points.missing).filter(([, reason]) => !STATUS_NOT_REACHED.includes(reason)).map(([kind, reason]) => (
|
|
waitingKey(kind, reason) ? t(waitingKey(kind, reason))
|
|
: t('POINTS.GAME.NONE', {kind: kindText(kind), reason: t(`POINTS.REASONS.${reason}`, {date: earlySinceText})}))),
|
|
];
|
|
return lines.map((line, i) => <p key={i} className="text-sm text-muted-foreground">{line}</p>);
|
|
}
|