Projekte-Abschnitt zurück auf die alte Darstellung

Die Live-Kachelansicht aller Gitea-Repositories ist nicht gewollt: der
Abschnitt "Projekte" zeigt wieder die kuratierte Liste wie zuvor.

Dynamisch bleibt allein der Projektzähler im Hero – lib/projects.ts ist
deshalb auf fetchProjects + Snapshot-Typ eingedampft, ProjectCard.tsx
entfällt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-08-06 21:14:11 +00:00
1 parent 60c827733f
commit d12fa7d04f
3 files changed
+98 -496

No files matched your search

+11 -92
View File
@@ -1,109 +1,28 @@
/**
* Live-Projektdaten für den Abschnitt „Projekte".
* Live-Projektzahl für die Statistik-Zeile im Hero.
*
* Quelle: GET /api/projects (contact-api-Container) – führt serverseitig alle
* Gitea-Repositories mit den Hosting-Metadaten aus Appwrite zusammen.
* Im Browser liegen dadurch weder Gitea-Token noch Appwrite-Key.
* Quelle: GET /api/projects (contact-api-Container) – zählt serverseitig die
* Repositories, die aktuell in Gitea (git.webklar.com) liegen, angereichert um
* die Hosting-Metadaten aus Appwrite. Der Gitea-Token bleibt dadurch auf dem
* Server. Die Antwort enthält mehr Felder als hier genutzt werden; für die
* Homepage ist nur `total` relevant.
*/
export type ProjectCategory = "website" | "preview" | "template" | "app" | "internal" | "test";
/** live = öffentlich + einbettbar, online = öffentlich, protected = hinter Portal-Login */
export type ProjectState = "live" | "online" | "protected" | "hosted" | "repo" | "archived";
export type LiveProject = {
id: string;
name: string;
repo: string;
owner: string;
description: string;
category: ProjectCategory;
state: ProjectState;
url: string | null;
repoUrl: string;
language: string | null;
sizeKb: number;
updatedAt: string | null;
archived: boolean;
private: boolean;
hosted: boolean;
hostingStatus: string | null;
subdomain: string | null;
embeddable: boolean;
reachable: boolean;
gated: boolean;
lastCommit: { message: string; author: string | null; date: string | null } | null;
};
export type ProjectsSnapshot = {
/** Zeitpunkt, zu dem der Server-Snapshot erzeugt wurde (ISO) */
generatedAt: string;
/** Anzahl der Repositories, die aktuell in Gitea existieren */
total: number;
counts: Partial<Record<ProjectCategory, number>>;
embeddable: number;
counts: Record<string, number>;
source: string;
projects: LiveProject[];
};
const API_URL = import.meta.env.VITE_PROJECTS_API ?? "/api/projects";
export async function fetchProjects(signal?: AbortSignal): Promise<ProjectsSnapshot> {
const res = await fetch(API_URL, { signal, headers: { Accept: "application/json" } });
if (!res.ok) throw new Error(`Projekte konnten nicht geladen werden (HTTP ${res.status})`);
if (!res.ok) throw new Error(`Projektzahl konnte nicht geladen werden (HTTP ${res.status})`);
const data = (await res.json()) as ProjectsSnapshot;
if (!Array.isArray(data?.projects)) throw new Error("Unerwartete Antwort der Projekt-API");
if (typeof data?.total !== "number") throw new Error("Unerwartete Antwort der Projekt-API");
return data;
}
export const CATEGORY_LABELS: Record<ProjectCategory, string> = {
website: "Kundenprojekte",
preview: "Kunden-Previews",
template: "Vorlagen",
app: "Apps & Tools",
internal: "Eigene Systeme",
test: "Tests",
};
/** Reihenfolge der Filter-Chips */
export const CATEGORY_ORDER: ProjectCategory[] = [
"website",
"preview",
"app",
"template",
"internal",
"test",
];
export const STATE_LABELS: Record<ProjectState, string> = {
live: "Live-Vorschau",
online: "Öffentlich online",
protected: "Login geschützt",
hosted: "Gehostet",
repo: "Nur Repository",
archived: "Archiviert",
};
/**
* Deterministische Akzentfarbe aus dem Repo-Namen – gleiche Kachel behält beim
* Neuladen ihre Farbe, ohne dass wir Farben pflegen müssen.
*/
export function accentFor(seed: string): { hue: number; from: string; to: string } {
let hash = 0;
for (let i = 0; i < seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) >>> 0;
const hue = hash % 360;
return {
hue,
from: `hsl(${hue} 70% 55% / 0.55)`,
to: `hsl(${(hue + 48) % 360} 70% 45% / 0.18)`,
};
}
/** Kürzel für die Platzhalter-Kachel (max. 2 Zeichen) */
export function monogram(name: string): string {
const words = name
.replace(/[^\p{L}\p{N}\s-]/gu, " ")
.split(/[\s-]+/)
.filter(Boolean);
if (words.length === 0) return "··";
if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
return (words[0][0] + words[1][0]).toUpperCase();
}