+
+ {t("title")}
+
+
+ {/* ── Botgenossen featured collaboration card ── */}
+
+
+ {/* Top amber accent line */}
+
+
+
+ {/* Left: content */}
+
+ {/* Badges row */}
+
+
+
+ {tBot("live")}
+
+
+ {t("featuredCollab")}
+
+
+ AI & Automation
+
+
+
+ {/* Title */}
+
+ {tBot("title")}
+
+
+ {/* Description */}
+
+ {tBot("description")}
+
+
+ {/* Tech tags */}
+
+ {BOTGENOSSEN_TECHS.map((tech) => (
+
+ {tech}
+
+ ))}
+
+
+ {/* Footer: company + link */}
+
+
+
+
+ {tBot("company")}
+
+ ·
+
+ {tBot("role")}
+
+
+
+
+ buettelborn.de
+
+
+
+
+
+ {/* Right: large logo */}
+
+
+
+
+
+
+
+
+ {projects.map((project, index) => (
+
+ {/* Top accent line */}
+
+
+
+ {/* Number + category */}
+
+
+ {String(index + 1).padStart(2, "0")}
+
+
+ {isFeaturedProject(project) && (
+
+ {t("featuredProject")}
+
+ )}
+
+ {tCategories(project.category)}
+
+
+
+
+ {/* Title */}
+
+ {project.title}
+
+
+ {/* Description */}
+
+ {tDescriptions(project.description)}
+
+
+ {/* Tech tags — top 3 */}
+
+ {project.technologies.slice(0, 3).map((tech) => (
+
+ {tech}
+
+ ))}
+ {project.technologies.length > 3 && (
+
+ +{project.technologies.length - 3}
+
+ )}
+
+
+ {/* Links */}
+
+ {project.githubUrl && (
+
+
+
+ )}
+ {project.liveUrl && (
+
+
+
+ )}
+ {project.npmUrl && (
+
+
+
+ )}
+
+
+
+ ))}
+
+
+ {/* View all link */}
+
+
+ {t("viewAllProjects")}
+
+
+
+
+ );
+}
diff --git a/components/projects/index.ts b/components/projects/index.ts
index 3be4e612..7bdd1936 100644
--- a/components/projects/index.ts
+++ b/components/projects/index.ts
@@ -6,4 +6,5 @@
export { default as ProjectsShowcase } from "./ProjectsShowcase";
export { ProjectCard } from "./ProjectCard";
export { ProjectsFilter } from "./ProjectsFilter";
+export { ProjectsHomeShowcase } from "./ProjectsHomeShowcase";
export * from "./projects-showcase.utils";
diff --git a/components/projects/projects-showcase.utils.ts b/components/projects/projects-showcase.utils.ts
index 4f6ef875..bdf4a7ce 100644
--- a/components/projects/projects-showcase.utils.ts
+++ b/components/projects/projects-showcase.utils.ts
@@ -11,6 +11,23 @@ import type {
Project,
} from "@/types/hubs/projects";
+/**
+ * Derives the GitHub social preview (OG) image URL from a repo URL.
+ * Returns null when githubUrl is empty.
+ */
+export function getGithubOgImage(githubUrl: string): string | null {
+ if (!githubUrl) return null;
+ try {
+ const { pathname } = new URL(githubUrl);
+ // pathname = "/OWNER/REPO" — strip leading slash
+ const path = pathname.replace(/^\//, "").replace(/\.git$/, "");
+ if (!path || !path.includes("/")) return null;
+ return `https://opengraph.githubassets.com/1/${path}`;
+ } catch {
+ return null;
+ }
+}
+
/**
* Custom hook for handling project card logic
*/
@@ -112,6 +129,7 @@ export function getLicenseBadgeClasses(licenseType?: string): string {
return "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800";
case "gpl":
case "copyleft":
+ case "agpl":
return "bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/20 dark:text-purple-400 dark:border-purple-800";
case "apache":
return "bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-900/20 dark:text-orange-400 dark:border-orange-800";
@@ -137,6 +155,7 @@ export function getLicenseEmoji(licenseType?: string): string {
return "🔵";
case "gpl":
case "copyleft":
+ case "agpl":
return "🔒";
case "apache":
return "🦅";
diff --git a/components/use-cases/screenshot-gallery.tsx b/components/use-cases/screenshot-gallery.tsx
index c24d2a4c..94063e38 100644
--- a/components/use-cases/screenshot-gallery.tsx
+++ b/components/use-cases/screenshot-gallery.tsx
@@ -1,11 +1,12 @@
/**
* @author ColdByDefault
* @copyright 2026 ColdByDefault. All Rights Reserved.
-*/
+ */
"use client";
import { useState, useCallback, useEffect } from "react";
+import { createPortal } from "react-dom";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight, X } from "lucide-react";
@@ -74,82 +75,85 @@ export function ScreenshotGallery({
alt={`${projectTitle} screenshot ${currentIndex + 1}`}
fill
className="object-cover transition-transform group-hover:scale-105"
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
- {/* Fullscreen overlay */}
- {isOpen && (
-
- {/* Close button */}
-
-
-
-
- {/* Navigation arrows - fixed position */}
- {screenshots.length > 1 && (
- <>
-
{
- e.stopPropagation();
- goToPrevious();
- }}
- aria-label="Previous screenshot"
- >
-
-
-
{
- e.stopPropagation();
- goToNext();
- }}
- aria-label="Next screenshot"
- >
-
-
- >
- )}
-
- {/* Image container - stop propagation so clicking image doesn't close */}
+ {/* Fullscreen overlay — rendered via portal so fixed positioning is always relative to viewport */}
+ {isOpen &&
+ createPortal(
e.stopPropagation()}
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
+ onClick={handleClose}
+ role="dialog"
+ aria-modal="true"
+ aria-label={`${projectTitle} screenshot viewer`}
>
-
-
- {/* Image counter */}
+ {/* Close button */}
+
+
+
+
+ {/* Navigation arrows - fixed position */}
{screenshots.length > 1 && (
-
- {currentIndex + 1} / {screenshots.length}
-
+ <>
+
{
+ e.stopPropagation();
+ goToPrevious();
+ }}
+ aria-label="Previous screenshot"
+ >
+
+
+
{
+ e.stopPropagation();
+ goToNext();
+ }}
+ aria-label="Next screenshot"
+ >
+
+
+ >
)}
-
-
- )}
+
+ {/* Image container - stop propagation so clicking image doesn't close */}
+ e.stopPropagation()}
+ >
+
+
+ {/* Image counter */}
+ {screenshots.length > 1 && (
+
+ {currentIndex + 1} / {screenshots.length}
+
+ )}
+
+