diff --git a/frontend/src/router.js b/frontend/src/router.js index 2073c84..fa86c0f 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -2,14 +2,12 @@ import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router' import SettingsView from './views/SettingsView.vue' import GalleryView from './views/GalleryView.vue' import ShowcaseView from './views/ShowcaseView.vue' -import TagsView from './views/TagsView.vue' +import BrowseView from './views/BrowseView.vue' import ArtistView from './views/ArtistView.vue' import SeriesView from './views/SeriesView.vue' import SeriesManageView from './views/SeriesManageView.vue' import SeriesReaderView from './views/SeriesReaderView.vue' import SubscriptionsView from './views/SubscriptionsView.vue' -import PostsView from './views/PostsView.vue' -import ArtistsView from './views/ArtistsView.vue' // The application's front door. `/` redirects here. Changing the front door // is a one-line edit (e.g. '/gallery' or '/tags'). @@ -22,27 +20,36 @@ const routes = [ // FC-2: image backbone { path: '/showcase', name: 'showcase', component: ShowcaseView, meta: { title: 'Showcase' } }, { path: '/gallery', name: 'gallery', component: GalleryView, meta: { title: 'Gallery' } }, - { path: '/artists', name: 'artists', component: ArtistsView, meta: { title: 'Artists' } }, - { path: '/tags', name: 'tags', component: TagsView, meta: { title: 'Tags' } }, + // Browse hub (operator-asked 2026-06-09): Posts / Artists / Tags as tabs — + // the three "browse the library by an axis" surfaces. One nav entry; the old + // standalone paths redirect into the matching tab (below). + { path: '/browse', name: 'browse', component: BrowseView, meta: { title: 'Browse' } }, // Artist detail — no meta.title (reached by clicking an artist, not nav). { path: '/artist/:slug', name: 'artist', component: ArtistView }, - // Series browse — a nav entry (meta.title). Peer of Posts. + // Series browse — a nav entry (meta.title). { path: '/series', name: 'series', component: SeriesView, meta: { title: 'Series' } }, // Series management — no meta.title (reached from a series card/tag). { path: '/series/:tagId', name: 'series-manage', component: SeriesManageView }, // Series reader — immersive (no top nav, no meta.title). { path: '/series/:tagId/read', name: 'series-read', component: SeriesReaderView, meta: { immersive: true } }, - { path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings' } }, - // FC-3: subscription backbone - // /credentials and /downloads were folded into /subscriptions as subtabs - // 2026-05-27 (?tab=settings and ?tab=downloads). The hub view owns the - // whole download pipeline domain. - { path: '/posts', name: 'posts', component: PostsView, meta: { title: 'Posts' } }, + // FC-3: subscription backbone — purely management (sources/downloads), + // distinct from the Browse hub. { path: '/subscriptions', name: 'subscriptions', component: SubscriptionsView, meta: { title: 'Subscriptions' } }, - // Bookmark/back-button safety net for the routes that got folded in - // (no meta.title — stay out of TopNav). + // Settings — config, pinned to the right of the nav (TopNav special-cases it). + { path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings' } }, + + // The old standalone paths now redirect into the Browse hub, preserving any + // deep-link query (e.g. /posts?post_id=N → /browse?tab=posts&post_id=N). The + // route NAMES stay so existing { name: 'posts' | 'artists' | 'tags' } links + // and path pushes keep resolving. + { path: '/posts', name: 'posts', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'posts' } }) }, + { path: '/artists', name: 'artists', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'artists' } }) }, + { path: '/tags', name: 'tags', redirect: (to) => ({ name: 'browse', query: { ...to.query, tab: 'tags' } }) }, + + // Bookmark/back-button safety net for the routes that got folded into + // /subscriptions (no meta.title — stay out of TopNav). { path: '/credentials', redirect: '/subscriptions?tab=settings' }, { path: '/downloads', redirect: '/subscriptions?tab=downloads' } ] diff --git a/frontend/src/views/BrowseView.vue b/frontend/src/views/BrowseView.vue new file mode 100644 index 0000000..76a8da4 --- /dev/null +++ b/frontend/src/views/BrowseView.vue @@ -0,0 +1,47 @@ + + + diff --git a/frontend/test/router.spec.js b/frontend/test/router.spec.js index c976fd3..338412f 100644 --- a/frontend/test/router.spec.js +++ b/frontend/test/router.spec.js @@ -17,13 +17,29 @@ describe('router', () => { expect(router.resolve('/gallery').name).toBe('gallery') }) - it('showcase, tags, artist resolve to named routes', () => { + it('showcase, artist resolve to named routes', () => { expect(router.resolve('/showcase').name).toBe('showcase') - expect(router.resolve('/tags').name).toBe('tags') expect(router.resolve('/artist/some-slug').name).toBe('artist') expect(router.resolve('/artist/some-slug').params.slug).toBe('some-slug') }) + it('browse hub is reachable and old axis paths redirect into its tabs', async () => { + expect(router.resolve('/browse').name).toBe('browse') + + await router.push('/tags') + expect(router.currentRoute.value.name).toBe('browse') + expect(router.currentRoute.value.query.tab).toBe('tags') + + await router.push('/artists') + expect(router.currentRoute.value.query.tab).toBe('artists') + + // A posts deep link survives the redirect into the hub tab. + await router.push({ path: '/posts', query: { post_id: '7' } }) + expect(router.currentRoute.value.name).toBe('browse') + expect(router.currentRoute.value.query.tab).toBe('posts') + expect(router.currentRoute.value.query.post_id).toBe('7') + }) + it('series-read is an immersive route', () => { const r = router.resolve('/series/5/read') expect(r.name).toBe('series-read')