From d12fa7d04f2a6204ba3602d371a81a4b40af6c58 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 6 Aug 2026 21:14:11 +0000 Subject: [PATCH] =?UTF-8?q?Projekte-Abschnitt=20zur=C3=BCck=20auf=20die=20?= =?UTF-8?q?alte=20Darstellung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/components/ProjectCard.tsx | 203 -------------------- src/components/ProjectShowcase.tsx | 288 +++++++++-------------------- src/lib/projects.ts | 103 ++--------- 3 files changed, 98 insertions(+), 496 deletions(-) delete mode 100644 src/components/ProjectCard.tsx diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx deleted file mode 100644 index aed9431..0000000 --- a/src/components/ProjectCard.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { ArrowUpRight, GitBranch, Lock, Archive, Globe } from "lucide-react"; -import { formatDistanceToNow } from "date-fns"; -import { de } from "date-fns/locale"; -import BorderGlow from "@/components/BorderGlow"; -import { - accentFor, - monogram, - STATE_LABELS, - CATEGORY_LABELS, - type LiveProject, -} from "@/lib/projects"; - -/** Breite, mit der die eingebettete Seite gerendert und dann herunterskaliert wird */ -const DESIGN_WIDTH = 1280; -const DESIGN_HEIGHT = 800; - -/** Iframe erst laden, wenn die Karte in Sichtweite kommt – 100 Karten sollen den Browser nicht killen. */ -function useInViewport(rootMargin = "300px") { - const ref = useRef(null); - const [inView, setInView] = useState(false); - - useEffect(() => { - const el = ref.current; - if (!el || inView) return; - if (typeof IntersectionObserver === "undefined") { - setInView(true); - return; - } - const observer = new IntersectionObserver( - (entries) => { - if (entries.some((e) => e.isIntersecting)) { - setInView(true); - observer.disconnect(); - } - }, - { rootMargin } - ); - observer.observe(el); - return () => observer.disconnect(); - }, [inView, rootMargin]); - - return { ref, inView }; -} - -/** Skalierungsfaktor, damit die 1280px-Seite exakt in die Kachel passt */ -function useScaleToWidth(designWidth: number) { - const ref = useRef(null); - const [scale, setScale] = useState(0.25); - - useEffect(() => { - const el = ref.current; - if (!el) return; - const update = () => setScale(el.clientWidth / designWidth); - update(); - if (typeof ResizeObserver === "undefined") { - window.addEventListener("resize", update); - return () => window.removeEventListener("resize", update); - } - const observer = new ResizeObserver(update); - observer.observe(el); - return () => observer.disconnect(); - }, [designWidth]); - - return { ref, scale }; -} - -function relativeDate(iso: string | null): string | null { - if (!iso) return null; - const date = new Date(iso); - if (Number.isNaN(date.getTime())) return null; - return formatDistanceToNow(date, { addSuffix: true, locale: de }); -} - -const STATE_STYLES: Record = { - live: { dot: "bg-emerald-400", text: "text-emerald-400", Icon: Globe }, - online: { dot: "bg-emerald-400", text: "text-emerald-400", Icon: Globe }, - protected: { dot: "bg-amber-400", text: "text-amber-400", Icon: Lock }, - hosted: { dot: "bg-sky-400", text: "text-sky-400", Icon: Globe }, - repo: { dot: "bg-muted-foreground", text: "text-muted-foreground", Icon: GitBranch }, - archived: { dot: "bg-muted-foreground/60", text: "text-muted-foreground", Icon: Archive }, -}; - -type Props = { project: LiveProject; cardBg: string }; - -const ProjectCard = ({ project, cardBg }: Props) => { - const { ref: viewRef, inView } = useInViewport(); - const { ref: frameRef, scale } = useScaleToWidth(DESIGN_WIDTH); - const [frameFailed, setFrameFailed] = useState(false); - - const accent = accentFor(project.repo); - const state = STATE_STYLES[project.state]; - const showFrame = project.embeddable && project.url && inView && !frameFailed; - const updated = relativeDate(project.lastCommit?.date ?? project.updatedAt); - const linkUrl = project.reachable && project.url ? project.url : project.repoUrl; - - return ( - - - {/* Vorschau: echtes Live-Iframe, sonst Live-Daten-Kachel */} -
- {showFrame ? ( -