Add a persistent left sidebar to the signed-in layout

App.vue now renders a left AppSidebar beside the routed view for any
requiresAuth page, staying mounted as you move between the dashboard and
projects. The sidebar has a Dashboard link (icon), a divider, the project list
(each an icon link, current page highlighted via RouterLink active-class), and a
compact new-project form that jumps to the created project.

- New /dashboard route + DashboardView ("under construction"); / and unknown
  paths redirect there. HomeView removed -- its project list and form moved into
  the sidebar.
- <RouterView :key="route.path"> so navigating project -> project via the
  sidebar remounts and reloads instead of reusing the instance.
- Signed-out routes (login/register/verify-email) render without the sidebar.

Icons are inline SVG -- no new dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 13:55:31 +01:00
co-authored by Claude Sonnet 5
parent 9c1768ddb2
commit cd54b41ab7
8 changed files with 303 additions and 149 deletions
+22
View File
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
</script>
<template>
<section class="card">
<h1>Dashboard</h1>
<div v-if="!auth.emailVerified" class="notice">
Your email address <strong>{{ auth.user?.email }}</strong> has not been
verified yet.
</div>
<div class="under-construction">
<span class="under-construction__icon" aria-hidden="true">🚧</span>
<p>This page is under construction.</p>
<p class="muted">Pick a project from the sidebar to get started.</p>
</div>
</section>
</template>
-100
View File
@@ -1,100 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { ApiError } from '../lib/api'
import { useAuthStore } from '../stores/auth'
import { MAX_PROJECTS, useProjectsStore } from '../stores/projects'
const auth = useAuthStore()
const projects = useProjectsStore()
const loadError = ref<string | null>(null)
const title = ref('')
const createError = ref<ApiError | null>(null)
const submitting = ref(false)
const atLimit = computed(() => projects.projects.length >= MAX_PROJECTS)
const summary = computed(() => {
const n = projects.projects.length
return `You have ${n} ${n === 1 ? 'project' : 'projects'}.`
})
onMounted(load)
async function load() {
loadError.value = null
try {
await projects.fetchProjects()
} catch (e) {
loadError.value = e instanceof ApiError ? e.message : 'Could not load your projects.'
}
}
async function onCreate() {
submitting.value = true
createError.value = null
try {
await projects.createProject(title.value)
title.value = ''
} catch (e) {
createError.value = e instanceof ApiError ? e : new ApiError('Could not create the project.', 0)
} finally {
submitting.value = false
}
}
</script>
<template>
<section class="card">
<h1>Your projects</h1>
<div v-if="!auth.emailVerified" class="notice">
Your email address <strong>{{ auth.user?.email }}</strong> has not been
verified yet.
</div>
<p v-if="loadError" class="form-error">{{ loadError }}</p>
<p v-else-if="projects.loading && !projects.loaded" class="muted">Loading</p>
<template v-else>
<p class="muted">{{ summary }}</p>
<p v-if="projects.projects.length === 0" class="muted">
No projects yet create your first one below.
</p>
<ul v-else class="projects">
<li v-for="project in projects.projects" :key="project.id" class="projects__item">
<RouterLink :to="{ name: 'project', params: { id: project.id } }" class="projects__link">
<div class="projects__head">
<span class="projects__title">{{ project.title }}</span>
<span class="badge">{{ project.completed_count }} / {{ project.card_count }} done</span>
</div>
</RouterLink>
</li>
</ul>
<form class="form form--new-project" @submit.prevent="onCreate">
<label>
<span>New project title</span>
<input v-model="title" type="text" maxlength="255" required :disabled="atLimit" />
<small v-if="createError?.fieldError('title')" class="field-error">
{{ createError.fieldError('title') }}
</small>
</label>
<p v-if="createError && Object.keys(createError.details).length === 0" class="form-error">
{{ createError.message }}
</p>
<button type="submit" :disabled="submitting || atLimit">
{{ submitting ? 'Creating…' : 'Create project' }}
</button>
<p v-if="atLimit" class="hint">
You have reached the maximum of {{ MAX_PROJECTS }} projects.
</p>
</form>
</template>
</section>
</template>