feat(web): storage admin, search and catalog pages (M2)
- Storage admin: local folders with a folder picker and NFS sources, connection test, scan and full scan with live progress, enable, disable, delete - path pattern editor with reorder, examples and a preview table that shows which pattern matched and what came from tags; compile errors land under the broken pattern - Library page with counters, scans and unreadable files; search reindex for admins - search box in the top bar: combobox with keyboard navigation, 150 ms debounce, cancelled stale requests, "/" and Ctrl+K, layout fix hint - search page with tabs and paging, artist and album pages, album cards and track lists; unavailable tracks are dimmed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c1160cccd3
commit
a685b1b7ca
27 files changed
+2505
-9
No files matched your search
@@ -9,9 +9,15 @@ import {
|
||||
} from '@tanstack/react-router';
|
||||
|
||||
import { AuditPage } from '../pages/admin/AuditPage';
|
||||
import { LibraryPage } from '../pages/admin/LibraryPage';
|
||||
import { PatternsPage } from '../pages/admin/PatternsPage';
|
||||
import { SourcesPage } from '../pages/admin/SourcesPage';
|
||||
import { UsersPage } from '../pages/admin/UsersPage';
|
||||
import { AlbumPage } from '../pages/AlbumPage';
|
||||
import { ArtistPage } from '../pages/ArtistPage';
|
||||
import { HomePage } from '../pages/HomePage';
|
||||
import { LoginPage } from '../pages/LoginPage';
|
||||
import { SearchPage, type SearchType } from '../pages/SearchPage';
|
||||
import { SettingsPage } from '../pages/SettingsPage';
|
||||
import { SetupPage } from '../pages/SetupPage';
|
||||
import { AppShell } from '../widgets/AppShell';
|
||||
@@ -96,6 +102,10 @@ const appRoute = createRoute({
|
||||
component: AppShell,
|
||||
});
|
||||
|
||||
const adminOnly = ({ context }: { context: { user: AuthUser } }) => {
|
||||
if (context.user.role !== 'admin') throw redirect({ to: '/' });
|
||||
};
|
||||
|
||||
const homeRoute = createRoute({ getParentRoute: () => appRoute, path: '/', component: HomePage });
|
||||
|
||||
const settingsRoute = createRoute({ getParentRoute: () => appRoute, path: '/settings', component: SettingsPage });
|
||||
@@ -112,17 +122,86 @@ const usersRoute = createRoute({
|
||||
const auditRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/admin/audit',
|
||||
beforeLoad: ({ context }) => {
|
||||
if (context.user.role !== 'admin') throw redirect({ to: '/' });
|
||||
},
|
||||
beforeLoad: adminOnly,
|
||||
component: AuditPage,
|
||||
});
|
||||
|
||||
const searchRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/search',
|
||||
validateSearch: (search: Record<string, unknown>): { q: string; type?: Exclude<SearchType, 'all'> } => {
|
||||
const type = search.type;
|
||||
return {
|
||||
q: typeof search.q === 'string' ? search.q : '',
|
||||
...(type === 'artists' || type === 'albums' || type === 'tracks' ? { type } : {}),
|
||||
};
|
||||
},
|
||||
component: function SearchRoute() {
|
||||
const { q, type } = searchRoute.useSearch();
|
||||
return <SearchPage q={q} type={type ?? 'all'} />;
|
||||
},
|
||||
});
|
||||
|
||||
const artistRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/artist/$id',
|
||||
component: function ArtistRoute() {
|
||||
const { id } = artistRoute.useParams();
|
||||
return <ArtistPage key={id} id={id} />;
|
||||
},
|
||||
});
|
||||
|
||||
const albumRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/album/$id',
|
||||
component: function AlbumRoute() {
|
||||
const { id } = albumRoute.useParams();
|
||||
return <AlbumPage key={id} id={id} />;
|
||||
},
|
||||
});
|
||||
|
||||
const sourcesRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/admin/sources',
|
||||
beforeLoad: adminOnly,
|
||||
component: SourcesPage,
|
||||
});
|
||||
|
||||
const patternsRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/admin/sources/$id/patterns',
|
||||
beforeLoad: adminOnly,
|
||||
component: function PatternsRoute() {
|
||||
const { id } = patternsRoute.useParams();
|
||||
return <PatternsPage id={id} />;
|
||||
},
|
||||
});
|
||||
|
||||
const libraryRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/admin/library',
|
||||
beforeLoad: ({ context }) => {
|
||||
if (context.user.role === 'user') throw redirect({ to: '/' });
|
||||
},
|
||||
component: LibraryPage,
|
||||
});
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
loginRoute,
|
||||
setupAdminRoute,
|
||||
setupPasswordRoute,
|
||||
appRoute.addChildren([homeRoute, settingsRoute, usersRoute, auditRoute]),
|
||||
appRoute.addChildren([
|
||||
homeRoute,
|
||||
settingsRoute,
|
||||
searchRoute,
|
||||
artistRoute,
|
||||
albumRoute,
|
||||
usersRoute,
|
||||
auditRoute,
|
||||
sourcesRoute,
|
||||
patternsRoute,
|
||||
libraryRoute,
|
||||
]),
|
||||
]);
|
||||
|
||||
export function createAppRouter(session: AuthSession, opts: { history?: Parameters<typeof createRouter>[0]['history'] } = {}) {
|
||||
|
||||
Reference in new issue
Block a user