diff --git a/README.md b/README.md index fa244b0..7584819 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Each user owns **projects**, and each project holds ordered **cards**. | 8 | Passwordless login — magic-link by default, password login behind a toggle | ✅ done | | 9 | Per-project card statuses ("To do" / "Doing" / "Done"); status chip, new cards start with none | ✅ done | | 10 | Project view — full-width, tabbed: alphabetical "All tasks" list + "Kanban" board, per-column drag ordering | ✅ done | +| 11 | Persistent left sidebar — Dashboard link + project list/new-project form; `/` → dashboard placeholder | ✅ done | Registration signs the user in immediately and emails a magic link that verifies the address; `user.email_verified` stays `false` until the link is opened. See diff --git a/web/README.md b/web/README.md index 138e334..986667a 100644 --- a/web/README.md +++ b/web/README.md @@ -35,12 +35,27 @@ src/stores/auth.ts Pinia store: token in localStorage, register/login/fetch src/stores/projects.ts Pinia store: the user's projects (fetch + create) src/stores/cards.ts Pinia store: one project's cards (CRUD + reorderColumn) src/lib/api.ts fetch wrapper, bearer token, typed ApiError +src/components/AppSidebar.vue left nav: Dashboard link, divider, project list + new-project form src/components/CardRow.vue editable text + status chip + delete, one card src/components/KanbanCard.vue small draggable card for the board columns -src/views/ HomeView, ProjectView, LoginView, RegisterView, +src/views/ DashboardView, ProjectView, LoginView, RegisterView, ProfileView, VerifyEmailView ``` +## Layout + +Signed-in "app" routes (`meta.requiresAuth`) render inside a persistent shell: +the top bar, then a left **sidebar** (`AppSidebar.vue`) beside the routed view. +The sidebar stays mounted across navigation — it holds a **Dashboard** link, a +divider, the project list, and a compact new-project form (creating one jumps to +it). The current page is highlighted via RouterLink's `active-class`. +`` remounts the view on every path change so +sidebar → project → project navigation always does a fresh load. + +`/` redirects to `/dashboard` (`DashboardView.vue`, an "under construction" +placeholder). Signed-out routes (`/login`, `/register`, `/verify-email`) render +without the sidebar. + ## Project detail `/projects/:id` shows one project. It renders on a **full-width** layout (the @@ -48,7 +63,7 @@ route sets `meta.wide`, which widens `.app__main` in `App.vue`). The title and description are inline-editable (saved on blur via `PATCH /api/projects/:id`; the description shows an "Add a description" placeholder when empty). A **Manage** menu (top right) has a **Delete project** action that opens a confirmation modal; -confirming calls `DELETE /api/projects/:id` and returns to the all-projects view. +confirming calls `DELETE /api/projects/:id` and returns to the dashboard. Below the header are two tabs (local `activeTab` state, `v-show` so both stay mounted): diff --git a/web/src/App.vue b/web/src/App.vue index e4d8b2a..9973a31 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,5 +1,7 @@ + + diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 22f4d76..0cfd0a4 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -4,10 +4,11 @@ import { useAuthStore } from '../stores/auth' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ + { path: '/', redirect: { name: 'dashboard' } }, { - path: '/', - name: 'home', - component: () => import('../views/HomeView.vue'), + path: '/dashboard', + name: 'dashboard', + component: () => import('../views/DashboardView.vue'), meta: { requiresAuth: true }, }, { @@ -40,22 +41,24 @@ const router = createRouter({ component: () => import('../views/RegisterView.vue'), meta: { guestOnly: true }, }, - { path: '/:pathMatch(.*)*', redirect: { name: 'home' } }, + { path: '/:pathMatch(.*)*', redirect: { name: 'dashboard' } }, ], }) +const DEFAULT_PATHS = new Set(['/', '/dashboard']) + router.beforeEach((to) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.isAuthenticated) { return { name: 'login', - query: to.fullPath === '/' ? {} : { redirect: to.fullPath }, + query: DEFAULT_PATHS.has(to.path) ? {} : { redirect: to.fullPath }, } } if (to.meta.guestOnly && auth.isAuthenticated) { - return { name: 'home' } + return { name: 'dashboard' } } }) diff --git a/web/src/style.css b/web/src/style.css index ee70493..b7ba44b 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -72,7 +72,14 @@ a.badge { text-decoration: none; } +.app__body { + display: flex; + align-items: flex-start; +} + .app__main { + flex: 1; + min-width: 0; max-width: 32rem; margin: 2.5rem auto; padding: 0 1.25rem; @@ -84,6 +91,112 @@ a.badge { margin: 1.5rem auto; } +/* --- left sidebar (signed-in app pages) ------------------------------ */ + +.sidebar { + flex: 0 0 15rem; + align-self: stretch; + position: sticky; + top: 0; + max-height: 100vh; + overflow-y: auto; + padding: 1rem 0.75rem 1.5rem; + border-right: 1px solid var(--border); + background: var(--surface); +} + +.sidebar__nav { + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +.sidebar__link { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.45rem 0.6rem; + border-radius: 7px; + color: var(--text); + text-decoration: none; + font-size: 0.9rem; +} + +.sidebar__link:hover { + background: var(--bg); +} + +.sidebar__link--active, +.sidebar__link--active:hover { + background: var(--accent); + color: var(--accent-text); +} + +.sidebar__icon { + flex: none; + width: 1rem; + height: 1rem; + fill: currentColor; +} + +.sidebar__link-text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sidebar__divider { + border: none; + border-top: 1px solid var(--border); + margin: 0.75rem 0; +} + +.sidebar__heading { + margin: 0 0 0.35rem; + padding: 0 0.6rem; + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--muted); +} + +.sidebar__projects { + max-height: 45vh; + overflow-y: auto; +} + +.sidebar__note { + padding: 0 0.6rem; + font-size: 0.85rem; +} + +.sidebar__form { + display: flex; + flex-direction: column; + gap: 0.4rem; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border); +} + +.sidebar__form input { + font: inherit; + font-size: 0.9rem; + padding: 0.4rem 0.55rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); +} + +.sidebar__form button[type='submit'] { + padding: 0.45rem 0.7rem; + font-size: 0.9rem; + border-radius: 6px; +} + .card { background: var(--surface); border: 1px solid var(--border); @@ -110,6 +223,23 @@ h1 { font-size: 0.9rem; } +.under-construction { + margin-top: 1.5rem; + padding: 2.5rem 1rem; + text-align: center; + border: 1px dashed var(--border); + border-radius: 10px; +} + +.under-construction__icon { + font-size: 2rem; + line-height: 1; +} + +.under-construction p { + margin: 0.5rem 0 0; +} + .badge { padding: 0.1rem 0.45rem; border-radius: 999px; @@ -126,43 +256,6 @@ h1 { color: inherit; } -.projects { - list-style: none; - margin: 1.5rem 0 0; - padding: 0; - display: grid; - gap: 0.75rem; -} - -.projects__item { - border: 1px solid var(--border); - border-radius: 8px; -} - -.projects__link { - display: block; - padding: 0.75rem 0.9rem; - color: inherit; - text-decoration: none; - border-radius: 8px; -} - -.projects__link:hover { - background: var(--bg); -} - -.projects__head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; -} - -.projects__title { - font-weight: 600; - word-break: break-word; -} - /* --- project detail: cards ------------------------------------------ */ .cards { @@ -546,7 +639,6 @@ h1 { margin: 1.25rem 0; } -.form--new-project, .form--new-card { margin-top: 1.5rem; padding-top: 1.25rem; diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue new file mode 100644 index 0000000..8097d99 --- /dev/null +++ b/web/src/views/DashboardView.vue @@ -0,0 +1,22 @@ + + + diff --git a/web/src/views/HomeView.vue b/web/src/views/HomeView.vue deleted file mode 100644 index 69f8fb0..0000000 --- a/web/src/views/HomeView.vue +++ /dev/null @@ -1,100 +0,0 @@ - - -