Make the dashboard a grid of projects with their inbox

DashboardView drops the placeholder for a full-width grid of project cards.
Each card links to the project and lists its inbox cards (status_id === null)
under a "New" heading, or "Nothing new." when empty. Cards are fetched per
project (one GET /projects/{id}/cards each) after the project list resolves.

projects store: fetchProjects() now shares one in-flight request between
concurrent callers (the sidebar and the dashboard mount together).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 14:02:47 +01:00
co-authored by Claude Sonnet 5
parent cd54b41ab7
commit 8f8ad8593d
6 changed files with 139 additions and 31 deletions
+58 -8
View File
@@ -1,22 +1,72 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { ApiError, apiRequest } from '../lib/api'
import { useAuthStore } from '../stores/auth'
import { useProjectsStore } from '../stores/projects'
import type { Card } from '../types'
const auth = useAuthStore()
const projects = useProjectsStore()
const loading = ref(true)
const loadError = ref<string | null>(null)
/** project id -> its inbox cards (status_id === null) */
const inbox = ref<Record<number, Card[]>>({})
onMounted(load)
async function load() {
loading.value = true
loadError.value = null
try {
await projects.fetchProjects()
const entries = await Promise.all(
projects.projects.map(async (project): Promise<[number, Card[]]> => {
const { cards } = await apiRequest<{ cards: Card[] }>(`/projects/${project.id}/cards`, {
auth: true,
})
return [project.id, cards.filter((card) => card.status_id === null)]
}),
)
inbox.value = Object.fromEntries(entries)
} catch (e) {
loadError.value = e instanceof ApiError ? e.message : 'Could not load the dashboard.'
} finally {
loading.value = false
}
}
</script>
<template>
<section class="card">
<h1>Dashboard</h1>
<div class="dashboard">
<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>
<p v-if="loadError" class="form-error">{{ loadError }}</p>
<p v-else-if="loading" class="muted">Loading</p>
<p v-else-if="projects.projects.length === 0" class="muted">
No projects yet create one from the sidebar.
</p>
<div v-else class="dashboard__grid">
<article v-for="project in projects.projects" :key="project.id" class="project-card">
<RouterLink
:to="{ name: 'project', params: { id: project.id } }"
class="project-card__title"
>
{{ project.title }}
</RouterLink>
<p class="project-card__heading">New</p>
<ul v-if="inbox[project.id]?.length" class="project-card__cards">
<li v-for="card in inbox[project.id]" :key="card.id">{{ card.text }}</li>
</ul>
<p v-else class="project-card__empty muted">Nothing new.</p>
</article>
</div>
</section>
</div>
</template>