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