diff --git a/app/_ui/ProjectDetailClient.tsx b/app/_ui/ProjectDetailClient.tsx index d642379..ba35c97 100644 --- a/app/_ui/ProjectDetailClient.tsx +++ b/app/_ui/ProjectDetailClient.tsx @@ -6,6 +6,7 @@ import ReactMarkdown from "react-markdown"; import { useTranslations } from "next-intl"; import Image from "next/image"; import { useRouter } from "next/navigation"; +import ProjectThumbnail from "@/app/components/ProjectThumbnail"; export type ProjectDetailData = { id: number; @@ -90,9 +91,13 @@ export default function ProjectDetailClient({ {project.imageUrl ? ( {project.title} ) : ( -
- {project.title.charAt(0)} -
+ )} diff --git a/app/_ui/ProjectsPageClient.tsx b/app/_ui/ProjectsPageClient.tsx index 227bdc0..7643578 100644 --- a/app/_ui/ProjectsPageClient.tsx +++ b/app/_ui/ProjectsPageClient.tsx @@ -7,6 +7,7 @@ import Link from "next/link"; import { useTranslations } from "next-intl"; import Image from "next/image"; import { Skeleton } from "../components/ui/Skeleton"; +import ProjectThumbnail from "@/app/components/ProjectThumbnail"; export type ProjectListItem = { id: number | string; // Allow both for Directus (string) and Prisma (number) compatibility @@ -74,7 +75,7 @@ export default function ProjectsPageClient({

- Archive. + {tList("title")}.

{tList("intro")} @@ -127,10 +128,20 @@ export default function ProjectsPageClient({

- {project.imageUrl && ( + {project.imageUrl ? (
{project.title}
+ ) : ( +
+ +
)}
diff --git a/app/components/ProjectThumbnail.tsx b/app/components/ProjectThumbnail.tsx new file mode 100644 index 0000000..44eec1f --- /dev/null +++ b/app/components/ProjectThumbnail.tsx @@ -0,0 +1,236 @@ +"use client"; + +import { useMemo, useId } from "react"; +import { + Terminal, + Smartphone, + Globe, + Code, + LayoutDashboard, + MessageSquare, + Cloud, + Wrench, + Cpu, + Shield, + Boxes, + type LucideIcon, +} from "lucide-react"; + +interface ProjectThumbnailProps { + title: string; + category?: string; + tags?: string[]; + slug?: string; + size?: "card" | "hero"; +} + +const categoryThemes: Record< + string, + { + icon: LucideIcon; + gradient: string; + darkGradient: string; + iconColor: string; + darkIconColor: string; + pattern: "dots" | "grid" | "diagonal" | "circuit" | "waves" | "terminal"; + } +> = { + "Web Development": { + icon: Code, + gradient: "from-liquid-sky/20 via-liquid-blue/10 to-liquid-lavender/20", + darkGradient: "dark:from-liquid-sky/10 dark:via-liquid-blue/5 dark:to-liquid-lavender/10", + iconColor: "text-blue-500", + darkIconColor: "dark:text-blue-400", + pattern: "circuit", + }, + "Mobile Development": { + icon: Smartphone, + gradient: "from-liquid-mint/20 via-liquid-teal/10 to-liquid-sky/20", + darkGradient: "dark:from-liquid-mint/10 dark:via-liquid-teal/5 dark:to-liquid-sky/10", + iconColor: "text-emerald-500", + darkIconColor: "dark:text-emerald-400", + pattern: "waves", + }, + "Web Application": { + icon: Globe, + gradient: "from-liquid-lavender/20 via-liquid-purple/10 to-liquid-pink/20", + darkGradient: "dark:from-liquid-lavender/10 dark:via-liquid-purple/5 dark:to-liquid-pink/10", + iconColor: "text-violet-500", + darkIconColor: "dark:text-violet-400", + pattern: "dots", + }, + "Backend Development": { + icon: Cpu, + gradient: "from-liquid-amber/20 via-liquid-yellow/10 to-liquid-peach/20", + darkGradient: "dark:from-liquid-amber/10 dark:via-liquid-yellow/5 dark:to-liquid-peach/10", + iconColor: "text-amber-500", + darkIconColor: "dark:text-amber-400", + pattern: "grid", + }, + "Full-Stack Development": { + icon: Boxes, + gradient: "from-liquid-teal/20 via-liquid-mint/10 to-liquid-lavender/20", + darkGradient: "dark:from-liquid-teal/10 dark:via-liquid-mint/5 dark:to-liquid-lavender/10", + iconColor: "text-teal-500", + darkIconColor: "dark:text-teal-400", + pattern: "grid", + }, + DevOps: { + icon: Shield, + gradient: "from-liquid-coral/20 via-liquid-rose/10 to-liquid-peach/20", + darkGradient: "dark:from-liquid-coral/10 dark:via-liquid-rose/5 dark:to-liquid-peach/10", + iconColor: "text-red-500", + darkIconColor: "dark:text-red-400", + pattern: "diagonal", + }, + default: { + icon: Wrench, + gradient: "from-liquid-peach/20 via-liquid-rose/10 to-liquid-lavender/20", + darkGradient: "dark:from-liquid-peach/10 dark:via-liquid-rose/5 dark:to-liquid-lavender/10", + iconColor: "text-stone-400", + darkIconColor: "dark:text-stone-500", + pattern: "dots", + }, +}; + +const slugIcons: Record = { + "kernel-panic-404-interactive-terminal": Terminal, + "portfolio-website": LayoutDashboard, + "real-time-chat-application": MessageSquare, + "weather-forecast-app": Cloud, + "clarity": Smartphone, + "e-commerce-platform-api": Boxes, + "task-management-dashboard": LayoutDashboard, +}; + +function PatternOverlay({ pattern, id }: { pattern: string; id: string }) { + const patterns: Record = { + dots: ( + + + + + + + + + ), + grid: ( + + + + + + + + + ), + diagonal: ( + + + + + + + + + ), + circuit: ( + + + + + + + + + + + + + + ), + waves: ( + + + + + + + + + ), + terminal: ( + + + + $_ + + 404 + + ERR + + + + + ), + }; + + return patterns[pattern] || patterns.dots; +} + +export default function ProjectThumbnail({ + title, + category, + tags, + slug, + size = "card", +}: ProjectThumbnailProps) { + const uniqueId = useId(); + const theme = useMemo(() => { + if (slug && slugIcons[slug]) { + const matchedTheme = categoryThemes[category || ""] || categoryThemes.default; + return { ...matchedTheme, icon: slugIcons[slug] }; + } + return categoryThemes[category || ""] || categoryThemes.default; + }, [category, slug]); + + const Icon = theme.icon; + const isHero = size === "hero"; + const displayTags = tags?.slice(0, 3) ?? []; + + return ( +
+ + +
+
+ +
+ + + {title} + + + {displayTags.length > 0 && ( +
+ {displayTags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index d6de5bc..e3e538e 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -7,6 +7,7 @@ import Link from "next/link"; import Image from "next/image"; import { useLocale, useTranslations } from "next-intl"; import { Skeleton } from "boneyard-js/react"; +import ProjectThumbnail from "./ProjectThumbnail"; interface Project { id: number; @@ -27,7 +28,7 @@ const Projects = () => { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const locale = useLocale(); - useTranslations("home.projects"); + const t = useTranslations("home.projects"); useEffect(() => { const loadProjects = async () => { @@ -52,22 +53,22 @@ const Projects = () => {

- Selected Work. + {t("title")}.

- Projects that pushed my boundaries. + {t("subtitle")}

- - View Archive - + + {t("viewAll")} +
{projects.length === 0 && !loading ? (
- No projects yet. + {t("noProjects")}
) : ( projects.map((project) => ( @@ -86,9 +87,13 @@ const Projects = () => { className="object-cover transition-transform duration-700 group-hover:scale-105" /> ) : ( -
- {project.title.charAt(0)} -
+ )} {/* Overlay on Hover */}
diff --git a/messages/de.json b/messages/de.json index 804867c..d3e56b9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -86,10 +86,11 @@ } }, "projects": { - "title": "Ausgewählte Projekte", - "subtitle": "Eine Auswahl an Projekten, an denen ich gearbeitet habe – von Web-Apps bis zu Experimenten.", + "title": "Ausgewählte Arbeiten", + "subtitle": "Projekte, die meine Grenzen erweitert haben.", "featured": "Featured", - "viewAll": "Alle Projekte ansehen" + "viewAll": "Archiv ansehen", + "noProjects": "Noch keine Projekte." }, "contact": { "title": "Kontakt", diff --git a/messages/en.json b/messages/en.json index 189c247..b8cff94 100644 --- a/messages/en.json +++ b/messages/en.json @@ -87,10 +87,11 @@ } }, "projects": { - "title": "Selected Works", - "subtitle": "A collection of projects I've worked on, ranging from web applications to experiments.", + "title": "Selected Work", + "subtitle": "Projects that pushed my boundaries.", "featured": "Featured", - "viewAll": "View All Projects" + "viewAll": "View Archive", + "noProjects": "No projects yet." }, "contact": { "title": "Contact Me",