diff --git a/public/i18n/de.json b/public/i18n/de.json index 8880dee..899be1b 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -575,7 +575,6 @@ "TREND_ADDED": "Diesen Monat zum Stapel hinzugekommen", "TREND_DONE": "Diesen Monat vom Stapel gegangen", "NEW_IN_LIBRARY": "Neu", - "OPEN": "Im Steam-Shop öffnen", "GAMES_one": "{{count}} Spiel", "GAMES_other": "{{count}} Spiele" } diff --git a/public/i18n/en.json b/public/i18n/en.json index d9c0d99..634686a 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -575,7 +575,6 @@ "TREND_ADDED": "Added to the pile this month", "TREND_DONE": "Off the pile this month", "NEW_IN_LIBRARY": "New", - "OPEN": "Open in the Steam store", "GAMES_one": "{{count}} game", "GAMES_other": "{{count}} games" } diff --git a/src/components/AchievementsCard.jsx b/src/components/AchievementsCard.jsx index b7a66c2..f7d6014 100644 --- a/src/components/AchievementsCard.jsx +++ b/src/components/AchievementsCard.jsx @@ -5,6 +5,7 @@ import {Trophy} from 'lucide-react'; import {Card} from '@/components/ui/card'; import GameCover from './GameCover.jsx'; import FitText from './FitText.jsx'; +import {StatCount} from './stats/StatDrill.jsx'; /** * @param summary the answer of GET /api/library/achievements/summary @@ -15,12 +16,14 @@ export default function AchievementsCard({summary}) { const started = summary.startedGames ?? 0; const pending = summary.pendingGames ?? 0; const hidden = summary.hiddenGames ?? 0; + const perfect = summary.perfectGames ?? 0; // A private profile keeps games pending; the privacy hint explains that better than the pending count. const steamHidesAll = hidden > 0 && started === 0; const stats = [ [(summary.unlocked ?? 0).toLocaleString(i18n.language), t('DASHBOARD.ACHIEVEMENTS.UNLOCKED')], - [(summary.perfectGames ?? 0).toLocaleString(i18n.language), t('DASHBOARD.ACHIEVEMENTS.PERFECT')], + [perfect.toLocaleString(i18n.language), t('DASHBOARD.ACHIEVEMENTS.PERFECT'), + {params: {key: 'PERFECT'}, count: perfect}], [summary.averageCompletion != null ? `${summary.averageCompletion.toLocaleString(i18n.language)} %` : '–', t('DASHBOARD.ACHIEVEMENTS.AVERAGE')], ]; @@ -33,9 +36,11 @@ export default function AchievementsCard({summary}) {
- {stats.map(([value, label]) => ( + {stats.map(([value, label, drill]) => (
-
{value}
+
+ {value} +
{label}
))} diff --git a/src/components/AchievementsCard.test.jsx b/src/components/AchievementsCard.test.jsx index 419e18d..d8cfac7 100644 --- a/src/components/AchievementsCard.test.jsx +++ b/src/components/AchievementsCard.test.jsx @@ -1,8 +1,10 @@ // @vitest-environment jsdom import {afterEach, describe, expect, it, vi} from 'vitest'; -import {cleanup, render, screen} from '@testing-library/react'; +import {cleanup, fireEvent, render, screen} from '@testing-library/react'; import {MemoryRouter} from 'react-router-dom'; import AchievementsCard from './AchievementsCard.jsx'; +import {StatDrillProvider} from './stats/StatDrill.jsx'; +import {fetchRoutes, json} from '../test/fetchRoutes.js'; vi.mock('react-i18next', () => import('../test/i18n-mock.js')); @@ -49,4 +51,22 @@ describe('AchievementsCard', () => { expect(screen.getByText('DASHBOARD.ACHIEVEMENTS.NONE')).toBeTruthy(); }); + + it('opensThePerfectGamesInTheOwnLibrary', async () => { + fetchRoutes({'GET /api/library/stats/games': json(200, { + content: [{appid: -7, name: 'Manual Game'}], page: 0, size: 40, totalElements: 1, totalPages: 1})}); + render(); + + fireEvent.click(screen.getByRole('button', {name: 'DASHBOARD.ACHIEVEMENTS.PERFECT: 2'})); + + const link = await screen.findByRole('link', {name: /Manual Game/}); + expect(fetch.mock.calls[0][0]).toContain('key=PERFECT&page=0'); + expect(link.getAttribute('href')).toBe('/library/-7'); + }); + + it('perfectGamesAreNotClickableAtZero', () => { + render(); + + expect(screen.queryByRole('button')).toBeNull(); + }); }); diff --git a/src/components/PileOfShameCard.jsx b/src/components/PileOfShameCard.jsx index 3856fa1..b7bebe6 100644 --- a/src/components/PileOfShameCard.jsx +++ b/src/components/PileOfShameCard.jsx @@ -36,7 +36,7 @@ export default function PileOfShameCard({summary, trend, syncing, onShuffle, shu
- {backlog.toLocaleString(i18n.language)} @@ -77,7 +77,8 @@ export default function PileOfShameCard({summary, trend, syncing, onShuffle, shu
  • {t(`BACKLOG.STATUS.${s}`)} - + {summary.byStatus[s]}
  • diff --git a/src/components/PileOfShameCard.test.jsx b/src/components/PileOfShameCard.test.jsx index 0491577..ffb5850 100644 --- a/src/components/PileOfShameCard.test.jsx +++ b/src/components/PileOfShameCard.test.jsx @@ -65,6 +65,16 @@ describe('PileOfShameCard', () => { expect(screen.getByText('DASHBOARD.BACKLOG_EMPTY')).toBeTruthy(); }); + it('zeroCountsAreNotClickable', () => { + render( + {}} summary={summary(0)}/> + ); + + expect(screen.queryByRole('link', {name: 'BACKLOG.STATUS.BACKLOG: 0'})).toBeNull(); + expect(screen.queryByRole('link', {name: 'BACKLOG.STATUS.FINISHED: 0'})).toBeNull(); + expect(screen.getByRole('link', {name: 'BACKLOG.STATUS.PLAYING: 3'})).toBeTruthy(); + }); + it('shows the backlog hours when IGDB knows some', () => { renderCard({summary: summary(2, {backlogMinutes: 7440})}); diff --git a/src/components/StatCard.jsx b/src/components/StatCard.jsx index 937025d..85bef4f 100644 --- a/src/components/StatCard.jsx +++ b/src/components/StatCard.jsx @@ -32,7 +32,7 @@ function useCountUp(target, durationMs = 1600) { export default function StatCard({label, value, icon: Icon, suffix, drill}) { const {i18n} = useTranslation(); const animated = useCountUp(value); - const target = useDrillTarget({title: label, ...drill}); + const target = useDrillTarget({title: label, count: value, ...drill}); const card = ( {Icon && ( diff --git a/src/components/SteamGameCount.jsx b/src/components/SteamGameCount.jsx index 6229c3b..5e1f7bb 100644 --- a/src/components/SteamGameCount.jsx +++ b/src/components/SteamGameCount.jsx @@ -6,6 +6,7 @@ import {ArrowRight, Gamepad2} from 'lucide-react'; import {Card} from '@/components/ui/card'; const DURATION_MS = 1000; +const COUNT_CLASS = 'block text-5xl font-bold tracking-tight tabular-nums'; const SteamGameCount = ({targetCount}) => { const {t} = useTranslation(); @@ -33,7 +34,9 @@ const SteamGameCount = ({targetCount}) => {
    - {count} + {targetCount > 0 + ? {count} + : {count}}
    {t('DASHBOARD.STEAM_GAMES')}
    diff --git a/src/components/SteamGameCount.test.jsx b/src/components/SteamGameCount.test.jsx new file mode 100644 index 0000000..d082c2a --- /dev/null +++ b/src/components/SteamGameCount.test.jsx @@ -0,0 +1,20 @@ +// @vitest-environment jsdom +import {afterEach, describe, expect, it, vi} from 'vitest'; +import {cleanup, render, screen} from '@testing-library/react'; +import {MemoryRouter} from 'react-router-dom'; +import SteamGameCount from './SteamGameCount.jsx'; + +vi.mock('react-i18next', () => import('../test/i18n-mock.js')); + +describe('SteamGameCount', () => { + afterEach(cleanup); + + it('linksTheCountToTheLibraryOnlyWhenThereAreGames', () => { + const {container, rerender} = render(); + expect(container.querySelectorAll('a[href="/library"]')).toHaveLength(1); + + rerender(); + expect(container.querySelectorAll('a[href="/library"]')).toHaveLength(2); + expect(screen.getByText('DASHBOARD.STEAM_GAMES')).toBeTruthy(); + }); +}); diff --git a/src/components/stats/GameRow.jsx b/src/components/stats/GameRow.jsx index a6b0246..5fc8d31 100644 --- a/src/components/stats/GameRow.jsx +++ b/src/components/stats/GameRow.jsx @@ -1,15 +1,13 @@ import {Link} from 'react-router-dom'; -import {useTranslation} from 'react-i18next'; import GameCover from '../GameCover.jsx'; import FitText from '../FitText.jsx'; import {gameHref} from '../../utils/statGames.js'; /** - * A game with its cover and a value on the right; links to the game page in the user's own library, to the Steam - * store for a visitor. + * A game with its cover and a value on the right; links to the game page when the games are the viewer's own, plain + * otherwise. */ export default function GameRow({game, value, linked = true}) { - const {t} = useTranslation(); const content = ( <> @@ -17,14 +15,11 @@ export default function GameRow({game, value, linked = true}) { {value} ); - const className = 'flex items-center gap-3 hover:text-primary transition-colors'; - if (linked) return {content}; - const href = gameHref(game.appid, false); - if (href) { + if (linked) { return ( - + {content} - + ); } return
    {content}
    ; diff --git a/src/components/stats/HabitsStats.jsx b/src/components/stats/HabitsStats.jsx index 2ab4d74..527be0d 100644 --- a/src/components/stats/HabitsStats.jsx +++ b/src/components/stats/HabitsStats.jsx @@ -25,7 +25,7 @@ export default function HabitsStats({habits}) { + drill={{params: {key: 'UNDER_ONE_HOUR'}, count: habits.underOneHourGames}}/> )}
    diff --git a/src/components/stats/OverviewStats.jsx b/src/components/stats/OverviewStats.jsx index cebf363..a9b144f 100644 --- a/src/components/stats/OverviewStats.jsx +++ b/src/components/stats/OverviewStats.jsx @@ -15,18 +15,19 @@ export default function OverviewStats({overview}) {
    + drill={{to: libraryLink(), params: {key: 'TOTAL'}, count: overview.totalGames}}/> + drill={{params: {key: 'PLAYED'}, count: overview.playedGames}}/> + drill={{params: {key: 'NEVER_STARTED'}, count: overview.neverStarted}}/>