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
+2
-1
@@ -1182,7 +1182,7 @@
|
|||||||
"RULES": {
|
"RULES": {
|
||||||
"STARTED": "5 Punkte, wenn du ein Spiel von deinem Stapel startest und spielst (Steam-Spiele, einmal pro Spiel).",
|
"STARTED": "5 Punkte, wenn du ein Spiel von deinem Stapel startest und spielst (Steam-Spiele, einmal pro Spiel).",
|
||||||
"FINISHED": "30, 50 oder 80 Punkte (je nach Spieldauer), wenn du ein Steam-Spiel durchspielst und es einen Nachweis gibt, dass du es gespielt hast. Ohne Nachweis bekommst du sofort 10 Punkte und den Rest, sobald du das Spiel nach dem Durchspielen auf Steam spielst. Ist keine Spielzeit erfasst, gibt es auch erst 10 Punkte, den Rest nur mit Steam-Spielzeit. Einmal pro Spiel.",
|
"FINISHED": "30, 50 oder 80 Punkte (je nach Spieldauer), wenn du ein Steam-Spiel durchspielst und es einen Nachweis gibt, dass du es gespielt hast. Ohne Nachweis bekommst du sofort 10 Punkte und den Rest, sobald du das Spiel nach dem Durchspielen auf Steam spielst. Ist keine Spielzeit erfasst, gibt es auch erst 10 Punkte, den Rest nur mit Steam-Spielzeit. Einmal pro Spiel.",
|
||||||
"COMPLETED": "+40 Punkte, wenn ein Spiel auf 100 % steht und du alle Erfolge freigeschaltet hast. Dafür braucht es ein nachgewiesenes Durchspielen auf Steam (die frühen 10 Punkte reichen erst, wenn der Nachweis da ist) oder Punkte fürs Durchspielen einer manuellen Edition des Spiels; ein manuelles Spiel selbst bekommt diesen Bonus nie.",
|
"COMPLETED": "+40 Punkte, wenn ein Spiel auf 100 % steht und du alle Erfolge freigeschaltet hast. Dafür braucht es ein nachgewiesenes Durchspielen auf Steam (die frühen 10 Punkte reichen erst, wenn der Nachweis da ist) oder Punkte fürs Durchspielen einer manuellen Edition des Spiels, wenn du die 100 % am {{date}} oder später erreicht hast; ein manuelles Spiel selbst bekommt diesen Bonus nie.",
|
||||||
"FINISHED_MANUAL": "15 Punkte, wenn du ein manuelles Spiel durchspielst. Höchstens 3 durchgespielte Spiele pro Woche (Montag bis Sonntag) bringen Punkte; manuelle Spiele und Steam-Spiele ohne Nachweis teilen sich dieses Limit. Ein 4. manuelles Spiel in dieser Woche bringt keine Punkte; ein 4. Steam-Spiel ohne Nachweis bekommt die vollen Punkte, sobald der Nachweis da ist.",
|
"FINISHED_MANUAL": "15 Punkte, wenn du ein manuelles Spiel durchspielst. Höchstens 3 durchgespielte Spiele pro Woche (Montag bis Sonntag) bringen Punkte; manuelle Spiele und Steam-Spiele ohne Nachweis teilen sich dieses Limit. Ein 4. manuelles Spiel in dieser Woche bringt keine Punkte; ein 4. Steam-Spiel ohne Nachweis bekommt die vollen Punkte, sobald der Nachweis da ist.",
|
||||||
"MONTH": "40 Punkte, wenn dein Stapel am Monatsende kleiner ist als am Monatsanfang und du in dem Monat ein Spiel durchgespielt hast.",
|
"MONTH": "40 Punkte, wenn dein Stapel am Monatsende kleiner ist als am Monatsanfang und du in dem Monat ein Spiel durchgespielt hast.",
|
||||||
"STREAK": "20 Punkte für jede Woche, in der du ein Spiel durchspielst, wenn du auch in der Woche davor eins durchgespielt hast.",
|
"STREAK": "20 Punkte für jede Woche, in der du ein Spiel durchspielst, wenn du auch in der Woche davor eins durchgespielt hast.",
|
||||||
@@ -1220,6 +1220,7 @@
|
|||||||
"WEEKLY_LIMIT": "Wochenlimit erreicht",
|
"WEEKLY_LIMIT": "Wochenlimit erreicht",
|
||||||
"WEEKLY_LIMIT_NO_PROOF": "Wochenlimit erreicht, darum keine frühen Punkte; mit Nachweis gibt es weiter die vollen Punkte fürs Durchspielen",
|
"WEEKLY_LIMIT_NO_PROOF": "Wochenlimit erreicht, darum keine frühen Punkte; mit Nachweis gibt es weiter die vollen Punkte fürs Durchspielen",
|
||||||
"NEEDS_PROVEN_FINISH": "braucht zuerst ein nachgewiesenes Durchspielen auf Steam oder Punkte fürs Durchspielen einer manuellen Edition",
|
"NEEDS_PROVEN_FINISH": "braucht zuerst ein nachgewiesenes Durchspielen auf Steam oder Punkte fürs Durchspielen einer manuellen Edition",
|
||||||
|
"COMPLETION_TOO_OLD": "100 % vor dem {{date}} erreicht – zu früh für diese Punkte",
|
||||||
"NOT_ALL_ACHIEVEMENTS": "nicht alle Erfolge freigeschaltet"
|
"NOT_ALL_ACHIEVEMENTS": "nicht alle Erfolge freigeschaltet"
|
||||||
},
|
},
|
||||||
"SLOTS": {
|
"SLOTS": {
|
||||||
|
|||||||
+2
-1
@@ -1182,7 +1182,7 @@
|
|||||||
"RULES": {
|
"RULES": {
|
||||||
"STARTED": "5 points when you start a game from your pile and play it (Steam games, once per game).",
|
"STARTED": "5 points when you start a game from your pile and play it (Steam games, once per game).",
|
||||||
"FINISHED": "30, 50 or 80 points (by time to beat) when you finish a Steam game and there is proof that you played it. Without proof you get 10 points now and the rest once you play the game on Steam after finishing it. With no playtime on record you also get 10 points first, and the rest only with Steam playtime. Once per game.",
|
"FINISHED": "30, 50 or 80 points (by time to beat) when you finish a Steam game and there is proof that you played it. Without proof you get 10 points now and the rest once you play the game on Steam after finishing it. With no playtime on record you also get 10 points first, and the rest only with Steam playtime. Once per game.",
|
||||||
"COMPLETED": "+40 points when a game is at 100% and you unlocked all achievements. It needs a proven Steam finish (the early 10 points count only once proof arrives) or finish points for a manual edition of the game; a manual game itself never gets this bonus.",
|
"COMPLETED": "+40 points when a game is at 100% and you unlocked all achievements. It needs a proven Steam finish (the early 10 points count only once proof arrives) or finish points for a manual edition of the game, when you reached 100% on or after {{date}}; a manual game itself never gets this bonus.",
|
||||||
"FINISHED_MANUAL": "15 points when you finish a manual game. At most 3 finishes per week (Monday to Sunday) earn points; manual games and Steam games without proof share this limit. A 4th manual finish in that week earns nothing; a 4th Steam finish without proof still earns the full points once proven.",
|
"FINISHED_MANUAL": "15 points when you finish a manual game. At most 3 finishes per week (Monday to Sunday) earn points; manual games and Steam games without proof share this limit. A 4th manual finish in that week earns nothing; a 4th Steam finish without proof still earns the full points once proven.",
|
||||||
"MONTH": "40 points when your pile is smaller at the end of a month than at its start and you finished a game that month.",
|
"MONTH": "40 points when your pile is smaller at the end of a month than at its start and you finished a game that month.",
|
||||||
"STREAK": "20 points for every week in which you finish a game, if you also finished one the week before.",
|
"STREAK": "20 points for every week in which you finish a game, if you also finished one the week before.",
|
||||||
@@ -1220,6 +1220,7 @@
|
|||||||
"WEEKLY_LIMIT": "weekly limit reached",
|
"WEEKLY_LIMIT": "weekly limit reached",
|
||||||
"WEEKLY_LIMIT_NO_PROOF": "weekly limit reached, so no early points; with proof you still get the full finish points",
|
"WEEKLY_LIMIT_NO_PROOF": "weekly limit reached, so no early points; with proof you still get the full finish points",
|
||||||
"NEEDS_PROVEN_FINISH": "needs a proven Steam finish or finish points for a manual edition first",
|
"NEEDS_PROVEN_FINISH": "needs a proven Steam finish or finish points for a manual edition first",
|
||||||
|
"COMPLETION_TOO_OLD": "100% reached before {{date}}, too early for these points",
|
||||||
"NOT_ALL_ACHIEVEMENTS": "not all achievements unlocked"
|
"NOT_ALL_ACHIEVEMENTS": "not all achievements unlocked"
|
||||||
},
|
},
|
||||||
"SLOTS": {
|
"SLOTS": {
|
||||||
|
|||||||
@@ -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 {useEffect, useRef, useState} from 'react';
|
||||||
import {useTranslation} from 'react-i18next';
|
import {useTranslation} from 'react-i18next';
|
||||||
import usePointsCheck from '../hooks/usePointsCheck.js';
|
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';
|
import {STATUS_NOT_REACHED} from '../utils/rewards.js';
|
||||||
|
|
||||||
const WAITING = {NO_PROOF: 'POINTS.GAME.WAITING', NO_PLAY_AFTER_START: 'POINTS.GAME.WAITING_START'};
|
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]);
|
}, [check, game.appid, game.status]);
|
||||||
|
|
||||||
if (!points) return null;
|
if (!points) return null;
|
||||||
|
const earlySinceText = points.earlySince && formatDay(points.earlySince, i18n.language, {dateStyle: 'medium'});
|
||||||
const kindText = (kind) => t(`POINTS.KINDS.${kind}`);
|
const kindText = (kind) => t(`POINTS.KINDS.${kind}`);
|
||||||
const lines = [
|
const lines = [
|
||||||
...points.paid.map((row) => t('POINTS.GAME.PAID', {
|
...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')}`)),
|
}) + (row.appid === game.appid ? '' : ` ${t('POINTS.GAME.OTHER_EDITION')}`)),
|
||||||
...Object.entries(points.missing).filter(([, reason]) => !STATUS_NOT_REACHED.includes(reason)).map(([kind, reason]) => (
|
...Object.entries(points.missing).filter(([, reason]) => !STATUS_NOT_REACHED.includes(reason)).map(([kind, reason]) => (
|
||||||
waitingKey(kind, reason) ? t(waitingKey(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>);
|
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 {useTranslation} from 'react-i18next';
|
||||||
import {toast} from 'sonner';
|
import {toast} from 'sonner';
|
||||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
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';
|
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',
|
toast(t(notice.rest ? 'POINTS.GAME.TOAST_PAID_REST' : 'POINTS.GAME.TOAST_PAID',
|
||||||
{date: formatDate(notice.date, i18n.language, {dateStyle: 'medium'})}));
|
{date: formatDate(notice.date, i18n.language, {dateStyle: 'medium'})}));
|
||||||
} else if (notice.type === 'none') {
|
} 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 {
|
} else {
|
||||||
toast(t('POINTS.GAME.TOAST_STATUS'));
|
toast(t('POINTS.GAME.TOAST_STATUS'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ describe('translation keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('points texts', () => {
|
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', () => {
|
it('pointsTexts_SayEarnNotPay', () => {
|
||||||
const texts = (file) => flatTexts(readTexts(file).POINTS).map(([, text]) => text);
|
const texts = (file) => flatTexts(readTexts(file).POINTS).map(([, text]) => text);
|
||||||
for (const text of texts('de.json')) expect(text).not.toMatch(/\bzahl|Beweis|bezahlt/);
|
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 {StatCount, StatDrillProvider, useDrillTarget} from '../components/stats/StatDrill.jsx';
|
||||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||||
import useInView from '../hooks/useInView.js';
|
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';
|
import {buyItem, equipItem, LOOKS, SLOTS} from '../utils/rewards.js';
|
||||||
|
|
||||||
const GAME_KINDS = ['STARTED', 'FINISHED', 'FINISHED_MANUAL', 'FINISHED_EARLY', 'FINISHED_TOPUP', 'COMPLETED'];
|
const GAME_KINDS = ['STARTED', 'FINISHED', 'FINISHED_MANUAL', 'FINISHED_EARLY', 'FINISHED_TOPUP', 'COMPLETED'];
|
||||||
@@ -72,6 +72,8 @@ export default function PointsPage() {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const earlySince = wallet.earlySince && formatDay(wallet.earlySince, i18n.language, {dateStyle: 'medium'});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatDrillProvider source="points">
|
<StatDrillProvider source="points">
|
||||||
<PageContainer className="p-4 md:p-8 space-y-6">
|
<PageContainer className="p-4 md:p-8 space-y-6">
|
||||||
@@ -86,7 +88,7 @@ export default function PointsPage() {
|
|||||||
<Card className="gap-2 p-5">
|
<Card className="gap-2 p-5">
|
||||||
<h2 className="font-semibold">{t('POINTS.HOW_TO')}</h2>
|
<h2 className="font-semibold">{t('POINTS.HOW_TO')}</h2>
|
||||||
<ul className="list-disc space-y-1 pl-5 text-sm text-muted-foreground">
|
<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>
|
</ul>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user