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
+109
View File
@@ -0,0 +1,109 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { ApiError } from '../lib/api'
import { MAX_PROJECTS, useProjectsStore } from '../stores/projects'
const projects = useProjectsStore()
const router = useRouter()
const title = ref('')
const createError = ref<ApiError | null>(null)
const submitting = ref(false)
const loadError = ref<string | null>(null)
const atLimit = computed(() => projects.projects.length >= MAX_PROJECTS)
onMounted(() => {
if (!projects.loaded) void 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 {
const project = await projects.createProject(title.value)
title.value = ''
await router.push({ name: 'project', params: { id: project.id } })
} catch (e) {
createError.value = e instanceof ApiError ? e : new ApiError('Could not create the project.', 0)
} finally {
submitting.value = false
}
}
</script>
<template>
<aside class="sidebar">
<nav class="sidebar__nav">
<RouterLink :to="{ name: 'dashboard' }" class="sidebar__link" active-class="sidebar__link--active">
<svg class="sidebar__icon" viewBox="0 0 16 16" aria-hidden="true">
<rect x="1" y="1" width="6" height="6" rx="1.2" />
<rect x="9" y="1" width="6" height="6" rx="1.2" />
<rect x="1" y="9" width="6" height="6" rx="1.2" />
<rect x="9" y="9" width="6" height="6" rx="1.2" />
</svg>
<span>Dashboard</span>
</RouterLink>
</nav>
<hr class="sidebar__divider" />
<p class="sidebar__heading">Projects</p>
<p v-if="loadError" class="sidebar__note form-error">{{ loadError }}</p>
<p v-else-if="projects.loading && !projects.loaded" class="sidebar__note muted">Loading</p>
<p v-else-if="projects.projects.length === 0" class="sidebar__note muted">No projects yet.</p>
<nav v-else class="sidebar__nav sidebar__projects">
<RouterLink
v-for="project in projects.projects"
:key="project.id"
:to="{ name: 'project', params: { id: project.id } }"
class="sidebar__link sidebar__link--project"
active-class="sidebar__link--active"
>
<svg class="sidebar__icon" viewBox="0 0 16 16" aria-hidden="true">
<rect x="1" y="2" width="3.5" height="12" rx="1" />
<rect x="6.25" y="2" width="3.5" height="9" rx="1" />
<rect x="11.5" y="2" width="3.5" height="6" rx="1" />
</svg>
<span class="sidebar__link-text">{{ project.title }}</span>
</RouterLink>
</nav>
<form class="sidebar__form" @submit.prevent="onCreate">
<input
v-model="title"
type="text"
maxlength="255"
required
:disabled="atLimit"
placeholder="New project"
aria-label="New project title"
/>
<small v-if="createError?.fieldError('title')" class="field-error">
{{ createError.fieldError('title') }}
</small>
<small
v-else-if="createError && Object.keys(createError.details).length === 0"
class="field-error"
>
{{ createError.message }}
</small>
<button type="submit" :disabled="submitting || atLimit">
{{ submitting ? 'Creating…' : 'Add project' }}
</button>
<small v-if="atLimit" class="hint">Limit of {{ MAX_PROJECTS }} projects reached.</small>
</form>
</aside>
</template>