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:
Senko-san
2026-06-07 17:05:21 +03:00
parent e45bcef3a5
commit aed0572071
25 changed files with 603 additions and 541 deletions
+24 -5
View File
@@ -1,13 +1,32 @@
import { Outlet } from 'react-router';
import { useTranslation } from 'react-i18next';
import { Window } from '@olly/modern-sk';
import { SubNav, type SubNavItem } from '../../components/common/SubNav';
/**
* `/admin` — A9 admin shell (admin-gated). Secondary nav + nested `<Outlet/>`
* for users/sources/instance. `/admin` redirects to `/admin/users`.
*/
export function AdminPage() {
const { t } = useTranslation();
const items: SubNavItem[] = [
{ to: '/admin/users', label: t('admin.tabs.users') },
{ to: '/admin/sources', label: t('admin.tabs.sources') },
{ to: '/admin/instance', label: t('admin.tabs.instance') },
];
return (
<div style={{ padding: '1.5rem' }}>
<Window title={t('pages.admin')}>
<p style={{ color: 'var(--color-text-2)' }}>{t('common.comingSoon')}</p>
</Window>
<div
style={{
padding: '1.5rem',
display: 'flex',
flexDirection: 'column',
gap: '1.25rem',
}}
>
<h1 className="page-title">{t('pages.admin')}</h1>
<SubNav items={items} />
<Outlet />
</div>
);
}
+37
View File
@@ -0,0 +1,37 @@
import { useTranslation } from 'react-i18next';
import { Window } from '@olly/modern-sk';
function StubPanel({ title }: { title: string }) {
const { t } = useTranslation();
return (
<Window title={title}>
<p style={{ color: 'var(--color-text-2)', margin: 0 }}>
{t('common.comingSoon')}
</p>
</Window>
);
}
/** `/admin/users` — user list (add/remove). Scaffold. */
export function AdminUsers() {
const { t } = useTranslation();
return <StubPanel title={t('admin.tabs.users')} />;
}
/** `/admin/users/:userId` — per-user permissions / reset password / status. Scaffold. */
export function AdminUserDetail() {
const { t } = useTranslation();
return <StubPanel title={t('admin.userDetail')} />;
}
/** `/admin/sources` — pluggable source management (creds/cookies/status). Scaffold. */
export function AdminSources() {
const { t } = useTranslation();
return <StubPanel title={t('admin.tabs.sources')} />;
}
/** `/admin/instance` — service health, ML_SERVICE_URL, reindex. Scaffold. */
export function AdminInstance() {
const { t } = useTranslation();
return <StubPanel title={t('admin.tabs.instance')} />;
}