feat(header): add community link to nav
Build, Test & Push Frontend / quality-check (push) Successful in 39s
Build, Test & Push Frontend / docker (push) Successful in 23s

Add an icon-only nav link to "/" so logged-in users can get back
to the community stats. Make room for it: less nav link padding
below sm, points chip no longer shrinks from lg, and the view-as
badge hangs below the header at every width.
This commit is contained in:
lobo committed 2026-10-08 13:17:29 +02:00
1 parent e7b08cfb47
commit 1dab069a4d
4 files changed
+35 -9

No files matched your search

+1
View File
@@ -380,6 +380,7 @@
"LIGHT_MODE": "Helles Design",
"PLAY_NEXT": "Nächstes Spiel",
"STATS": "Statistik",
"COMMUNITY": "Community",
"POINTS": "Pile Points",
"ADMIN": "Admin",
"VIEW_AS": {
+1
View File
@@ -380,6 +380,7 @@
"LIGHT_MODE": "Light mode",
"PLAY_NEXT": "Play next",
"STATS": "Stats",
"COMMUNITY": "Community",
"POINTS": "Pile Points",
"ADMIN": "Admin",
"VIEW_AS": {
+8 -7
View File
@@ -1,7 +1,7 @@
import {useEffect, useState} from 'react';
import {Link, NavLink, useLocation, useNavigate} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {Bell, ChartColumn, Coins, Dices, Eye, Languages, LayoutDashboard, Library, Link2, LogOut, Mail, Megaphone, Moon, Newspaper, ScrollText, Search, Settings, Shield, Sun} from 'lucide-react';
import {Bell, ChartColumn, Coins, Dices, Eye, Languages, LayoutDashboard, Library, Link2, LogOut, Mail, Megaphone, Moon, Newspaper, ScrollText, Search, Settings, Shield, Sun, Users} from 'lucide-react';
import {toast} from 'sonner';
import {Badge} from '@/components/ui/badge';
@@ -34,10 +34,11 @@ const NAV_ITEMS = [
{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},
{to: '/', labelKey: 'NAV.COMMUNITY', Icon: Users, iconOnly: true},
];
const navLinkClass = (isActive) => cn(
'flex items-center gap-2 rounded-lg px-2 py-2 text-sm font-medium transition-colors sm:px-3',
'flex items-center gap-2 rounded-lg px-1 py-2 text-sm font-medium transition-colors sm:px-3',
isActive
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
@@ -128,23 +129,23 @@ export default function AppHeader({isAuthenticated, onLogout}) {
{isAuthenticated && (
<nav className="flex items-center sm:gap-1">
{NAV_ITEMS.map(({to, labelKey, Icon, alsoActiveOn = []}) => (
{NAV_ITEMS.map(({to, labelKey, Icon, alsoActiveOn = [], iconOnly}) => (
<NavLink key={to} to={to} title={t(labelKey)}
className={({isActive}) => navLinkClass(isActive || alsoActiveOn.includes(pathname))}>
<Icon className="size-4"/>
<span className="hidden lg:inline">{t(labelKey)}</span>
{!iconOnly && <span className="hidden lg:inline">{t(labelKey)}</span>}
</NavLink>
))}
</nav>
)}
<div className="ml-auto flex min-w-0 items-center gap-1">
{/* Below md the badge hangs on the header's bottom edge: in the row it would push the page sideways. */}
{/* The badge hangs on the header's bottom edge at every width: in the row it would push the page sideways. */}
{isAuthenticated && viewingAs && (
<Badge variant="secondary" className="max-md:absolute max-md:top-full max-md:left-1/2 max-md:-translate-x-1/2 max-md:-translate-y-1/2 max-md:pointer-events-none max-md:border-border md:mr-1">{t('NAV.VIEW_AS.BADGE', {role: t(`ROLES.${viewingAs}`)})}</Badge>
<Badge variant="secondary" className="absolute top-full left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none border-border">{t('NAV.VIEW_AS.BADGE', {role: t(`ROLES.${viewingAs}`)})}</Badge>
)}
{isAuthenticated && wallet && (
<Button asChild variant="ghost" size="sm" title={`${t('POINTS.TITLE')}: ${wallet.balance}`} className="min-w-0 shrink">
<Button asChild variant="ghost" size="sm" title={`${t('POINTS.TITLE')}: ${wallet.balance}`} className="min-w-0 shrink lg:shrink-0">
<Link to="/points"><Coins className="text-primary"/><span className="min-w-0 truncate tabular-nums">{wallet.balance}</span></Link>
</Button>
)}
+25 -2
View File
@@ -16,11 +16,11 @@ afterEach(() => {
useTranslation().i18n.language = 'de';
});
const loggedInAs = (me, routes = {}) => {
const loggedInAs = (me, routes = {}, options = {}) => {
localStorage.setItem('tbp_session', '1');
fetchRoutes({'GET /api/users/me': json(200, me), 'GET /api/library/steam-profile': json(200, {}),
'GET /api/inbox': json(200, {results: [], unread: 0}), ...routes});
renderWithProviders(<AppHeader isAuthenticated onLogout={() => {}}/>);
renderWithProviders(<AppHeader isAuthenticated onLogout={() => {}}/>, options);
};
const openMenu = () => fireEvent.keyDown(screen.getByRole('button', {name: 'NAV.MENU'}), {key: 'Enter'});
@@ -44,6 +44,29 @@ describe('AppHeader', () => {
screen.getByText('ToBePlayed').style.display = 'none';
expect(screen.getByRole('link', {name: 'ToBePlayed'})).toBeTruthy();
expect(screen.queryByTitle('NAV.COMMUNITY')).toBeNull();
});
it('links to the community stats when logged in', () => {
loggedInAs({role: 'USER', activeRole: 'USER'});
const link = screen.getByTitle('NAV.COMMUNITY');
expect(link.getAttribute('href')).toBe('/');
expect(link.getAttribute('aria-current')).toBe('page');
});
it('shows the community link as an icon without a text label', () => {
loggedInAs({role: 'USER', activeRole: 'USER'});
expect(screen.getByTitle('NAV.COMMUNITY').textContent).toBe('');
});
it('marks community active only on the landing page', () => {
loggedInAs({role: 'USER', activeRole: 'USER'}, {}, {entries: ['/dashboard']});
expect(screen.getByTitle('NAV.COMMUNITY').getAttribute('aria-current')).toBeNull();
expect(screen.getByTitle('NAV.DASHBOARD').getAttribute('aria-current')).toBe('page');
});
it('shows how many inbox notes are unread', async () => {