Public Access
feat(points): show early-since date in point texts
Refs #44. Core sends earlySince in the games and wallet responses. COMPLETION_TOO_OLD and the COMPLETED rule show it as {{date}}.
This commit is contained in:
1 parent
e044959629
commit
c966877b23
7 files changed
+62
-8
No files matched your search
@@ -0,0 +1,40 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, render, screen} from '@testing-library/react';
|
||||
import GamePointsLine from './GamePointsLine.jsx';
|
||||
import {PreferencesProvider} from '../context/PreferencesContext.jsx';
|
||||
import {fetchRoutes, json} from '../test/fetchRoutes.js';
|
||||
|
||||
// t() shows its options, so the test sees what Core's earlySince becomes.
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key, options) => (options ? `${key}|${Object.values(options).join('|')}` : key),
|
||||
i18n: {language: 'en-GB'},
|
||||
}),
|
||||
}));
|
||||
vi.mock('sonner', () => ({toast: Object.assign(vi.fn(), {success: vi.fn(), error: vi.fn()})}));
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('tbp_session', '1');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GamePointsLine date', () => {
|
||||
it('puts the early-since day of Core into the reason text', async () => {
|
||||
fetchRoutes({
|
||||
'GET /api/rewards/games/220': json(200, {
|
||||
balance: 15, paid: [], earlySince: '2026-09-10', missing: {COMPLETED: 'COMPLETION_TOO_OLD'},
|
||||
}),
|
||||
'GET /api/rewards/wallet': json(200, {balance: 15, catalog: [], owned: [], equipped: {}, history: []}),
|
||||
});
|
||||
|
||||
render(<PreferencesProvider><GamePointsLine game={{appid: 220, status: 'COMPLETED'}}/></PreferencesProvider>);
|
||||
|
||||
expect(await screen.findByText(/POINTS\.REASONS\.COMPLETION_TOO_OLD\|10 Sept(ember)? 2026/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import usePointsCheck from '../hooks/usePointsCheck.js';
|
||||
import {formatDate} from '../utils/format.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'};
|
||||
@@ -33,6 +33,7 @@ export default function GamePointsLine({game}) {
|
||||
}, [check, game.appid, game.status]);
|
||||
|
||||
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', {
|
||||
@@ -41,7 +42,7 @@ export default function GamePointsLine({game}) {
|
||||
}) + (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}`)}))),
|
||||
: 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>);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {toast} from 'sonner';
|
||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||
import {formatDate} from '../utils/format.js';
|
||||
import {formatDate, formatDay} from '../utils/format.js';
|
||||
import {loadGamePoints, pointsNotice} from '../utils/rewards.js';
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,8 @@ export default function usePointsCheck() {
|
||||
toast(t(notice.rest ? 'POINTS.GAME.TOAST_PAID_REST' : 'POINTS.GAME.TOAST_PAID',
|
||||
{date: formatDate(notice.date, i18n.language, {dateStyle: 'medium'})}));
|
||||
} else if (notice.type === 'none') {
|
||||
toast(t('POINTS.GAME.TOAST_NONE', {reason: t(`POINTS.REASONS.${notice.reason}`)}));
|
||||
toast(t('POINTS.GAME.TOAST_NONE', {reason: t(`POINTS.REASONS.${notice.reason}`,
|
||||
{date: data.earlySince && formatDay(data.earlySince, i18n.language, {dateStyle: 'medium'})})}));
|
||||
} else {
|
||||
toast(t('POINTS.GAME.TOAST_STATUS'));
|
||||
}
|
||||
|
||||
@@ -72,6 +72,14 @@ describe('translation keys', () => {
|
||||
});
|
||||
|
||||
describe('points texts', () => {
|
||||
it('pointsTexts_NameTheEarlySinceDateInBothLanguages', () => {
|
||||
for (const file of ['de.json', 'en.json']) {
|
||||
const points = readTexts(file).POINTS;
|
||||
expect(points.REASONS.COMPLETION_TOO_OLD).toContain('{{date}}');
|
||||
expect(points.RULES.COMPLETED).toContain('{{date}}');
|
||||
}
|
||||
});
|
||||
|
||||
it('pointsTexts_SayEarnNotPay', () => {
|
||||
const texts = (file) => flatTexts(readTexts(file).POINTS).map(([, text]) => text);
|
||||
for (const text of texts('de.json')) expect(text).not.toMatch(/\bzahl|Beweis|bezahlt/);
|
||||
|
||||
@@ -15,7 +15,7 @@ import PageContainer from '../components/PageContainer.jsx';
|
||||
import {StatCount, StatDrillProvider, useDrillTarget} from '../components/stats/StatDrill.jsx';
|
||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||
import useInView from '../hooks/useInView.js';
|
||||
import {formatDate} from '../utils/format.js';
|
||||
import {formatDate, formatDay} from '../utils/format.js';
|
||||
import {buyItem, equipItem, LOOKS, SLOTS} from '../utils/rewards.js';
|
||||
|
||||
const GAME_KINDS = ['STARTED', 'FINISHED', 'FINISHED_MANUAL', 'FINISHED_EARLY', 'FINISHED_TOPUP', 'COMPLETED'];
|
||||
@@ -72,6 +72,8 @@ export default function PointsPage() {
|
||||
return null;
|
||||
};
|
||||
|
||||
const earlySince = wallet.earlySince && formatDay(wallet.earlySince, i18n.language, {dateStyle: 'medium'});
|
||||
|
||||
return (
|
||||
<StatDrillProvider source="points">
|
||||
<PageContainer className="p-4 md:p-8 space-y-6">
|
||||
@@ -86,7 +88,7 @@ export default function PointsPage() {
|
||||
<Card className="gap-2 p-5">
|
||||
<h2 className="font-semibold">{t('POINTS.HOW_TO')}</h2>
|
||||
<ul className="list-disc space-y-1 pl-5 text-sm text-muted-foreground">
|
||||
{HOW_TO.map((kind) => <li key={kind}>{t(`POINTS.RULES.${kind}`)}</li>)}
|
||||
{HOW_TO.map((kind) => <li key={kind}>{t(`POINTS.RULES.${kind}`, {date: earlySince})}</li>)}
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in new issue
Block a user