Artist detail pages previously always pushed /artist-detail to the URL, so refreshing the page or sharing a link would drop users on a broken empty page with no artist loaded. URL format is now /artist-detail/:source/:id (e.g. /artist-detail/spotify/4tZwfgrHOc3mvqsCAfo4LT or /artist-detail/library/42). The source segment lets the backend synthesize a response from the right metadata client without a DB hit. Changes: Client routing (legacy shell + TanStack bridge) - buildArtistDetailPath / _getDeepLinkArtistDetail added to init.js; parse both new :source/:id and legacy bare :id formats so old bookmarks still work - navigateToPage passes artistId + artistSource through to the router bridge, which builds the dynamic href instead of hardcoding route.path - resolveShellPageFromPath / resolveLegacyShellPageFromPath use a prefix match so /artist-detail/* resolves to artist-detail page-id - globals.d.ts typed for artistId / artistSource options - activateLegacyPath and syncActivePageFromLocation (popstate) both restore artist from URL using skipRouteChange:true to avoid a re-navigation loop back to /artist-detail - loadInitialData restores artist from URL on page load (router not yet mounted at DOMContentLoaded so legacy path runs unconditionally) - Same-artist guard in navigateToArtistDetail prevents double-fetch when the router fires activateLegacyPath after the initial navigation Server - artist_source_detail.build_source_only_artist_detail now resolves artist name from the source API when none is supplied, so deep-link restores with an empty name string still render correctly Tests - test_spa_deep_linking: /artist-detail/42 and /artist-detail/spotify/ID both serve index.html - bridge.test.ts: source-aware URL building and library fallback - route-manifest.test.ts: prefix path resolution - artist_source_detail: name resolved from source when input is empty
88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
export const shellPageIds = [
|
|
'dashboard',
|
|
'sync',
|
|
'search',
|
|
'discover',
|
|
'playlist-explorer',
|
|
'watchlist',
|
|
'wishlist',
|
|
'automations',
|
|
'active-downloads',
|
|
'library',
|
|
'tools',
|
|
'artist-detail',
|
|
'stats',
|
|
'import',
|
|
'settings',
|
|
'issues',
|
|
'help',
|
|
'hydrabase',
|
|
] as const;
|
|
|
|
export type ShellPageId = (typeof shellPageIds)[number];
|
|
export type ShellRouteKind = 'legacy' | 'react';
|
|
|
|
export interface ShellRouteDefinition {
|
|
pageId: ShellPageId;
|
|
path: `/${string}`;
|
|
kind: ShellRouteKind;
|
|
}
|
|
|
|
export const shellRouteManifest: readonly ShellRouteDefinition[] = [
|
|
{ pageId: 'dashboard', path: '/dashboard', kind: 'legacy' },
|
|
{ pageId: 'sync', path: '/sync', kind: 'legacy' },
|
|
{ pageId: 'search', path: '/search', kind: 'legacy' },
|
|
{ pageId: 'discover', path: '/discover', kind: 'legacy' },
|
|
{ pageId: 'playlist-explorer', path: '/playlist-explorer', kind: 'legacy' },
|
|
{ pageId: 'watchlist', path: '/watchlist', kind: 'legacy' },
|
|
{ pageId: 'wishlist', path: '/wishlist', kind: 'legacy' },
|
|
{ pageId: 'automations', path: '/automations', kind: 'legacy' },
|
|
{ pageId: 'active-downloads', path: '/active-downloads', kind: 'legacy' },
|
|
{ pageId: 'import', path: '/import', kind: 'legacy' },
|
|
{ pageId: 'library', path: '/library', kind: 'legacy' },
|
|
{ pageId: 'tools', path: '/tools', kind: 'legacy' },
|
|
{ pageId: 'artist-detail', path: '/artist-detail', kind: 'legacy' },
|
|
{ pageId: 'stats', path: '/stats', kind: 'legacy' },
|
|
{ pageId: 'settings', path: '/settings', kind: 'legacy' },
|
|
{ pageId: 'issues', path: '/issues', kind: 'react' },
|
|
{ pageId: 'help', path: '/help', kind: 'legacy' },
|
|
{ pageId: 'hydrabase', path: '/hydrabase', kind: 'legacy' },
|
|
] as const;
|
|
|
|
const routeByPageId = new Map(shellRouteManifest.map((route) => [route.pageId, route]));
|
|
const routeByPath = new Map(shellRouteManifest.map((route) => [route.path, route]));
|
|
|
|
export const reactShellRoutes = shellRouteManifest.filter((route) => route.kind === 'react');
|
|
export const legacyShellRoutes = shellRouteManifest.filter((route) => route.kind === 'legacy');
|
|
|
|
export function normalizeShellPath(pathname: string): string {
|
|
if (!pathname) return '/';
|
|
if (pathname === '/') return '/';
|
|
const normalized = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
|
|
return normalized || '/';
|
|
}
|
|
|
|
export function getShellRouteByPageId(pageId: ShellPageId): ShellRouteDefinition | undefined {
|
|
return routeByPageId.get(pageId);
|
|
}
|
|
|
|
export function getShellRouteByPath(pathname: string): ShellRouteDefinition | undefined {
|
|
return routeByPath.get(normalizeShellPath(pathname) as `/${string}`);
|
|
}
|
|
|
|
export function resolveShellPageFromPath(pathname: string): ShellPageId | null {
|
|
const normalized = normalizeShellPath(pathname);
|
|
if (normalized === '/artist-detail' || normalized.startsWith('/artist-detail/')) {
|
|
return 'artist-detail';
|
|
}
|
|
return getShellRouteByPath(pathname)?.pageId ?? null;
|
|
}
|
|
|
|
export function resolveLegacyShellPageFromPath(pathname: string): ShellPageId | null {
|
|
const normalized = normalizeShellPath(pathname);
|
|
if (normalized === '/artist-detail' || normalized.startsWith('/artist-detail/')) {
|
|
return 'artist-detail';
|
|
}
|
|
const route = getShellRouteByPath(pathname);
|
|
return route?.kind === 'legacy' ? route.pageId : null;
|
|
}
|