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
+109 -51
View File
@@ -1,14 +1,38 @@
import { lazy } from 'react';
import { Routes, Route, Navigate } from 'react-router';
import { AppShell } from '../components/layout/AppShell';
import { ProtectedRoute } from './ProtectedRoute';
// Public (outside the shell)
import { ConnectPage } from '../features/connect/ConnectPage';
import { HomePage } from '../features/home/HomePage';
import { LoginPage } from '../features/auth/LoginPage';
// Core screens (eager — first paint inside the shell)
import { LibraryPage } from '../features/library/LibraryPage';
import { AlbumDetailPage } from '../features/album-detail/AlbumDetailPage';
import { ArtistDetailPage } from '../features/artist-detail/ArtistDetailPage';
import { PlaylistsPage } from '../features/playlists/PlaylistsPage';
import { PlaylistDetailPage } from '../features/playlist-detail/PlaylistDetailPage';
import { lazy, Suspense } from 'react';
import { LoadingSkeleton } from '../components/common/LoadingSkeleton';
// Settings / Admin layouts + panels (small, eager)
import { SettingsPage } from '../features/settings/SettingsPage';
import {
ProfileSettings,
PlaybackSettings,
ScrobblingSettings,
InstanceSettings,
} from '../features/settings/panels';
import { AdminPage } from '../features/admin/AdminPage';
import {
AdminUsers,
AdminUserDetail,
AdminSources,
AdminInstance,
} from '../features/admin/panels';
import { NotFoundPage } from '../features/not-found/NotFoundPage';
// Secondary screens — lazily loaded (Suspense boundary lives in AppShell)
const SearchDownloadPage = lazy(() =>
import('../features/search-download/SearchDownloadPage').then((m) => ({
default: m.SearchDownloadPage,
@@ -19,30 +43,36 @@ const DownloadsManagerPage = lazy(() =>
default: m.DownloadsManagerPage,
})),
);
const UploadPage = lazy(() =>
import('../features/upload/UploadPage').then((m) => ({ default: m.UploadPage })),
);
const MetadataEditorPage = lazy(() =>
import('../features/metadata-editor/MetadataEditorPage').then((m) => ({
default: m.MetadataEditorPage,
})),
);
const StoragePage = lazy(() =>
import('../features/storage/StoragePage').then((m) => ({
default: m.StoragePage,
})),
);
const AdminPage = lazy(() =>
import('../features/admin/AdminPage').then((m) => ({ default: m.AdminPage })),
);
const SettingsPage = lazy(() =>
import('../features/settings/SettingsPage').then((m) => ({
default: m.SettingsPage,
const StorageMaintenancePage = lazy(() =>
import('../features/storage/StorageMaintenancePage').then((m) => ({
default: m.StorageMaintenancePage,
})),
);
const Fallback = () => (
<div style={{ padding: '2rem' }}>
<LoadingSkeleton />
</div>
const QueuePage = lazy(() =>
import('../features/queue/QueuePage').then((m) => ({ default: m.QueuePage })),
);
export function AppRoutes() {
return (
<Routes>
{/* Public */}
<Route path="/connect" element={<ConnectPage />} />
<Route path="/login" element={<LoginPage />} />
{/* Authenticated shell */}
<Route
element={
<ProtectedRoute>
@@ -50,57 +80,85 @@ export function AppRoutes() {
</ProtectedRoute>
}
>
<Route index element={<HomePage />} />
<Route index element={<Navigate to="/library" replace />} />
{/* Library */}
<Route path="/library" element={<LibraryPage />} />
<Route path="/library/albums/:albumId" element={<AlbumDetailPage />} />
<Route path="/albums/:albumId" element={<AlbumDetailPage />} />
<Route path="/artists/:artistId" element={<ArtistDetailPage />} />
{/* Playlists */}
<Route path="/playlists" element={<PlaylistsPage />} />
<Route path="/playlists/:playlistId" element={<PlaylistDetailPage />} />
{/* Discover & downloads (permission-gated) */}
<Route
path="/library/playlists/:playlistId"
element={<PlaylistDetailPage />}
/>
<Route
path="/search"
path="/discover"
element={
<Suspense fallback={<Fallback />}>
<ProtectedRoute requirePermission="download">
<SearchDownloadPage />
</Suspense>
</ProtectedRoute>
}
/>
<Route
path="/downloads"
element={
<Suspense fallback={<Fallback />}>
<ProtectedRoute requirePermission="download">
<DownloadsManagerPage />
</Suspense>
}
/>
<Route
path="/storage"
element={
<Suspense fallback={<Fallback />}>
<StoragePage />
</Suspense>
}
/>
<Route
path="/settings"
element={
<Suspense fallback={<Fallback />}>
<SettingsPage />
</Suspense>
}
/>
<Route
path="/admin/*"
element={
<ProtectedRoute requireAdmin>
<Suspense fallback={<Fallback />}>
<AdminPage />
</Suspense>
</ProtectedRoute>
}
/>
{/* Upload & metadata */}
<Route
path="/upload"
element={
<ProtectedRoute requirePermission="upload">
<UploadPage />
</ProtectedRoute>
}
/>
<Route
path="/tracks/:trackId/metadata"
element={<MetadataEditorPage />}
/>
<Route path="/metadata/batch" element={<MetadataEditorPage batch />} />
{/* Storage */}
<Route path="/storage" element={<StoragePage />} />
<Route path="/storage/maintenance" element={<StorageMaintenancePage />} />
{/* Queue (narrow viewports) */}
<Route path="/queue" element={<QueuePage />} />
{/* Settings */}
<Route path="/settings" element={<SettingsPage />}>
<Route index element={<Navigate to="/settings/profile" replace />} />
<Route path="profile" element={<ProfileSettings />} />
<Route path="playback" element={<PlaybackSettings />} />
<Route path="scrobbling" element={<ScrobblingSettings />} />
<Route path="instance" element={<InstanceSettings />} />
</Route>
{/* Admin (admin-gated) */}
<Route
path="/admin"
element={
<ProtectedRoute requireAdmin>
<AdminPage />
</ProtectedRoute>
}
>
<Route index element={<Navigate to="/admin/users" replace />} />
<Route path="users" element={<AdminUsers />} />
<Route path="users/:userId" element={<AdminUserDetail />} />
<Route path="sources" element={<AdminSources />} />
<Route path="instance" element={<AdminInstance />} />
</Route>
{/* 404 */}
<Route path="*" element={<NotFoundPage />} />
</Route>
<Route path="*" element={<Navigate to="/library" replace />} />
</Routes>
);
}