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:
@@ -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>
|
||||
Reference in New Issue
Block a user