Public Access
feat(header): add community link to nav
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:
1 parent
e7b08cfb47
commit
1dab069a4d
4 files changed
+35
-9
No files matched your search
@@ -380,6 +380,7 @@
|
|||||||
"LIGHT_MODE": "Helles Design",
|
"LIGHT_MODE": "Helles Design",
|
||||||
"PLAY_NEXT": "Nächstes Spiel",
|
"PLAY_NEXT": "Nächstes Spiel",
|
||||||
"STATS": "Statistik",
|
"STATS": "Statistik",
|
||||||
|
"COMMUNITY": "Community",
|
||||||
"POINTS": "Pile Points",
|
"POINTS": "Pile Points",
|
||||||
"ADMIN": "Admin",
|
"ADMIN": "Admin",
|
||||||
"VIEW_AS": {
|
"VIEW_AS": {
|
||||||
|
|||||||
@@ -380,6 +380,7 @@
|
|||||||
"LIGHT_MODE": "Light mode",
|
"LIGHT_MODE": "Light mode",
|
||||||
"PLAY_NEXT": "Play next",
|
"PLAY_NEXT": "Play next",
|
||||||
"STATS": "Stats",
|
"STATS": "Stats",
|
||||||
|
"COMMUNITY": "Community",
|
||||||
"POINTS": "Pile Points",
|
"POINTS": "Pile Points",
|
||||||
"ADMIN": "Admin",
|
"ADMIN": "Admin",
|
||||||
"VIEW_AS": {
|
"VIEW_AS": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {Link, NavLink, useLocation, useNavigate} from 'react-router-dom';
|
import {Link, NavLink, useLocation, useNavigate} from 'react-router-dom';
|
||||||
import {useTranslation} from 'react-i18next';
|
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 {toast} from 'sonner';
|
||||||
|
|
||||||
import {Badge} from '@/components/ui/badge';
|
import {Badge} from '@/components/ui/badge';
|
||||||
@@ -34,10 +34,11 @@ const NAV_ITEMS = [
|
|||||||
{to: '/library', labelKey: 'NAV.LIBRARY', Icon: Library, alsoActiveOn: ['/wishlist', '/backlog']},
|
{to: '/library', labelKey: 'NAV.LIBRARY', Icon: Library, alsoActiveOn: ['/wishlist', '/backlog']},
|
||||||
{to: '/play-next', labelKey: 'NAV.PLAY_NEXT', Icon: Dices},
|
{to: '/play-next', labelKey: 'NAV.PLAY_NEXT', Icon: Dices},
|
||||||
{to: '/stats', labelKey: 'NAV.STATS', Icon: ChartColumn},
|
{to: '/stats', labelKey: 'NAV.STATS', Icon: ChartColumn},
|
||||||
|
{to: '/', labelKey: 'NAV.COMMUNITY', Icon: Users, iconOnly: true},
|
||||||
];
|
];
|
||||||
|
|
||||||
const navLinkClass = (isActive) => cn(
|
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
|
isActive
|
||||||
? 'bg-accent text-accent-foreground'
|
? 'bg-accent text-accent-foreground'
|
||||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
|
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
|
||||||
@@ -128,23 +129,23 @@ export default function AppHeader({isAuthenticated, onLogout}) {
|
|||||||
|
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<nav className="flex items-center sm:gap-1">
|
<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)}
|
<NavLink key={to} to={to} title={t(labelKey)}
|
||||||
className={({isActive}) => navLinkClass(isActive || alsoActiveOn.includes(pathname))}>
|
className={({isActive}) => navLinkClass(isActive || alsoActiveOn.includes(pathname))}>
|
||||||
<Icon className="size-4"/>
|
<Icon className="size-4"/>
|
||||||
<span className="hidden lg:inline">{t(labelKey)}</span>
|
{!iconOnly && <span className="hidden lg:inline">{t(labelKey)}</span>}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="ml-auto flex min-w-0 items-center gap-1">
|
<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 && (
|
{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 && (
|
{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>
|
<Link to="/points"><Coins className="text-primary"/><span className="min-w-0 truncate tabular-nums">{wallet.balance}</span></Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ afterEach(() => {
|
|||||||
useTranslation().i18n.language = 'de';
|
useTranslation().i18n.language = 'de';
|
||||||
});
|
});
|
||||||
|
|
||||||
const loggedInAs = (me, routes = {}) => {
|
const loggedInAs = (me, routes = {}, options = {}) => {
|
||||||
localStorage.setItem('tbp_session', '1');
|
localStorage.setItem('tbp_session', '1');
|
||||||
fetchRoutes({'GET /api/users/me': json(200, me), 'GET /api/library/steam-profile': json(200, {}),
|
fetchRoutes({'GET /api/users/me': json(200, me), 'GET /api/library/steam-profile': json(200, {}),
|
||||||
'GET /api/inbox': json(200, {results: [], unread: 0}), ...routes});
|
'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'});
|
const openMenu = () => fireEvent.keyDown(screen.getByRole('button', {name: 'NAV.MENU'}), {key: 'Enter'});
|
||||||
@@ -44,6 +44,29 @@ describe('AppHeader', () => {
|
|||||||
screen.getByText('ToBePlayed').style.display = 'none';
|
screen.getByText('ToBePlayed').style.display = 'none';
|
||||||
|
|
||||||
expect(screen.getByRole('link', {name: 'ToBePlayed'})).toBeTruthy();
|
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 () => {
|
it('shows how many inbox notes are unread', async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user