208 lines
6.8 KiB
TypeScript
208 lines
6.8 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { useEffect, useState, type SVGProps } from 'react';
|
|
import { useI18n } from './I18nProvider';
|
|
|
|
type SessionUser = { id: string; email: string; role: string; status: string };
|
|
|
|
function Icon({ name }: { name: string }) {
|
|
const common: SVGProps<SVGSVGElement> = {
|
|
width: 16,
|
|
height: 16,
|
|
stroke: 'currentColor',
|
|
fill: 'none',
|
|
strokeWidth: 1.6,
|
|
strokeLinecap: 'round',
|
|
strokeLinejoin: 'round',
|
|
};
|
|
switch (name) {
|
|
case 'profile':
|
|
return (
|
|
<svg {...common} viewBox="0 0 24 24" aria-hidden>
|
|
<circle cx="12" cy="8" r="4" />
|
|
<path d="M5 20c0-3.3 3.1-6 7-6s7 2.7 7 6" />
|
|
</svg>
|
|
);
|
|
case 'list':
|
|
return (
|
|
<svg {...common} viewBox="0 0 24 24" aria-hidden>
|
|
<path d="M9 6h11" />
|
|
<path d="M9 12h11" />
|
|
<path d="M9 18h11" />
|
|
<path d="M4 6h0.01" />
|
|
<path d="M4 12h0.01" />
|
|
<path d="M4 18h0.01" />
|
|
</svg>
|
|
);
|
|
case 'plus':
|
|
return (
|
|
<svg {...common} viewBox="0 0 24 24" aria-hidden>
|
|
<path d="M12 5v14" />
|
|
<path d="M5 12h14" />
|
|
</svg>
|
|
);
|
|
case 'logout':
|
|
return (
|
|
<svg {...common} viewBox="0 0 24 24" aria-hidden>
|
|
<path d="M9 21H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3" />
|
|
<path d="M16 17l5-5-5-5" />
|
|
<path d="M21 12H9" />
|
|
</svg>
|
|
);
|
|
case 'login':
|
|
return (
|
|
<svg {...common} viewBox="0 0 24 24" aria-hidden>
|
|
<path d="M15 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3" />
|
|
<path d="M10 17l5-5-5-5" />
|
|
<path d="M15 12H3" />
|
|
</svg>
|
|
);
|
|
case 'users':
|
|
return (
|
|
<svg {...common} viewBox="0 0 24 24" aria-hidden>
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
|
<circle cx="9" cy="7" r="4" />
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
</svg>
|
|
);
|
|
case 'check':
|
|
return (
|
|
<svg {...common} viewBox="0 0 24 24" aria-hidden>
|
|
<path d="M20 6L9 17l-5-5" />
|
|
</svg>
|
|
);
|
|
case 'globe':
|
|
return (
|
|
<svg {...common} viewBox="0 0 24 24" aria-hidden>
|
|
<circle cx="12" cy="12" r="9" />
|
|
<path d="M3 12h18" />
|
|
<path d="M12 3a15.3 15.3 0 0 1 4 9 15.3 15.3 0 0 1-4 9 15.3 15.3 0 0 1-4-9 15.3 15.3 0 0 1 4-9z" />
|
|
</svg>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default function NavBar() {
|
|
const { t, locale, setLocale } = useI18n();
|
|
const [user, setUser] = useState<SessionUser | null>(null);
|
|
const [pendingCount, setPendingCount] = useState<number>(0);
|
|
|
|
async function loadUser() {
|
|
try {
|
|
const res = await fetch('/api/auth/me', { cache: 'no-store' });
|
|
const data = await res.json();
|
|
if (data.user) setUser(data.user);
|
|
else setUser(null);
|
|
} catch (e) {
|
|
setUser(null);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadUser();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const role = user?.role;
|
|
const canSeeApprovals = role === 'ADMIN' || role === 'LISTING_MODERATOR' || role === 'USER_MODERATOR';
|
|
if (!canSeeApprovals) {
|
|
setPendingCount(0);
|
|
return;
|
|
}
|
|
fetch('/api/admin/pending/count', { cache: 'no-store' })
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
if (data && typeof data.total === 'number') setPendingCount(data.total);
|
|
})
|
|
.catch(() => setPendingCount(0));
|
|
}, [user]);
|
|
|
|
async function logout() {
|
|
try {
|
|
await fetch('/api/auth/logout', { method: 'POST' });
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
setUser(null);
|
|
}
|
|
|
|
const isAdmin = user?.role === 'ADMIN';
|
|
const isListingMod = user?.role === 'LISTING_MODERATOR';
|
|
const isUserMod = user?.role === 'USER_MODERATOR';
|
|
const showApprovals = Boolean(user && (isAdmin || isListingMod || isUserMod));
|
|
|
|
return (
|
|
<header style={{ padding: '12px 20px', borderBottom: '1px solid #eee', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
<Link href="/" className="brand">
|
|
{t('brand')}
|
|
</Link>
|
|
<Link href="/listings" className="button secondary">
|
|
<Icon name="list" /> {t('navBrowse')}
|
|
</Link>
|
|
</div>
|
|
<nav style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
{user ? (
|
|
<>
|
|
<span style={{ fontSize: 12, color: '#444', border: '1px solid #ddd', borderRadius: 12, padding: '4px 10px' }}>
|
|
{user.email} · {user.role}
|
|
</span>
|
|
<Link href="/me" className="button secondary">
|
|
<Icon name="profile" /> {t('navProfile')}
|
|
</Link>
|
|
<Link href="/listings/mine" className="button secondary">
|
|
<Icon name="list" /> {t('navMyListings')}
|
|
</Link>
|
|
<Link href="/listings/new" className="button secondary">
|
|
<Icon name="plus" /> {t('navNewListing')}
|
|
</Link>
|
|
{showApprovals ? (
|
|
<>
|
|
<Link href="/admin/pending" className="button secondary">
|
|
<Icon name="check" /> {t('navApprovals')}
|
|
{pendingCount > 0 ? (
|
|
<span style={{ marginLeft: 6, background: '#ff7043', color: '#fff', borderRadius: 10, padding: '2px 6px', fontSize: 12 }}>
|
|
{t('approvalsBadge', { count: pendingCount })}
|
|
</span>
|
|
) : null}
|
|
</Link>
|
|
{isAdmin ? (
|
|
<Link href="/admin/users" className="button secondary">
|
|
<Icon name="users" /> {t('navUsers')}
|
|
</Link>
|
|
) : null}
|
|
</>
|
|
) : null}
|
|
<button className="button secondary" onClick={logout}>
|
|
<Icon name="logout" /> {t('navLogout')}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Link href="/auth/login" className="button secondary">
|
|
<Icon name="login" /> {t('navLogin')}
|
|
</Link>
|
|
<Link href="/auth/register" className="button">
|
|
<Icon name="plus" /> {t('navSignup')}
|
|
</Link>
|
|
</>
|
|
)}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginLeft: 8 }}>
|
|
<span style={{ fontSize: 12, color: '#555', display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
<Icon name="globe" /> {t('navLanguage')}:
|
|
</span>
|
|
<button className="button secondary" onClick={() => setLocale('fi')} style={{ padding: '4px 8px', opacity: locale === 'fi' ? 1 : 0.7 }}>
|
|
FI
|
|
</button>
|
|
<button className="button secondary" onClick={() => setLocale('en')} style={{ padding: '4px 8px', opacity: locale === 'en' ? 1 : 0.7 }}>
|
|
EN
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
);
|
|
}
|