Public Access
feat(library): add ordered backlog page and up-next dashboard tile
This commit is contained in:
1 parent
c158909083
commit
b213b067dc
21 files changed
+677
-24
No files matched your search
@@ -29,8 +29,8 @@ import {expireSession, markSession} from '../utils/auth.js';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{to: '/dashboard', labelKey: 'NAV.DASHBOARD', Icon: LayoutDashboard},
|
||||
// The wishlist is a tab of the library.
|
||||
{to: '/library', labelKey: 'NAV.LIBRARY', Icon: Library, alsoActiveOn: ['/wishlist']},
|
||||
// The wishlist and the backlog order are tabs of the library.
|
||||
{to: '/library', labelKey: 'NAV.LIBRARY', Icon: Library, alsoActiveOn: ['/wishlist', '/backlog']},
|
||||
{to: '/play-next', labelKey: 'NAV.PLAY_NEXT', Icon: Dices},
|
||||
{to: '/stats', labelKey: 'NAV.STATS', Icon: ChartColumn},
|
||||
];
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {NavLink} from 'react-router-dom';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {Gift, Library} from 'lucide-react';
|
||||
import {Gift, Library, ListOrdered} from 'lucide-react';
|
||||
|
||||
import {cn} from '@/lib/utils';
|
||||
|
||||
const TABS = [
|
||||
{to: '/library', labelKey: 'LIBRARY.TITLE', icon: Library},
|
||||
{to: '/backlog', labelKey: 'BACKLOG_ORDER.TITLE', icon: ListOrdered},
|
||||
{to: '/wishlist', labelKey: 'WISHLIST.TITLE', icon: Gift},
|
||||
];
|
||||
|
||||
@@ -18,13 +19,19 @@ export default function LibraryTabs() {
|
||||
key={to}
|
||||
to={to}
|
||||
end
|
||||
title={t(labelKey)}
|
||||
className={({isActive}) => cn(
|
||||
'flex min-w-0 items-center gap-1.5 rounded-md px-3 py-1.5 font-medium transition-colors',
|
||||
isActive ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4 shrink-0"/>
|
||||
<span className="truncate">{t(labelKey)}</span>
|
||||
{({isActive}) => (
|
||||
<>
|
||||
<Icon className="size-4 shrink-0"/>
|
||||
{/* Below sm only the active tab shows its label; the others keep it as their name. */}
|
||||
<span className={cn('truncate', !isActive && 'sr-only sm:not-sr-only')}>{t(labelKey)}</span>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {Link} from 'react-router-dom';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {ArrowRight, ListOrdered} from 'lucide-react';
|
||||
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Card} from '@/components/ui/card';
|
||||
import GameCover from './GameCover.jsx';
|
||||
import FitText from './FitText.jsx';
|
||||
|
||||
/**
|
||||
* The first games of the user's own backlog order.
|
||||
*/
|
||||
export default function UpNextCard({games}) {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Card className="gap-0 py-0">
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-3 px-5 py-4 border-b">
|
||||
<h2 className="flex items-center gap-2 font-semibold">
|
||||
<ListOrdered className="size-4 text-primary"/>
|
||||
{t('DASHBOARD.UP_NEXT')}
|
||||
</h2>
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link to="/backlog">{t('DASHBOARD.SHOW_ALL')}<ArrowRight/></Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{games.length === 0 ? (
|
||||
<p className="px-5 py-10 text-center text-sm text-muted-foreground">{t('DASHBOARD.UP_NEXT_EMPTY')}</p>
|
||||
) : (
|
||||
<ol className="divide-y">
|
||||
{games.map((g, index) => (
|
||||
<li key={g.appid}>
|
||||
<Link to={`/library/${g.appid}`} className="group flex items-center gap-3 px-4 py-2.5 transition-colors hover:bg-muted/50">
|
||||
<span className="w-4 shrink-0 text-right text-sm tabular-nums text-muted-foreground">{index + 1}</span>
|
||||
<GameCover appid={g.appid} imageUrl={g.imageUrl} className="w-16 shrink-0"/>
|
||||
<div className="@container min-w-0 flex-1 text-sm font-medium group-hover:text-primary transition-colors">
|
||||
<FitText text={g.name ?? `App ${g.appid}`}/>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// @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 UpNextCard from './UpNextCard.jsx';
|
||||
|
||||
vi.mock('react-i18next', () => import('../test/i18n-mock.js'));
|
||||
|
||||
const renderCard = (games) => render(<MemoryRouter><UpNextCard games={games}/></MemoryRouter>);
|
||||
|
||||
describe('UpNextCard', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
it('shows the games with their rank and links to the backlog page', () => {
|
||||
renderCard([{appid: 7, name: 'Hades'}, {appid: 9, name: 'Celeste'}]);
|
||||
|
||||
const rows = screen.getAllByRole('listitem');
|
||||
expect(rows.map((row) => row.textContent)).toEqual(['1Hades', '2Celeste']);
|
||||
expect(screen.getByRole('link', {name: /DASHBOARD.SHOW_ALL/}).getAttribute('href')).toBe('/backlog');
|
||||
});
|
||||
|
||||
it('says so when the backlog is empty', () => {
|
||||
renderCard([]);
|
||||
|
||||
expect(screen.getByText('DASHBOARD.UP_NEXT_EMPTY')).toBeTruthy();
|
||||
expect(screen.queryAllByRole('listitem')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user