Public Access
feat(points): show early finish and expected points on questions
Core pays 10 early points for an unproven Steam finish. The rest comes with proof. Show this in the toast and the game line. Questions cards show the full value from GET /api/rewards/expected.
This commit is contained in:
1 parent
814decc781
commit
c126592887
13 files changed
+166
-28
No files matched your search
@@ -18,7 +18,7 @@ import useInView from '../hooks/useInView.js';
|
||||
import {formatDate} from '../utils/format.js';
|
||||
import {buyItem, equipItem, LOOKS, SLOTS} from '../utils/rewards.js';
|
||||
|
||||
const GAME_KINDS = ['STARTED', 'FINISHED', 'FINISHED_MANUAL', 'COMPLETED'];
|
||||
const GAME_KINDS = ['STARTED', 'FINISHED', 'FINISHED_MANUAL', 'FINISHED_EARLY', 'FINISHED_TOPUP', 'COMPLETED'];
|
||||
const HOW_TO = ['STARTED', 'FINISHED', 'PROOF', 'COMPLETED', 'FINISHED_MANUAL', 'ONCE', 'PLAYSTATION', 'HIDDEN', 'CHECKED', 'MONTH',
|
||||
'STREAK', 'STARTER', 'UNLOCK'];
|
||||
|
||||
|
||||
@@ -214,6 +214,17 @@ describe('PointsPage', () => {
|
||||
expect(screen.getByText('+50')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('links early and top-up rows to the game', async () => {
|
||||
await renderPage({'GET /api/rewards/wallet': json(200, wallet({history: [
|
||||
{kind: 'FINISHED_EARLY', ref: '10', gameName: 'Early Game', points: 10, eventAt: '2026-09-30T10:00:00Z'},
|
||||
{kind: 'FINISHED_TOPUP', ref: '10', gameName: 'Early Game', points: 20, eventAt: '2026-09-30T10:00:00Z'},
|
||||
]}))});
|
||||
|
||||
const links = screen.getAllByRole('link', {name: 'Early Game'});
|
||||
expect(links).toHaveLength(2);
|
||||
expect(links[0].getAttribute('href')).toBe('/library/10');
|
||||
});
|
||||
|
||||
it('buys, then loads the wallet again', async () => {
|
||||
let balance = 160;
|
||||
const mock = await renderPageWith(() => json(200, wallet({balance})), {
|
||||
|
||||
@@ -7,7 +7,10 @@ import PageContainer from '../components/PageContainer.jsx';
|
||||
import QuestionCard from '../components/QuestionCard.jsx';
|
||||
import usePointsCheck from '../hooks/usePointsCheck.js';
|
||||
import {answerQuestion, fetchQuestions} from '../utils/questions.js';
|
||||
import {loadExpected} from '../utils/rewards.js';
|
||||
|
||||
// Core answers at most this many appids at once.
|
||||
const MAX_EXPECTED = 200;
|
||||
const STATUS_OF_QUESTION = {START: 'PLAYING', FINISHED: 'FINISHED', DROPPED: 'DROPPED'};
|
||||
|
||||
/**
|
||||
@@ -20,6 +23,7 @@ export default function QuestionsPage() {
|
||||
const [items, setItems] = useState([]);
|
||||
const [status, setStatus] = useState('loading');
|
||||
const [busyAppid, setBusyAppid] = useState(null);
|
||||
const [expected, setExpected] = useState({});
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const {ok, status: code, data} = await fetchQuestions();
|
||||
@@ -30,6 +34,12 @@ export default function QuestionsPage() {
|
||||
}
|
||||
setItems(data.results);
|
||||
setStatus('ready');
|
||||
// Points are a hint: a failed call shows no line and no error.
|
||||
const finished = data.results.filter((item) => item.question === 'FINISHED').map((item) => item.appid);
|
||||
for (let from = 0; from < finished.length; from += MAX_EXPECTED) {
|
||||
const points = await loadExpected(finished.slice(from, from + MAX_EXPECTED));
|
||||
if (points.ok) setExpected((current) => ({...current, ...points.data}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,7 +81,7 @@ export default function QuestionsPage() {
|
||||
<p className="text-sm text-muted-foreground">{t('QUESTIONS.EMPTY')}</p>
|
||||
)}
|
||||
{status === 'ready' && items.map((item) => (
|
||||
<QuestionCard key={item.appid} item={item} busy={busyAppid !== null}
|
||||
<QuestionCard key={item.appid} item={item} busy={busyAppid !== null} expectedPoints={expected[item.appid]}
|
||||
onAnswer={(choice) => answer(item, choice)}/>
|
||||
))}
|
||||
</PageContainer>
|
||||
|
||||
@@ -14,7 +14,11 @@ const card = (appid, question, name = `Game ${appid}`) => ({appid, name, imageUr
|
||||
playtimeMinutes: 1500, normallyMinutes: null, lastActivity: null});
|
||||
|
||||
const booked = (appid) => ({balance: 30, missing: {}, paid: [{kind: 'FINISHED', appid, points: 30, paidAt: '2026-10-09T10:00:00Z', bookedNow: true}]});
|
||||
const WALLET = {'GET /api/rewards/wallet': json(200, {balance: 30, catalog: [], owned: [], equipped: {}, history: []})};
|
||||
const WALLET = {
|
||||
'GET /api/rewards/wallet': json(200, {balance: 30, catalog: [], owned: [], equipped: {}, history: []}),
|
||||
'GET /api/rewards/expected': json(200, {}),
|
||||
};
|
||||
const expectedCalls = () => globalThis.fetch.mock.calls.filter(([url]) => url.includes('/api/rewards/expected'));
|
||||
const pointsCalls = () => globalThis.fetch.mock.calls.filter(([url]) => url.includes('/api/rewards/games/'));
|
||||
|
||||
const renderPage = (routes) => {
|
||||
@@ -161,6 +165,45 @@ describe('QuestionsPage', () => {
|
||||
expect(screen.getByText('Game 1')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('showsTheExpectedPointsOnAFinishedCardOnly', async () => {
|
||||
renderPage({
|
||||
'GET /api/library/questions': json(200, {results: [card(1, 'FINISHED'), card(2, 'FINISHED'), card(3, 'START')]}),
|
||||
'GET /api/rewards/expected': json(200, {1: 50, 2: 0}),
|
||||
});
|
||||
|
||||
expect(await screen.findByText('QUESTIONS.FINISHED.POINTS:50')).toBeTruthy();
|
||||
expect(screen.getAllByText(/^QUESTIONS.FINISHED.POINTS/)).toHaveLength(1);
|
||||
expect(expectedCalls()[0][0]).toContain('appids=1,2');
|
||||
});
|
||||
|
||||
it('asksForTheExpectedPointsOfEveryFinishedCardInChunks', async () => {
|
||||
const cards = Array.from({length: 201}, (_, i) => card(i + 1, 'FINISHED'));
|
||||
renderPage({'GET /api/library/questions': json(200, {results: cards})});
|
||||
|
||||
await waitFor(() => expect(expectedCalls().length).toBeGreaterThan(0));
|
||||
const asked = expectedCalls().flatMap(([url]) => new URL(url, 'http://x').searchParams.get('appids').split(','));
|
||||
expect(asked).toContain('201');
|
||||
});
|
||||
|
||||
it('asksForNoExpectedPointsWithoutAFinishedCard', async () => {
|
||||
renderPage({'GET /api/library/questions': json(200, {results: [card(1, 'START')]})});
|
||||
await screen.findByText('Game 1');
|
||||
|
||||
expect(expectedCalls()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('showsNoPointsLineAndNoErrorWhenTheExpectedCallFails', async () => {
|
||||
renderPage({
|
||||
'GET /api/library/questions': json(200, {results: [card(1, 'FINISHED')]}),
|
||||
'GET /api/rewards/expected': json(500, {}),
|
||||
});
|
||||
|
||||
expect(await screen.findByText('Game 1')).toBeTruthy();
|
||||
await waitFor(() => expect(expectedCalls()).toHaveLength(1));
|
||||
expect(screen.queryByText(/^QUESTIONS.FINISHED.POINTS/)).toBeNull();
|
||||
expect(screen.queryByText('INFO.ERROR.NETWORK_FAILURE')).toBeNull();
|
||||
});
|
||||
|
||||
it('showsTheEmptyState', async () => {
|
||||
renderPage({'GET /api/library/questions': json(200, {results: []})});
|
||||
|
||||
|
||||
Reference in new issue
Block a user