Scaffold global navigation aligned to routes plan
Build out the full web route map from music-selfhost-routes.md as scaffolding (no functionality on new screens): - Full route tree: /login, /albums/:id, /artists/:id, /playlists(+detail), /discover, /upload, metadata editor (single + batch), /storage/maintenance, /queue, nested /settings and /admin, and a 404. - Sidebar rebuilt to the A1 spec with permission-gated Discover/Upload. - ProtectedRoute gains requirePermission; Permission exported. - AppShell wraps Outlet in a Suspense boundary for lazy routes. - Reusable Placeholder + SubNav; Settings/Admin become nested layouts. - Settings/Profile: wired language + theme selectors. - Remove orphaned Home feature (web has no Home; / -> /library) and the now-unused house icon + nav.home keys. - i18n keys (en + ru) and CSS for page-title/sub-nav. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { NavLink, useNavigate } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, type IconName } from '../common/Icon';
|
||||
import { useAppDispatch } from '../../hooks/useAppDispatch';
|
||||
import { usePermissions } from '../../hooks/usePermissions';
|
||||
import { usePermissions, type Permission } from '../../hooks/usePermissions';
|
||||
import { useConnectionStatus } from '../../hooks/useConnectionStatus';
|
||||
import { logout } from '../../store/slices/auth';
|
||||
import { useGetPlaylistsQuery } from '../../api/endpoints/playlists';
|
||||
@@ -13,13 +13,15 @@ interface NavDef {
|
||||
labelKey: string;
|
||||
icon: IconName;
|
||||
end?: boolean;
|
||||
/** Hide this item unless the user holds the permission. */
|
||||
perm?: Permission;
|
||||
}
|
||||
|
||||
const MAIN_NAV: NavDef[] = [
|
||||
{ to: '/', labelKey: 'nav.home', icon: 'house', end: true },
|
||||
{ to: '/library', labelKey: 'nav.library', icon: 'vinyl-record' },
|
||||
{ to: '/search', labelKey: 'nav.search', icon: 'magnifying-glass' },
|
||||
{ to: '/downloads', labelKey: 'nav.downloads', icon: 'arrow-circle-down' },
|
||||
{ to: '/discover', labelKey: 'nav.search', icon: 'magnifying-glass', perm: 'download' },
|
||||
{ to: '/downloads', labelKey: 'nav.downloads', icon: 'arrow-circle-down', perm: 'download' },
|
||||
{ to: '/upload', labelKey: 'nav.upload', icon: 'upload-simple', perm: 'upload' },
|
||||
{ to: '/storage', labelKey: 'nav.storage', icon: 'hard-drives' },
|
||||
];
|
||||
|
||||
@@ -38,7 +40,7 @@ export function Sidebar() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { user, isAdmin } = usePermissions();
|
||||
const { user, isAdmin, hasPermission } = usePermissions();
|
||||
const status = useConnectionStatus();
|
||||
const { data: playlists } = useGetPlaylistsQuery();
|
||||
const instance = getActiveInstance();
|
||||
@@ -61,20 +63,24 @@ export function Sidebar() {
|
||||
</div>
|
||||
|
||||
<div className="sb-sec">
|
||||
{MAIN_NAV.map(({ to, labelKey, icon, end }) => (
|
||||
<NavLink key={to} to={to} end={end} className={navClass}>
|
||||
<Icon name={icon} />
|
||||
<span>{t(labelKey)}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
{MAIN_NAV.filter((n) => !n.perm || hasPermission(n.perm)).map(
|
||||
({ to, labelKey, icon, end }) => (
|
||||
<NavLink key={to} to={to} end={end} className={navClass}>
|
||||
<Icon name={icon} />
|
||||
<span>{t(labelKey)}</span>
|
||||
</NavLink>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sb-sec">
|
||||
<span className="msk-label">{t('nav.playlists')}</span>
|
||||
<NavLink to="/playlists" end className="msk-label sb-sec-link">
|
||||
{t('nav.playlists')}
|
||||
</NavLink>
|
||||
{(playlists?.items ?? []).map((pl) => (
|
||||
<NavLink
|
||||
key={pl.id}
|
||||
to={`/library/playlists/${pl.id}`}
|
||||
to={`/playlists/${pl.id}`}
|
||||
className={navClass}
|
||||
>
|
||||
<Icon name="playlist" />
|
||||
@@ -84,7 +90,7 @@ export function Sidebar() {
|
||||
<button
|
||||
type="button"
|
||||
className="pl-item"
|
||||
onClick={() => void navigate('/library')}
|
||||
onClick={() => void navigate('/playlists')}
|
||||
>
|
||||
<Icon name="plus" />
|
||||
<span className="pl-name">{t('nav.newPlaylist')}</span>
|
||||
@@ -138,7 +144,11 @@ export function Sidebar() {
|
||||
{user.role} · {online ? t('user.online') : t('user.offline')}
|
||||
</div>
|
||||
</div>
|
||||
<span className="uc-action" onClick={handleLogout} title={t('user.signOut')}>
|
||||
<span
|
||||
className="uc-action"
|
||||
onClick={handleLogout}
|
||||
title={t('user.signOut')}
|
||||
>
|
||||
<Icon name="sign-out" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user