fix: i18n for project section strings, unique SVG pattern IDs, remove hardcoded text
- Projects.tsx: use t() for title, subtitle, viewAll, noProjects
- ProjectsPageClient.tsx: use tList('title') instead of hardcoded 'Archive'
- ProjectThumbnail.tsx: useId() for unique SVG pattern IDs to avoid collisions
- Remove unused sizeClasses variable
- en.json: update project subtitle and add noProjects key
- de.json: update German translations for project section
This commit is contained in:
@@ -75,7 +75,7 @@ export default function ProjectsPageClient({
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="text-6xl md:text-[10rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase">
|
<h1 className="text-6xl md:text-[10rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase">
|
||||||
Archive<span className="text-liquid-mint">.</span>
|
{tList("title")}<span className="text-liquid-mint">.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-8 text-xl md:text-3xl font-light text-stone-500 dark:text-stone-400 max-w-2xl leading-snug tracking-tight">
|
<p className="mt-8 text-xl md:text-3xl font-light text-stone-500 dark:text-stone-400 max-w-2xl leading-snug tracking-tight">
|
||||||
{tList("intro")}
|
{tList("intro")}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo, useId } from "react";
|
||||||
import {
|
import {
|
||||||
Terminal,
|
Terminal,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
@@ -103,42 +103,42 @@ const slugIcons: Record<string, LucideIcon> = {
|
|||||||
"task-management-dashboard": LayoutDashboard,
|
"task-management-dashboard": LayoutDashboard,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PatternOverlay({ pattern }: { pattern: string }) {
|
function PatternOverlay({ pattern, id }: { pattern: string; id: string }) {
|
||||||
const patternMap: Record<string, React.ReactNode> = {
|
const patterns: Record<string, React.ReactNode> = {
|
||||||
dots: (
|
dots: (
|
||||||
<svg className="absolute inset-0 w-full h-full opacity-[0.07] dark:opacity-[0.05]" xmlns="http://www.w3.org/2000/svg">
|
<svg className="absolute inset-0 w-full h-full opacity-[0.07] dark:opacity-[0.05]" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="dots" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
|
<pattern id={`pat-dots-${id}`} x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||||
<circle cx="2" cy="2" r="1.5" fill="currentColor" />
|
<circle cx="2" cy="2" r="1.5" fill="currentColor" />
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="url(#dots)" />
|
<rect width="100%" height="100%" fill={`url(#pat-dots-${id})`} />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
grid: (
|
grid: (
|
||||||
<svg className="absolute inset-0 w-full h-full opacity-[0.06] dark:opacity-[0.04]" xmlns="http://www.w3.org/2000/svg">
|
<svg className="absolute inset-0 w-full h-full opacity-[0.06] dark:opacity-[0.04]" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="grid" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
|
<pattern id={`pat-grid-${id}`} x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="currentColor" strokeWidth="1" />
|
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="currentColor" strokeWidth="1" />
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
<rect width="100%" height="100%" fill={`url(#pat-grid-${id})`} />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
diagonal: (
|
diagonal: (
|
||||||
<svg className="absolute inset-0 w-full h-full opacity-[0.06] dark:opacity-[0.04]" xmlns="http://www.w3.org/2000/svg">
|
<svg className="absolute inset-0 w-full h-full opacity-[0.06] dark:opacity-[0.04]" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="diagonal" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
|
<pattern id={`pat-diag-${id}`} x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse">
|
||||||
<line x1="0" y1="24" x2="24" y2="0" stroke="currentColor" strokeWidth="1" />
|
<line x1="0" y1="24" x2="24" y2="0" stroke="currentColor" strokeWidth="1" />
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="url(#diagonal)" />
|
<rect width="100%" height="100%" fill={`url(#pat-diag-${id})`} />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
circuit: (
|
circuit: (
|
||||||
<svg className="absolute inset-0 w-full h-full opacity-[0.07] dark:opacity-[0.05]" xmlns="http://www.w3.org/2000/svg">
|
<svg className="absolute inset-0 w-full h-full opacity-[0.07] dark:opacity-[0.05]" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="circuit" x="0" y="0" width="60" height="60" patternUnits="userSpaceOnUse">
|
<pattern id={`pat-circ-${id}`} x="0" y="0" width="60" height="60" patternUnits="userSpaceOnUse">
|
||||||
<path d="M0 30h20m20 0h20M30 0v20m0 20v20" stroke="currentColor" strokeWidth="0.8" fill="none" />
|
<path d="M0 30h20m20 0h20M30 0v20m0 20v20" stroke="currentColor" strokeWidth="0.8" fill="none" />
|
||||||
<circle cx="30" cy="30" r="3" fill="currentColor" />
|
<circle cx="30" cy="30" r="3" fill="currentColor" />
|
||||||
<circle cx="10" cy="30" r="2" fill="currentColor" />
|
<circle cx="10" cy="30" r="2" fill="currentColor" />
|
||||||
@@ -147,36 +147,36 @@ function PatternOverlay({ pattern }: { pattern: string }) {
|
|||||||
<circle cx="30" cy="50" r="2" fill="currentColor" />
|
<circle cx="30" cy="50" r="2" fill="currentColor" />
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="url(#circuit)" />
|
<rect width="100%" height="100%" fill={`url(#pat-circ-${id})`} />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
waves: (
|
waves: (
|
||||||
<svg className="absolute inset-0 w-full h-full opacity-[0.06] dark:opacity-[0.04]" xmlns="http://www.w3.org/2000/svg">
|
<svg className="absolute inset-0 w-full h-full opacity-[0.06] dark:opacity-[0.04]" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="waves" x="0" y="0" width="100" height="20" patternUnits="userSpaceOnUse">
|
<pattern id={`pat-wave-${id}`} x="0" y="0" width="100" height="20" patternUnits="userSpaceOnUse">
|
||||||
<path d="M0 10 Q25 0 50 10 T100 10" fill="none" stroke="currentColor" strokeWidth="1" />
|
<path d="M0 10 Q25 0 50 10 T100 10" fill="none" stroke="currentColor" strokeWidth="1" />
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="url(#waves)" />
|
<rect width="100%" height="100%" fill={`url(#pat-wave-${id})`} />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
terminal: (
|
terminal: (
|
||||||
<svg className="absolute inset-0 w-full h-full opacity-[0.08] dark:opacity-[0.06]" xmlns="http://www.w3.org/2000/svg">
|
<svg className="absolute inset-0 w-full h-full opacity-[0.08] dark:opacity-[0.06]" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="terminal" x="0" y="0" width="200" height="30" patternUnits="userSpaceOnUse">
|
<pattern id={`pat-term-${id}`} x="0" y="0" width="200" height="30" patternUnits="userSpaceOnUse">
|
||||||
<text x="4" y="18" fontFamily="monospace" fontSize="10" fill="currentColor">$_</text>
|
<text x="4" y="18" fontFamily="monospace" fontSize="10" fill="currentColor">$_</text>
|
||||||
<text x="50" y="18" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.4">│</text>
|
<text x="50" y="18" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.4">│</text>
|
||||||
<text x="70" y="18" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.6">404</text>
|
<text x="70" y="18" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.6">404</text>
|
||||||
<text x="110" y="18" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.4">│</text>
|
<text x="110" y="18" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.4">│</text>
|
||||||
<text x="130" y="18" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.3">ERR</text>
|
<text x="130" y="18" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.3">ERR</text>
|
||||||
</pattern>
|
</pattern>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="url(#terminal)" />
|
<rect width="100%" height="100%" fill={`url(#pat-term-${id})`} />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return patternMap[pattern] || patternMap.dots;
|
return patterns[pattern] || patterns.dots;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectThumbnail({
|
export default function ProjectThumbnail({
|
||||||
@@ -186,6 +186,7 @@ export default function ProjectThumbnail({
|
|||||||
slug,
|
slug,
|
||||||
size = "card",
|
size = "card",
|
||||||
}: ProjectThumbnailProps) {
|
}: ProjectThumbnailProps) {
|
||||||
|
const uniqueId = useId();
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => {
|
||||||
if (slug && slugIcons[slug]) {
|
if (slug && slugIcons[slug]) {
|
||||||
const matchedTheme = categoryThemes[category || ""] || categoryThemes.default;
|
const matchedTheme = categoryThemes[category || ""] || categoryThemes.default;
|
||||||
@@ -195,20 +196,14 @@ export default function ProjectThumbnail({
|
|||||||
}, [category, slug]);
|
}, [category, slug]);
|
||||||
|
|
||||||
const Icon = theme.icon;
|
const Icon = theme.icon;
|
||||||
|
|
||||||
const isHero = size === "hero";
|
const isHero = size === "hero";
|
||||||
|
|
||||||
const displayTags = tags?.slice(0, 3) ?? [];
|
const displayTags = tags?.slice(0, 3) ?? [];
|
||||||
|
|
||||||
const sizeClasses = isHero
|
|
||||||
? ""
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 flex items-center justify-center bg-gradient-to-br ${theme.gradient} ${theme.darkGradient} ${sizeClasses}`}
|
className={`absolute inset-0 flex items-center justify-center bg-gradient-to-br ${theme.gradient} ${theme.darkGradient}`}
|
||||||
>
|
>
|
||||||
<PatternOverlay pattern={theme.pattern} />
|
<PatternOverlay pattern={theme.pattern} id={uniqueId} />
|
||||||
|
|
||||||
<div className="relative z-10 flex flex-col items-center gap-3 sm:gap-4">
|
<div className="relative z-10 flex flex-col items-center gap-3 sm:gap-4">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const Projects = () => {
|
|||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
useTranslations("home.projects");
|
const t = useTranslations("home.projects");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadProjects = async () => {
|
const loadProjects = async () => {
|
||||||
@@ -53,22 +53,22 @@ const Projects = () => {
|
|||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 sm:mb-12 md:mb-16 gap-4 sm:gap-6">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 sm:mb-12 md:mb-16 gap-4 sm:gap-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black text-stone-900 dark:text-stone-50 tracking-tighter mb-2 sm:mb-4 uppercase">
|
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black text-stone-900 dark:text-stone-50 tracking-tighter mb-2 sm:mb-4 uppercase">
|
||||||
Selected Work<span className="text-emerald-600 dark:text-emerald-400">.</span>
|
{t("title")}<span className="text-emerald-600 dark:text-emerald-400">.</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-base sm:text-lg md:text-xl text-stone-500 max-w-xl font-light">
|
<p className="text-base sm:text-lg md:text-xl text-stone-500 max-w-xl font-light">
|
||||||
Projects that pushed my boundaries.
|
{t("subtitle")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/${locale}/projects`} className="group flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-all text-xs uppercase tracking-widest">
|
<Link href={`/${locale}/projects`} className="group flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-all text-xs uppercase tracking-widest">
|
||||||
View Archive <ArrowUpRight className="group-hover:-translate-y-1 group-hover:translate-x-1 transition-transform" size={14} />
|
{t("viewAll")} <ArrowUpRight className="group-hover:-translate-y-1 group-hover:translate-x-1 transition-transform" size={14} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Skeleton name="projects-grid" loading={loading} animate="shimmer" transition>
|
<Skeleton name="projects-grid" loading={loading} animate="shimmer" transition>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-12">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-12">
|
||||||
{projects.length === 0 && !loading ? (
|
{projects.length === 0 && !loading ? (
|
||||||
<div className="col-span-2 py-12 text-center text-stone-400 dark:text-stone-600 text-sm">
|
<div className="col-span-2 py-12 text-center text-stone-400 dark:text-stone-600 text-sm">
|
||||||
No projects yet.
|
{t("noProjects")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
projects.map((project) => (
|
projects.map((project) => (
|
||||||
|
|||||||
@@ -86,10 +86,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"title": "Ausgewählte Projekte",
|
"title": "Ausgewählte Arbeiten",
|
||||||
"subtitle": "Eine Auswahl an Projekten, an denen ich gearbeitet habe – von Web-Apps bis zu Experimenten.",
|
"subtitle": "Projekte, die meine Grenzen erweitert haben.",
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"viewAll": "Alle Projekte ansehen"
|
"viewAll": "Archiv ansehen",
|
||||||
|
"noProjects": "Noch keine Projekte."
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"title": "Kontakt",
|
"title": "Kontakt",
|
||||||
|
|||||||
@@ -87,10 +87,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"title": "Selected Works",
|
"title": "Selected Work",
|
||||||
"subtitle": "A collection of projects I've worked on, ranging from web applications to experiments.",
|
"subtitle": "Projects that pushed my boundaries.",
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"viewAll": "View All Projects"
|
"viewAll": "View Archive",
|
||||||
|
"noProjects": "No projects yet."
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"title": "Contact Me",
|
"title": "Contact Me",
|
||||||
|
|||||||
Reference in New Issue
Block a user