31560a712f
- Fix ClientWrappers missing 'about' namespace (MISSING_MESSAGE error) - Add system/light/dark theme toggle with prefers-color-scheme detection - Rewrite 404 page with i18n, accessibility, and proper navigation - Rewrite books page with Header/Footer, i18n, and semantic HTML - Add i18n keys to About, Footer, and both locale files - Fix dark mode contrast: text-stone-300/600 -> text-stone-400 - Replace raw hex bg-[#fdfcf8] with bg-stone-50 across all components - Guard console.error in ChatWidget and manage/page behind NODE_ENV - Add aria-label to admin login form - Remove emoji from manage page password toggle - Update stale dates in privacy-policy and legal-notice - Fix ScrollFadeIn index->delay prop type error in books page - Fix privacy-policy and legal-notice landmark structure - Add pre-push-check.test.ts: 13-category static analysis (i18n parity, namespace coverage, key resolution, accessibility, email validation, hex colors, emojis, console guards, env docs, types) - Add explicit i18n check step to CI workflow
100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
||
import ProjectsPageClient, { ProjectListItem } from "@/app/_ui/ProjectsPageClient";
|
||
import type { Metadata } from "next";
|
||
import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo";
|
||
import { getProjects as getDirectusProjects } from "@/lib/directus";
|
||
|
||
export const revalidate = 300;
|
||
|
||
export async function generateMetadata({
|
||
params,
|
||
}: {
|
||
params: Promise<{ locale: string }>;
|
||
}): Promise<Metadata> {
|
||
const { locale } = await params;
|
||
const languages = getLanguageAlternates({ pathWithoutLocale: "projects" });
|
||
const isDe = locale === "de";
|
||
return {
|
||
title: isDe ? "Projekte – Dennis Konkol" : "Projects – Dennis Konkol",
|
||
description: isDe
|
||
? "Webentwicklung, Fullstack-Apps und Mobile-Projekte von Dennis Konkol. Next.js, Flutter, Docker und mehr – Osnabrück."
|
||
: "Web development, fullstack apps and mobile projects by Dennis Konkol. Next.js, Flutter, Docker and more – Osnabrück.",
|
||
alternates: {
|
||
canonical: toAbsoluteUrl(`/${locale}/projects`),
|
||
languages,
|
||
},
|
||
};
|
||
}
|
||
|
||
export default async function ProjectsPage({
|
||
params,
|
||
}: {
|
||
params: Promise<{ locale: string }>;
|
||
}) {
|
||
const { locale } = await params;
|
||
|
||
// Fetch from PostgreSQL
|
||
const dbProjects = await prisma.project.findMany({
|
||
where: { published: true },
|
||
orderBy: { createdAt: "desc" },
|
||
include: {
|
||
translations: {
|
||
select: { title: true, description: true, locale: true },
|
||
},
|
||
},
|
||
});
|
||
|
||
// Fetch from Directus
|
||
let directusProjects: ProjectListItem[] = [];
|
||
try {
|
||
const fetched = await getDirectusProjects(locale, { published: true });
|
||
if (fetched) {
|
||
directusProjects = fetched.map(p => ({
|
||
...p,
|
||
id: typeof p.id === 'string' ? (parseInt(p.id) || 0) : p.id,
|
||
})) as ProjectListItem[];
|
||
}
|
||
} catch (err) {
|
||
if (process.env.NODE_ENV === "development") console.error("Directus projects fetch failed:", err);
|
||
}
|
||
|
||
const localizedDb: ProjectListItem[] = dbProjects.map((p) => {
|
||
const trPreferred = p.translations?.find((t) => t.locale === locale && (t?.title || t?.description));
|
||
const trDefault = p.translations?.find(
|
||
(t) => t.locale === p.defaultLocale && (t?.title || t?.description),
|
||
);
|
||
const tr = trPreferred ?? trDefault;
|
||
return {
|
||
id: p.id,
|
||
slug: p.slug,
|
||
title: tr?.title ?? p.title,
|
||
description: tr?.description ?? p.description,
|
||
tags: p.tags,
|
||
category: p.category,
|
||
date: p.date,
|
||
createdAt: p.createdAt.toISOString(),
|
||
imageUrl: p.imageUrl,
|
||
};
|
||
});
|
||
|
||
// Merge projects, prioritizing DB ones if slugs match
|
||
const allProjects: ProjectListItem[] = [...localizedDb];
|
||
const dbSlugs = new Set(localizedDb.map(p => p.slug));
|
||
|
||
for (const dp of directusProjects) {
|
||
if (!dbSlugs.has(dp.slug)) {
|
||
allProjects.push(dp);
|
||
}
|
||
}
|
||
|
||
// Final sort by date
|
||
allProjects.sort((a, b) => {
|
||
const dateA = new Date(a.date || a.createdAt || 0).getTime();
|
||
const dateB = new Date(b.date || b.createdAt || 0).getTime();
|
||
return dateB - dateA;
|
||
});
|
||
|
||
return <ProjectsPageClient projects={allProjects} locale={locale} />;
|
||
}
|
||
|