CI / pruefen (pull_request) Canceled after 0s
Eine Seite, die aus dem vorhandenen Aktivitaetsverlauf zusammenfasst, was in
einer Woche ueber alle Projekte hinweg passiert ist: Kennzahlen oben, darunter
die Vorgaenge nach Kalendertagen gebuendelt, neuester Tag zuerst. Blaettern in
frueher Wochen ueber ?woche=-n.
Der Verlauf kennt keinen Besitzer - die Trennung laeuft ueber das Projekt,
"project: { ownerId }" steht als erste Bedingung.
Zwei Dinge, die sonst still falsch werden:
- **Wochengrenzen** stehen als pruefbare Funktion in lib/review, nicht in der
Seite. Gerechnet wird ueber Berliner Kalendertage, nicht ueber "sieben mal
24 Stunden": liegt eine Zeitumstellung in der Woche, hat sie 167 oder 169
Stunden. Und im Container laeuft die Anwendung nach UTC, ein Eintrag von
Sonntagabend fiele sonst in die falsche Woche.
- **"Aufgaben erledigt"** wird am meta-Feld abgelesen (to: "DONE") und nicht
am Anzeigetext. Der Text ist zum Lesen da und aendert sich mit der
Formulierung; eine Zaehlung daran wuerde beim naechsten umformulierten
Einzeiler unbemerkt kippen. Eintraege ohne meta zaehlen nicht mit - lieber
untertreiben als raten.
Damit completeTaskFromIssue dieselbe Grundlage liefert, schreibt es jetzt
ebenfalls from/to ins meta, wie es das Verschieben von Hand schon tut.
Die Zuordnung Symbol/Farbe je Eintragsart lag bisher in ActivityFeed und wird
jetzt von zwei Stellen gebraucht - herausgezogen nach components/activityKinds,
sonst laufen die Kopien beim naechsten neuen Eintragstyp auseinander.
Erreichbar ueber die Schnellsuche und einen Verweis vom Dashboard. Bewusst
kein vierter Punkt in der Kopfleiste - die ist auf dem Telefon schon eng.
Geprueft: 20 neue Unit-Tests (Wochengrenzen inklusive beider Zeitumstellungen,
Jahreswechsel, Zaehlung), ein E2E-Test ueber Aufruf und Blaettern, axe-core auf
375px und 1280px ueber drei Zustaende - auch die leere Woche - ohne
Beanstandung, kein seitlicher Ueberlauf.
Refs #2
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
import Link from "next/link";
|
||
import { CalendarRange, CircleDot, GitBranch, Lightbulb, Rocket, Trophy } from "lucide-react";
|
||
import { ProjectBoard } from "@/components/ProjectBoard";
|
||
import { StatCard } from "@/components/StatCard";
|
||
import { requireUserPage } from "@/lib/auth/guard";
|
||
import { prisma } from "@/lib/db";
|
||
import { connectionSafeSelect, projectListInclude } from "@/lib/projects";
|
||
import { STATUS_META } from "@/lib/status";
|
||
import type { ConnectionSafe, ProjectListItem } from "@/types";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
export default async function DashboardPage() {
|
||
const user = await requireUserPage();
|
||
|
||
const [projects, connections] = await Promise.all([
|
||
prisma.project.findMany({
|
||
where: { ownerId: user.id },
|
||
include: projectListInclude,
|
||
orderBy: [{ favorite: "desc" }, { updatedAt: "desc" }],
|
||
}),
|
||
prisma.gitConnection.findMany({
|
||
where: { ownerId: user.id },
|
||
select: connectionSafeSelect,
|
||
orderBy: { name: "asc" },
|
||
}),
|
||
]);
|
||
|
||
const active = projects.filter((project) => project.status !== "ARCHIVED");
|
||
const ideas = active.filter((project) => project.status === "IDEA").length;
|
||
const inDev = active.filter((project) => project.status === "IN_DEVELOPMENT").length;
|
||
const done = active.filter((project) => project.status === "DONE").length;
|
||
const linked = active.filter((project) => project.repositories.length > 0).length;
|
||
|
||
const averageProgress =
|
||
active.length > 0
|
||
? Math.round(active.reduce((sum, project) => sum + project.progress, 0) / active.length)
|
||
: 0;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<header className="fade-up pt-4">
|
||
<p className="text-[11px] font-semibold uppercase tracking-[0.3em] text-violet-300/70">
|
||
Vibe Coding Kontrollzentrum
|
||
</p>
|
||
<h1 className="mt-2 text-3xl font-semibold tracking-tight text-white sm:text-4xl">
|
||
Deine Projekte auf{" "}
|
||
<span className="text-gradient">einen Blick</span>
|
||
</h1>
|
||
<p className="mt-2 max-w-2xl text-sm text-slate-400">
|
||
Ideen festhalten, Status pflegen, Notizen sammeln – und live sehen, was in den
|
||
verknuepften Repositories bei GitHub, GitLab und Gitea passiert.
|
||
</p>
|
||
{/* Kein weiterer Punkt in der Kopfleiste – die ist auf dem Telefon
|
||
schon eng. Der Rueckblick haengt dort, wo man ohnehin hinsieht. */}
|
||
<Link
|
||
href="/rueckblick"
|
||
className="mt-3 inline-flex items-center gap-1.5 text-sm text-violet-300 underline-offset-2 hover:underline"
|
||
>
|
||
<CalendarRange className="h-4 w-4" />
|
||
Wochenrückblick ansehen
|
||
</Link>
|
||
</header>
|
||
|
||
{/* Zwei Spalten schon auf dem Telefon: einspaltig fuellten die fuenf
|
||
Kacheln dort den ganzen ersten Bildschirm, und die Projekte – der
|
||
eigentliche Inhalt – begannen erst nach dem zweiten Wischen. */}
|
||
<div className="grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-5">
|
||
<StatCard
|
||
label="Projekte"
|
||
value={active.length}
|
||
hint={`Ø Fortschritt ${averageProgress}%`}
|
||
icon={CircleDot}
|
||
color="#a78bfa"
|
||
className="col-span-2 lg:col-span-1"
|
||
/>
|
||
<StatCard
|
||
label="Ideen"
|
||
value={ideas}
|
||
// Kurzer Hinweis statt der langen Statusbeschreibung – die wurde in
|
||
// der schmalen Kachel mitten im Wort abgeschnitten.
|
||
hint="Noch nichts gebaut"
|
||
icon={Lightbulb}
|
||
color={STATUS_META.IDEA.color}
|
||
/>
|
||
<StatCard
|
||
label="In Entwicklung"
|
||
value={inDev}
|
||
hint="Aktiv in Arbeit"
|
||
icon={Rocket}
|
||
color={STATUS_META.IN_DEVELOPMENT.color}
|
||
/>
|
||
<StatCard
|
||
label="Fertig"
|
||
value={done}
|
||
hint="Ausgeliefert"
|
||
icon={Trophy}
|
||
color={STATUS_META.DONE.color}
|
||
/>
|
||
<StatCard
|
||
label="Repos verknuepft"
|
||
value={linked}
|
||
hint={`${connections.length} Git-Verbindung${connections.length === 1 ? "" : "en"}`}
|
||
icon={GitBranch}
|
||
color="#38bdf8"
|
||
/>
|
||
</div>
|
||
|
||
<ProjectBoard
|
||
projects={projects as unknown as ProjectListItem[]}
|
||
connections={connections as unknown as ConnectionSafe[]}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|