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:
1 parent
60c827733f
commit
d12fa7d04f
3 files changed
+98
-496
No files matched your search
+11
-92
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user