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.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 ? (
+ ) : (
+
)}
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: (
+
+ ),
+ };
+
+ 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",