20b719893c
- Shared SubpageShell (HeaderClient paper strip + ink footer) and SubpageIntro, btn/ghost/card helpers in app/_ui/subpage.tsx - Projects list: poster cards with hard ink shadows, filter pills, ink search field; no fake 800ms skeleton and no framer-motion opacity:0 in SSR - Project detail: poster hero, .plakat-prose markdown, links/stack aside, back link works without JS - Books: server-side fetch (visible without JS), cover + rating pills - Legal notice / privacy: moved to app/_ui/*Client.tsx, root routes removed (middleware always redirects to /[locale]), consent reset via useConsent - ProjectThumbnail rebuilt on Plakat tokens - Old Header.tsx + test removed; brown/cream/sand palettes and unused glass-panel/admin-glass(-light)/liquid-bg-blob/float CSS dropped (liquid palette, glass-card, admin-glass-card stay for admin + 404) - .plakat-prose replaces the missing typography plugin Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
||
import ProjectsPageClient, { ProjectListItem } from "@/app/_ui/ProjectsPageClient";
|
||
import SubpageShell from "@/app/_ui/SubpageShell";
|
||
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 (
|
||
<SubpageShell locale={locale}>
|
||
<ProjectsPageClient projects={allProjects} locale={locale} />
|
||
</SubpageShell>
|
||
);
|
||
}
|
||
|