CI / pruefen (push) Canceled after 0s
Auf dem Telefon war die Projektseite doppelt so breit wie der Bildschirm: die Kopfleiste endete auf halber Strecke, "Jetzt synchronisieren" und der Notiz-Knopf lagen ausserhalb. Ursache sind die beiden Rasterspalten. Ein Rasterkind hat "min-width: auto" und weigert sich damit, unter die Inhaltsbreite seines breitesten Kindes zu schrumpfen. Breitestes Kind ist die Heatmap mit ueber 700px – sie zog die Spalte auf und mit ihr die ganze Seite. Das overflow-x-auto um sie herum kam gar nicht zum Zug, weil der Elter ihm die volle Breite zugestand. Mit min-w-0 an beiden Spalten greift es wieder: gemessen auf 390px liegen scrollWidth und clientWidth jetzt gleichauf, vorher 811 zu 390. Warum das beim Umbau der mobilen Ansicht nicht auffiel: die Messung lief gegen ein Projekt ohne Repository, ohne Snapshot und ohne README. Ohne Heatmap gibt es nichts, was die Spalte aufziehen koennte. Nachgestellt wurde das jetzt mit einem vollstaendigen Projekt – Snapshot, README mit Codeblock und Tabelle, Notizen, Aufgaben. Ein Regressionstest dafuer fehlt noch: die E2E-Tests legen Projekte ueber die API an und kommen ohne echten Abgleich zu keinem Snapshot, also auch zu keiner Heatmap. Ein Test ohne sie wuerde die Sicherheit vortaeuschen, ohne den Fall zu treffen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 lines
4.8 KiB
TypeScript
138 lines
4.8 KiB
TypeScript
import type { Metadata } from "next";
|
||
import { notFound } from "next/navigation";
|
||
import { FileText } from "lucide-react";
|
||
import { ActivityFeed } from "@/components/ActivityFeed";
|
||
import { GitPanel } from "@/components/GitPanel";
|
||
import { Markdown } from "@/components/Markdown";
|
||
import { NotesPanel } from "@/components/NotesPanel";
|
||
import { ReadmePanel } from "@/components/ReadmePanel";
|
||
import { ProjectHeader } from "@/components/ProjectHeader";
|
||
import { TaskBoard } from "@/components/tasks/TaskBoard";
|
||
import { requireUserPage } from "@/lib/auth/guard";
|
||
import { getSessionUser } from "@/lib/auth/session";
|
||
import { prisma } from "@/lib/db";
|
||
import { connectionSafeSelect, projectDetailInclude, webhookRepositoryIds } from "@/lib/projects";
|
||
import type { ConnectionSafe, ProjectDetail } from "@/types";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
type PageProps = { params: Promise<{ id: string }> };
|
||
|
||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||
const user = await getSessionUser();
|
||
if (!user) return { title: "Projekt" };
|
||
|
||
const { id } = await params;
|
||
const project = await prisma.project.findFirst({
|
||
where: { id, ownerId: user.id },
|
||
select: { name: true, summary: true },
|
||
});
|
||
return {
|
||
title: project?.name ?? "Projekt",
|
||
description: project?.summary ?? undefined,
|
||
};
|
||
}
|
||
|
||
export default async function ProjectPage({ params }: PageProps) {
|
||
const user = await requireUserPage();
|
||
const { id } = await params;
|
||
|
||
const [project, connections] = await Promise.all([
|
||
// findFirst mit Besitzerbedingung: ein fremdes Projekt fuehrt zu 404,
|
||
// nicht zu einer Fehlermeldung, die seine Existenz bestaetigt.
|
||
prisma.project.findFirst({
|
||
where: { id, ownerId: user.id },
|
||
include: projectDetailInclude,
|
||
}),
|
||
prisma.gitConnection.findMany({
|
||
where: { ownerId: user.id },
|
||
select: connectionSafeSelect,
|
||
orderBy: { name: "asc" },
|
||
}),
|
||
]);
|
||
|
||
if (!project) notFound();
|
||
|
||
const detail = project as unknown as ProjectDetail;
|
||
|
||
// Nur die Angabe "hier haengt ein Webhook", nicht das Geheimnis selbst.
|
||
const webhookIds = await webhookRepositoryIds(detail.id);
|
||
|
||
const fuehrend = detail.repositories[0]?.snapshot;
|
||
const readme = fuehrend?.readme
|
||
? {
|
||
content: fuehrend.readme,
|
||
path: fuehrend.readmePath,
|
||
baseUrl: fuehrend.readmeBaseUrl,
|
||
// Die Basis endet auf dem Verzeichnis; angehaengt ergibt das die Datei.
|
||
fileUrl:
|
||
fuehrend.readmeBaseUrl && fuehrend.readmePath
|
||
? `${fuehrend.readmeBaseUrl}${fuehrend.readmePath.split("/").pop()}`
|
||
: (fuehrend.htmlUrl ?? null),
|
||
}
|
||
: null;
|
||
|
||
return (
|
||
<div className="space-y-6 pt-4">
|
||
<ProjectHeader
|
||
project={detail}
|
||
connections={connections as unknown as ConnectionSafe[]}
|
||
/>
|
||
|
||
<TaskBoard
|
||
projectId={detail.id}
|
||
tasks={detail.tasks}
|
||
progressFromTasks={detail.progressFromTasks}
|
||
/>
|
||
|
||
{/*
|
||
min-w-0 an beiden Spalten ist hier nicht Kosmetik, sondern der Kern:
|
||
ein Rasterkind hat "min-width: auto" und weigert sich damit, unter
|
||
die Inhaltsbreite seines breitesten Kindes zu schrumpfen. Die
|
||
Heatmap ist ueber 700px breit – auf dem Telefon zog sie damit die
|
||
ganze Spalte und mit ihr die Seite auseinander. Erst mit min-w-0
|
||
greift das overflow-x-auto darin ueberhaupt.
|
||
*/}
|
||
<div className="grid gap-6 lg:grid-cols-12">
|
||
<div className="min-w-0 space-y-6 lg:col-span-5">
|
||
<section className="glass p-5">
|
||
<h2 className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-slate-300">
|
||
<FileText className="h-4 w-4 text-cyan-300" />
|
||
Beschreibung
|
||
</h2>
|
||
{detail.description ? (
|
||
<Markdown className="mt-3">{detail.description}</Markdown>
|
||
) : (
|
||
<p className="mt-3 text-sm text-slate-500">
|
||
Keine Beschreibung hinterlegt. Ueber „Bearbeiten“ kannst du die Idee ausformulieren.
|
||
</p>
|
||
)}
|
||
</section>
|
||
|
||
{/* Das README des fuehrenden Repositories – bei mehreren waere eine
|
||
zweite Vorschau untereinander mehr Laenge als Nutzen. */}
|
||
{readme ? (
|
||
<ReadmePanel
|
||
content={readme.content}
|
||
path={readme.path}
|
||
baseUrl={readme.baseUrl}
|
||
fileUrl={readme.fileUrl}
|
||
/>
|
||
) : null}
|
||
|
||
<NotesPanel projectId={detail.id} notes={detail.notes} />
|
||
</div>
|
||
|
||
<div className="min-w-0 space-y-6 lg:col-span-7">
|
||
<GitPanel
|
||
project={detail}
|
||
connections={connections as unknown as ConnectionSafe[]}
|
||
webhookRepoIds={webhookIds}
|
||
/>
|
||
<ActivityFeed projectId={detail.id} initial={detail.activities} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|