From 8641c3d013d5ba2a33a14ec6f0ce78c9332947d0 Mon Sep 17 00:00:00 2001 From: Aneurin Barker Snook Date: Sat, 5 Sep 2026 00:46:17 +0100 Subject: [PATCH] Make Explore and Kanban real routes, not tab state /projects/:id (name "project") is now Explore; /projects/:id/kanban (name "project-kanban") is Kanban. ProjectView.vue becomes a layout: header + sub-nav, loading the project and its cards (both children need the cards list) and rendering the active one via . ProjectExploreView.vue and ProjectKanbanView.vue hold what used to be each tab's own template/logic; Kanban additionally loads its own statuses, since Explore has no use for them. The sub-nav is now RouterLinks (active state matched on route.name), not buttons toggling local state. App.vue: the top-level was keyed by the full route path to force a fresh instance per project/card id -- with Explore/Kanban now separate paths under one layout, that would also remount the layout (and re-fetch the project) on every tab switch. Keyed by the matched route's top-level path + params instead, which is the same value for both of a project's child routes. AppSidebar: the project switcher and the inbox-drag refresh both used to check route.name === 'project' for "this project is open" -- fixed to cover both routes for the switcher, and narrowed to 'project-kanban' specifically for the inbox-drag refresh, since Kanban is the only route with a draggable list a card could have moved to/from. Co-Authored-By: Claude Sonnet 5 --- README.md | 2 + web/README.md | 39 +++-- web/src/App.vue | 15 +- web/src/components/AppSidebar.vue | 13 +- web/src/router/index.ts | 8 +- web/src/style.css | 6 +- web/src/views/ProjectExploreView.vue | 88 ++++++++++ web/src/views/ProjectKanbanView.vue | 153 +++++++++++++++++ web/src/views/ProjectView.vue | 247 +++------------------------ 9 files changed, 329 insertions(+), 242 deletions(-) create mode 100644 web/src/views/ProjectExploreView.vue create mode 100644 web/src/views/ProjectKanbanView.vue diff --git a/README.md b/README.md index 74d290e..5040c02 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ Each user owns **projects**, and each project holds ordered **cards**. | 14 | New-project form moved to the dashboard; sidebar project list is now a switcher dropdown; Kanban is a project's default tab | ✅ done | | 15 | Passkeys (WebAuthn) — register from the profile page, sign in with one instead of a magic link; a dismissible notice nudges users with none | ✅ done | | 16 | Project configuration view — manage a project's statuses: add, drag to reorder, delete (reassigning any cards on it first) | ✅ done | +| 17 | Card detail view (`/cards/:id`) + its own configuration view — a card's text is no longer inline-editable; every list links to its own page instead | ✅ done | +| 18 | Project view split into real routes — Explore (`/projects/:id`) and Kanban (`/projects/:id/kanban`) are separate pages under a shared layout, not client-side tab state | ✅ done | There is no password. Signing in is entering an email address and opening the magic link sent to it — the same step creates the account the first time. See diff --git a/web/README.md b/web/README.md index bab6c11..f9cf215 100644 --- a/web/README.md +++ b/web/README.md @@ -135,18 +135,27 @@ ghost. The same rule covers every kanban status column too (below). ## Project detail -`/projects/:id` shows one project. It renders on a **full-width** layout (the -route sets `meta.wide`, which widens `.app__main` in `App.vue`), so the header -spans the full width and the **Manage** menu sits top right. The title is a -plain heading, with an inline `.title-back` arrow to the dashboard right -before the text -- renaming lives on the configuration view (below). Manage -(`ProjectManageMenu.vue`) has a **Configure** link (to that view) and a -**Delete project** action that opens a confirmation modal; confirming calls -`DELETE /api/projects/:id` and returns to the dashboard. +`/projects/:id` shows one project. `ProjectView.vue` is a **layout**, not a +page of its own: it renders on a **full-width** layout (the parent route sets +`meta.wide`, inherited by its children, which widens `.app__main` in +`App.vue`), loads the project and its cards, and renders the header + a small +sub-nav — its two children (below) render into its ``. -Below the header are two tabs (local `activeTab` state, `v-show` so both stay -mounted). The tab order is fixed — **Explore** first, **Kanban** second — but -`activeTab` initialises to `'kanban'`, so a project opens on the board. +The header: a plain title heading, with an inline `.title-back` arrow to the +dashboard right before the text -- renaming lives on the configuration view +(below) -- and `ProjectManageMenu.vue` top right, with a **Configure** link +(to that view) and a **Delete project** action that opens a confirmation +modal; confirming calls `DELETE /api/projects/:id` and returns to the +dashboard. + +The sub-nav (`RouterLink`s styled as tabs, active one matched on `route.name`) +is real navigation, not client-side tab state -- **Explore** is the project's +own route (`/projects/:id`, name `project`), **Kanban** a child beneath it +(`/projects/:id/kanban`, name `project-kanban`). Both read the `cards` store +the layout already loaded; App.vue's top-level `` key is derived +from the matched route's *top-level* path plus params rather than the full +path, so switching between them doesn't remount the layout (and re-fetch the +project) the way switching to a different project's id still does. ### Explore @@ -160,8 +169,12 @@ sits outside that link. One column per project status, in `position` order -- the inbox is *not* a column here; it's in the sidebar (see above), though it's still a valid drag -source/target. `board` is derived from `cards.cards` + the project's statuses -and rebuilt by a `watch` whenever either changes. +source/target (the only view where that's true -- Explore has no draggable +list of its own, which the sidebar accounts for when deciding whether to +refresh a project's cards after an inbox drag). Unlike the cards, statuses +are this route's own fetch (`GET /api/projects/:id/statuses`) -- Explore has +no use for them. `board` is derived from `cards.cards` + those statuses and +rebuilt by a `watch` whenever either changes. Every drop — whether reordering within a column (`moved`) or dragging in from another column or the sidebar's inbox (`added`) — calls diff --git a/web/src/App.vue b/web/src/App.vue index 10dda13..6f3513d 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -27,6 +27,14 @@ const showSidebar = computed(() => auth.isAuthenticated && route.meta.requiresAu const drawerOpen = ref(false) watch(() => route.fullPath, () => (drawerOpen.value = false)) +// Keys the top-level RouterView by the current page and its params (id, +// mainly) rather than the full path -- so switching to a different project +// or card gets a fresh instance (a fresh load for its new :id), but a +// project's own Explore/Kanban routes -- children of the same top-level +// page, differing only in the trailing path segment -- share one, since +// ProjectView itself (their common layout) already handles that navigation. +const routeKey = computed(() => `${route.matched[0]?.path ?? route.path}:${JSON.stringify(route.params)}`) + async function onLogout() { auth.logout() projects.reset() @@ -70,9 +78,10 @@ async function onLogout() {
- - + +
diff --git a/web/src/components/AppSidebar.vue b/web/src/components/AppSidebar.vue index c9a7fc1..7b34ac8 100644 --- a/web/src/components/AppSidebar.vue +++ b/web/src/components/AppSidebar.vue @@ -37,10 +37,15 @@ async function loadProjects() { } } +// Explore and Kanban are both "viewing this project" for the switcher's +// purposes -- just its two different routes now, not a project-configure +// or card page also nested under /projects or /cards. +const ON_PROJECT_ROUTES = new Set(['project', 'project-kanban']) + // The dropdown doubles as a project switcher: it reflects whichever project // (if any) is currently open, and selecting one navigates there. const selectedProjectId = computed({ - get: () => (route.name === 'project' ? Number(route.params.id) : ''), + get: () => (ON_PROJECT_ROUTES.has(String(route.name)) ? Number(route.params.id) : ''), set: (id) => { if (id !== '') void router.push({ name: 'project', params: { id } }) }, @@ -84,8 +89,10 @@ async function onInboxChange(change: ColumnChange) { } catch (e) { inboxError.value = e instanceof ApiError ? e.message : 'Something went wrong.' } finally { - // The card may have come from (or gone to) the project currently open. - const openProjectId = route.name === 'project' ? Number(route.params.id) : null + // The card may have come from (or gone to) the kanban board currently + // open -- the only place a project's cards are a drag target/source + // alongside the inbox (Explore has no draggable list of its own). + const openProjectId = route.name === 'project-kanban' ? Number(route.params.id) : null await Promise.all([ loadInbox(), openProjectId !== null ? cards.load(openProjectId) : Promise.resolve(), diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 5e7d5a8..36c62c8 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -12,10 +12,16 @@ const router = createRouter({ meta: { requiresAuth: true, wide: true }, }, { + // ProjectView is a layout: header + tab nav, with Explore/Kanban as + // its own routes below (their meta -- requiresAuth/wide -- comes from + // this parent record, which Vue Router merges into theirs). path: '/projects/:id(\\d+)', - name: 'project', component: () => import('../views/ProjectView.vue'), meta: { requiresAuth: true, wide: true }, + children: [ + { path: '', name: 'project', component: () => import('../views/ProjectExploreView.vue') }, + { path: 'kanban', name: 'project-kanban', component: () => import('../views/ProjectKanbanView.vue') }, + ], }, { path: '/projects/:id(\\d+)/configure', diff --git a/web/src/style.css b/web/src/style.css index 7b4f144..d139b5c 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -725,7 +725,7 @@ h1 { color: var(--error); } -/* --- tabs -------------------------------------------------------------- */ +/* --- project sub-nav (Explore/Kanban routes) --------------------------- */ .tabs { display: flex; @@ -734,11 +734,15 @@ h1 { margin: 1.25rem 0 1rem; } +/* Real links (RouterLink) now, not buttons -- text-decoration/display reset + accordingly; everything else carries over unchanged. */ .tabs__tab { + display: inline-block; border: none; background: none; font: inherit; color: var(--muted); + text-decoration: none; cursor: pointer; padding: 0.5rem 0.9rem; border-bottom: 2px solid transparent; diff --git a/web/src/views/ProjectExploreView.vue b/web/src/views/ProjectExploreView.vue new file mode 100644 index 0000000..ea97e1c --- /dev/null +++ b/web/src/views/ProjectExploreView.vue @@ -0,0 +1,88 @@ + + + diff --git a/web/src/views/ProjectKanbanView.vue b/web/src/views/ProjectKanbanView.vue new file mode 100644 index 0000000..a7d2c17 --- /dev/null +++ b/web/src/views/ProjectKanbanView.vue @@ -0,0 +1,153 @@ + + + diff --git a/web/src/views/ProjectView.vue b/web/src/views/ProjectView.vue index 2ebdefc..8058d6a 100644 --- a/web/src/views/ProjectView.vue +++ b/web/src/views/ProjectView.vue @@ -1,107 +1,34 @@