feat(ui): add follow button, follows card, and inbox page
Build, Test & Push Frontend / quality-check (push) Successful in 34s
Build, Test & Push Frontend / docker (push) Successful in 23s

This commit is contained in:
lobo committed 2026-10-06 16:22:22 +02:00
1 parent 3aed243e35
commit 43c4862606
23 files changed
+603 -2

No files matched your search

+32 -1
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 {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} from 'lucide-react';
import {toast} from 'sonner';
import {Badge} from '@/components/ui/badge';
@@ -25,6 +25,7 @@ import useCurrentUser from '../hooks/useCurrentUser.js';
import useSteamProfile from '../hooks/useSteamProfile.js';
import {fetchPendingCount, ROLES, viewAs} from '../utils/admin.js';
import {canCorrect} from '../utils/corrections.js';
import {fetchInbox} from '../utils/inbox.js';
import {expireSession, markSession} from '../utils/auth.js';
const NAV_ITEMS = [
@@ -70,6 +71,20 @@ export default function AppHeader({isAuthenticated, onLogout}) {
}, [seesAdminPage, pathname, menuOpenings]);
const shownCount = seesAdminPage ? pendingCount : 0;
// Loaded again on every page change. The inbox page marks the notes read, so it shows no count.
const [unread, setUnread] = useState(0);
useEffect(() => {
if (!isAuthenticated) return undefined;
let current = true;
fetchInbox().then(({ok, data}) => {
if (current && ok) setUnread(data.unread);
});
return () => {
current = false;
};
}, [isAuthenticated, pathname]);
const shownUnread = pathname === '/inbox' ? 0 : unread;
const switchRole = async (role) => {
try {
const response = await viewAs(role === currentUser.role ? null : role);
@@ -133,6 +148,19 @@ export default function AppHeader({isAuthenticated, onLogout}) {
<Link to="/points"><Coins className="text-primary"/><span className="min-w-0 truncate tabular-nums">{wallet.balance}</span></Link>
</Button>
)}
{isAuthenticated && (
<Button asChild variant="ghost" size="icon" title={t('NAV.INBOX')} className="relative shrink-0">
<Link to="/inbox" aria-label={shownUnread > 0 ? t('NAV.INBOX_UNREAD', {count: shownUnread}) : t('NAV.INBOX')}>
<Bell/>
{shownUnread > 0 && (
<span aria-hidden="true"
className="absolute -top-1 -right-1 flex size-4 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-foreground">
{shownUnread > 9 ? '9+' : shownUnread}
</span>
)}
</Link>
</Button>
)}
{isAuthenticated ? (
<DropdownMenu onOpenChange={(open) => open && setMenuOpenings((count) => count + 1)}>
<DropdownMenuTrigger asChild>
@@ -154,6 +182,9 @@ export default function AppHeader({isAuthenticated, onLogout}) {
<DropdownMenuItem asChild>
<Link to="/points"><Coins/>{t('NAV.POINTS')}</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/inbox"><Bell/>{t('NAV.INBOX')}</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/whats-new"><Newspaper/>{t('NAV.WHATS_NEW')}</Link>
</DropdownMenuItem>