Public Access
feat(library): time to beat, short-games filter and backlog hours
This commit is contained in:
1 parent
fd343da4be
commit
91d5672421
11 files changed
+131
-11
No files matched your search
@@ -7,6 +7,7 @@ import {Card} from '@/components/ui/card';
|
||||
import GameCover from './GameCover.jsx';
|
||||
import {STATUS_META} from './GameStatusBadge.jsx';
|
||||
import {backlogShare, GAME_STATUSES} from '../utils/backlog.js';
|
||||
import {formatPlaytime} from '../utils/games.js';
|
||||
|
||||
export default function PileOfShameCard({summary, syncing, onShuffle, shuffling}) {
|
||||
const {t, i18n} = useTranslation();
|
||||
@@ -36,6 +37,11 @@ export default function PileOfShameCard({summary, syncing, onShuffle, shuffling}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t('DASHBOARD.NEVER_PLAYED', {count: summary.neverPlayed})}
|
||||
</p>
|
||||
{summary.backlogMinutes > 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('DASHBOARD.BACKLOG_HOURS', {time: formatPlaytime(summary.backlogMinutes)})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{summary.total > 0 && (
|
||||
|
||||
@@ -47,4 +47,16 @@ describe('PileOfShameCard', () => {
|
||||
|
||||
expect(screen.getByText('DASHBOARD.BACKLOG_EMPTY')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the backlog hours when IGDB knows some', () => {
|
||||
renderCard({summary: summary(2, {backlogMinutes: 7440})});
|
||||
|
||||
expect(screen.getByText('DASHBOARD.BACKLOG_HOURS')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides the backlog hours without data', () => {
|
||||
renderCard({summary: summary(2, {backlogMinutes: 0})});
|
||||
|
||||
expect(screen.queryByText('DASHBOARD.BACKLOG_HOURS')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {Timer} from 'lucide-react';
|
||||
|
||||
import {formatPlaytime} from '../utils/games.js';
|
||||
|
||||
const ROWS = [
|
||||
['hastilyMinutes', 'GAME_DETAIL.TTB_HASTILY'],
|
||||
['normallyMinutes', 'GAME_DETAIL.TTB_NORMALLY'],
|
||||
['completelyMinutes', 'GAME_DETAIL.TTB_COMPLETELY'],
|
||||
];
|
||||
|
||||
/**
|
||||
* IGDB's average time to beat; renders nothing without data.
|
||||
*/
|
||||
export default function TimeToBeat({time}) {
|
||||
const {t} = useTranslation();
|
||||
const known = ROWS.filter(([field]) => time?.[field]);
|
||||
if (known.length === 0) return null;
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
||||
<Timer className="size-4 text-primary" aria-label={t('GAME_DETAIL.TTB')}/>
|
||||
{known.map(([field, labelKey]) => (
|
||||
<span key={field}>
|
||||
{t(labelKey)} <span className="font-medium text-foreground tabular-nums">{formatPlaytime(time[field])}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// @vitest-environment jsdom
|
||||
import {afterEach, describe, expect, it, vi} from 'vitest';
|
||||
import {cleanup, render, screen} from '@testing-library/react';
|
||||
import TimeToBeat from './TimeToBeat.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
|
||||
describe('TimeToBeat', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
it('shows the known times', () => {
|
||||
render(<TimeToBeat time={{hastilyMinutes: 510, normallyMinutes: 750, completelyMinutes: null}}/>);
|
||||
|
||||
expect(screen.getByText('8.5h')).toBeTruthy();
|
||||
expect(screen.getByText('13h')).toBeTruthy();
|
||||
expect(screen.queryByText('GAME_DETAIL.TTB_COMPLETELY')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders nothing without data', () => {
|
||||
const {container} = render(<TimeToBeat time={{hastilyMinutes: null, normallyMinutes: null, completelyMinutes: null}}/>);
|
||||
|
||||
expect(container.textContent).toBe('');
|
||||
cleanup();
|
||||
expect(render(<TimeToBeat time={null}/>).container.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user