fix: resolve project 404s with Directus fallback and upgrade 404 page

Merged Directus and PostgreSQL project data, implemented single project fetch from CMS, and modernized the NotFound component with liquid design.
This commit is contained in:
denshooter
2026-02-15 22:47:25 +01:00
parent 6998a0e7a1
commit cc8fff14d2
7 changed files with 370 additions and 237 deletions
+57 -28
View File
@@ -3,6 +3,7 @@ import ProjectDetailClient from "@/app/_ui/ProjectDetailClient";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo"; import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo";
import { getProjectBySlug } from "@/lib/directus";
export const revalidate = 300; export const revalidate = 300;
@@ -12,6 +13,20 @@ export async function generateMetadata({
params: Promise<{ locale: string; slug: string }>; params: Promise<{ locale: string; slug: string }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const { locale, slug } = await params; const { locale, slug } = await params;
// Try Directus first for metadata
const directusProject = await getProjectBySlug(slug, locale);
if (directusProject) {
return {
title: directusProject.title,
description: directusProject.description,
alternates: {
canonical: toAbsoluteUrl(`/${locale}/projects/${slug}`),
languages: getLanguageAlternates({ pathWithoutLocale: `projects/${slug}` }),
},
};
}
const languages = getLanguageAlternates({ pathWithoutLocale: `projects/${slug}` }); const languages = getLanguageAlternates({ pathWithoutLocale: `projects/${slug}` });
return { return {
alternates: { alternates: {
@@ -28,7 +43,8 @@ export default async function ProjectPage({
}) { }) {
const { locale, slug } = await params; const { locale, slug } = await params;
const project = await prisma.project.findFirst({ // Try PostgreSQL first
const dbProject = await prisma.project.findFirst({
where: { slug, published: true }, where: { slug, published: true },
include: { include: {
translations: { translations: {
@@ -37,43 +53,56 @@ export default async function ProjectPage({
}, },
}); });
if (!project) return notFound(); let projectData: any = null;
const trPreferred = project.translations?.find((t) => t.locale === locale && (t?.title || t?.description)); if (dbProject) {
const trDefault = project.translations?.find( const trPreferred = dbProject.translations?.find((t) => t.locale === locale && (t?.title || t?.description));
(t) => t.locale === project.defaultLocale && (t?.title || t?.description), const trDefault = dbProject.translations?.find(
); (t) => t.locale === dbProject.defaultLocale && (t?.title || t?.description),
const tr = trPreferred ?? trDefault; );
const { translations: _translations, ...rest } = project; const tr = trPreferred ?? trDefault;
const localizedContent = (() => { const { translations: _translations, ...rest } = dbProject;
if (typeof tr?.content === "string") return tr.content; const localizedContent = (() => {
if (tr?.content && typeof tr.content === "object" && "markdown" in tr.content) { if (typeof tr?.content === "string") return tr.content;
const markdown = (tr.content as Record<string, unknown>).markdown; if (tr?.content && typeof tr.content === "object" && "markdown" in tr.content) {
if (typeof markdown === "string") return markdown; const markdown = (tr.content as Record<string, unknown>).markdown;
if (typeof markdown === "string") return markdown;
}
return dbProject.content;
})();
projectData = {
...rest,
title: tr?.title ?? dbProject.title,
description: tr?.description ?? dbProject.description,
content: localizedContent,
};
} else {
// Try Directus fallback
const directusProject = await getProjectBySlug(slug, locale);
if (directusProject) {
projectData = {
...directusProject,
id: parseInt(directusProject.id) || 0,
};
} }
return project.content; }
})();
const localized = { if (!projectData) return notFound();
...rest,
title: tr?.title ?? project.title,
description: tr?.description ?? project.description,
content: localizedContent,
};
const jsonLd = { const jsonLd = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "SoftwareSourceCode", "@type": "SoftwareSourceCode",
"name": localized.title, "name": projectData.title,
"description": localized.description, "description": projectData.description,
"codeRepository": localized.github, "codeRepository": projectData.github_url || projectData.github,
"programmingLanguage": localized.technologies, "programmingLanguage": projectData.technologies,
"author": { "author": {
"@type": "Person", "@type": "Person",
"name": "Dennis Konkol" "name": "Dennis Konkol"
}, },
"dateCreated": project.date, "dateCreated": projectData.date || projectData.created_at,
"url": toAbsoluteUrl(`/${locale}/projects/${slug}`), "url": toAbsoluteUrl(`/${locale}/projects/${slug}`),
"image": localized.imageUrl ? toAbsoluteUrl(localized.imageUrl) : undefined, "image": projectData.imageUrl || projectData.image_url ? toAbsoluteUrl(projectData.imageUrl || projectData.image_url) : undefined,
}; };
return ( return (
@@ -82,7 +111,7 @@ export default async function ProjectPage({
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/> />
<ProjectDetailClient project={localized} locale={locale} /> <ProjectDetailClient project={projectData} locale={locale} />
</> </>
); );
} }
+36 -3
View File
@@ -2,6 +2,7 @@ import { prisma } from "@/lib/prisma";
import ProjectsPageClient from "@/app/_ui/ProjectsPageClient"; import ProjectsPageClient from "@/app/_ui/ProjectsPageClient";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo"; import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo";
import { getProjects as getDirectusProjects } from "@/lib/directus";
export const revalidate = 300; export const revalidate = 300;
@@ -27,7 +28,8 @@ export default async function ProjectsPage({
}) { }) {
const { locale } = await params; const { locale } = await params;
const projects = await prisma.project.findMany({ // Fetch from PostgreSQL
const dbProjects = await prisma.project.findMany({
where: { published: true }, where: { published: true },
orderBy: { createdAt: "desc" }, orderBy: { createdAt: "desc" },
include: { include: {
@@ -37,7 +39,21 @@ export default async function ProjectsPage({
}, },
}); });
const localized = projects.map((p) => { // Fetch from Directus
let directusProjects: any[] = [];
try {
const fetched = await getDirectusProjects(locale, { published: true });
if (fetched) {
directusProjects = fetched.map(p => ({
...p,
id: parseInt(p.id) || 0,
}));
}
} catch (err) {
console.error("Directus projects fetch failed:", err);
}
const localizedDb = dbProjects.map((p) => {
const trPreferred = p.translations?.find((t) => t.locale === locale && (t?.title || t?.description)); const trPreferred = p.translations?.find((t) => t.locale === locale && (t?.title || t?.description));
const trDefault = p.translations?.find( const trDefault = p.translations?.find(
(t) => t.locale === p.defaultLocale && (t?.title || t?.description), (t) => t.locale === p.defaultLocale && (t?.title || t?.description),
@@ -51,6 +67,23 @@ export default async function ProjectsPage({
}; };
}); });
return <ProjectsPageClient projects={localized} locale={locale} />; // Merge projects, prioritizing DB ones if slugs match
const allProjects = [...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} />;
} }
+28 -47
View File
@@ -47,8 +47,9 @@ export async function GET(request: NextRequest) {
const locale = searchParams.get('locale') || 'en'; const locale = searchParams.get('locale') || 'en';
// Try Directus FIRST (Primary Source) // Try Directus FIRST (Primary Source)
let directusProjects: any[] = [];
try { try {
const directusProjects = await getDirectusProjects(locale, { const fetched = await getDirectusProjects(locale, {
featured: featured === 'true' ? true : featured === 'false' ? false : undefined, featured: featured === 'true' ? true : featured === 'false' ? false : undefined,
published: published === 'true' ? true : published === 'false' ? false : undefined, published: published === 'true' ? true : published === 'false' ? false : undefined,
category: category || undefined, category: category || undefined,
@@ -57,54 +58,34 @@ export async function GET(request: NextRequest) {
limit limit
}); });
if (directusProjects && directusProjects.length > 0) { if (fetched) {
return NextResponse.json({ directusProjects = fetched;
projects: directusProjects,
total: directusProjects.length,
page: 1,
limit: directusProjects.length,
source: 'directus'
});
} }
} catch (directusError) { } catch (directusError) {
console.log('Directus not available, trying PostgreSQL fallback'); console.log('Directus error, continuing with PostgreSQL');
} }
// Fallback 1: Try PostgreSQL // Fallback 1: Try PostgreSQL
try { try {
await prisma.$queryRaw`SELECT 1`; await prisma.$queryRaw`SELECT 1`;
} catch (dbError) { } catch (dbError) {
console.log('PostgreSQL also not available, using empty fallback'); console.log('PostgreSQL not available');
if (directusProjects.length > 0) {
return NextResponse.json({
projects: directusProjects,
total: directusProjects.length,
source: 'directus'
});
}
// Fallback 2: Return empty (components should have hardcoded fallback)
return NextResponse.json({ return NextResponse.json({
projects: [], projects: [],
total: 0, total: 0,
page: 1,
limit,
source: 'fallback' source: 'fallback'
}); });
} }
// Create cache parameters object
const cacheParams = {
page: page.toString(),
limit: limit.toString(),
category,
featured,
published,
difficulty,
search
};
// Check cache first
const cached = await apiCache.getProjects(cacheParams);
if (cached && !search) { // Don't cache search results
return NextResponse.json(cached);
}
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const where: Record<string, unknown> = {}; const where: Record<string, unknown> = {};
if (category) where.category = category; if (category) where.category = category;
@@ -116,12 +97,11 @@ export async function GET(request: NextRequest) {
where.OR = [ where.OR = [
{ title: { contains: search, mode: 'insensitive' } }, { title: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } },
{ tags: { hasSome: [search] } }, { tags: { hasSome: [search] } }
{ content: { contains: search, mode: 'insensitive' } }
]; ];
} }
const [projects, total] = await Promise.all([ const [dbProjects, total] = await Promise.all([
prisma.project.findMany({ prisma.project.findMany({
where, where,
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
@@ -131,20 +111,21 @@ export async function GET(request: NextRequest) {
prisma.project.count({ where }) prisma.project.count({ where })
]); ]);
const result = { // Merge logic
projects, const dbSlugs = new Set(dbProjects.map(p => p.slug));
total, const mergedProjects = [...dbProjects];
pages: Math.ceil(total / limit),
currentPage: page, for (const dp of directusProjects) {
source: 'postgresql' if (!dbSlugs.has(dp.slug)) {
}; mergedProjects.push(dp);
}
// Cache the result (only for non-search queries)
if (!search) {
await apiCache.setProjects(cacheParams, result);
} }
return NextResponse.json(result); return NextResponse.json({
projects: mergedProjects,
total: total + (mergedProjects.length - dbProjects.length),
source: 'merged'
});
} catch (error) { } catch (error) {
// Handle missing database table gracefully // Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
+97 -125
View File
@@ -1,149 +1,121 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { motion } from "framer-motion";
import { Home, ArrowLeft, Search, Ghost, RefreshCcw } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Home, ArrowLeft, Search } from "lucide-react"; import { useEffect, useState } from "react";
export default function NotFound() { export default function NotFound() {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [input, setInput] = useState("");
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
// In tests, avoid next/dynamic loadable timing and render a stable fallback if (!mounted) return null;
if (process.env.NODE_ENV === "test") {
return (
<div>
Oops! The page you&apos;re looking for doesn&apos;t exist.
</div>
);
}
if (!mounted) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#faf8f3]">
<div className="text-center">
<div className="text-[#795548]">Loading...</div>
</div>
</div>
);
}
const handleCommand = (cmd: string) => {
const command = cmd.toLowerCase().trim();
if (command === 'home' || command === 'cd ~' || command === 'cd /') {
router.push('/');
} else if (command === 'back' || command === 'cd ..') {
router.back();
} else if (command === 'search') {
router.push('/projects');
}
};
return ( return (
<div className="min-h-screen flex items-center justify-center bg-[#faf8f3] p-4"> <div className="min-h-screen flex items-center justify-center bg-[#fdfcf8] dark:bg-stone-950 overflow-hidden relative">
<div className="w-full max-w-2xl"> {/* Liquid Background Blobs */}
{/* Terminal-style 404 */} <motion.div
<div className="bg-[#3e2723] rounded-2xl shadow-2xl overflow-hidden border border-[#5d4037]"> className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-liquid-mint/20 dark:bg-liquid-mint/10 rounded-full blur-[120px]"
{/* Terminal Header */} animate={{
<div className="bg-[#5d4037] px-4 py-3 flex items-center gap-2 border-b border-[#795548]"> scale: [1, 1.2, 1],
<div className="flex gap-2"> x: [0, 50, 0],
<div className="w-3 h-3 rounded-full bg-[#d84315]"></div> y: [0, 30, 0],
<div className="w-3 h-3 rounded-full bg-[#bcaaa4]"></div> }}
<div className="w-3 h-3 rounded-full bg-[#a1887f]"></div> transition={{ duration: 15, repeat: Infinity, ease: "easeInOut" }}
</div> />
<div className="ml-4 text-[#faf8f3] text-sm font-mono"> <motion.div
terminal@portfolio ~ 404 className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-liquid-rose/20 dark:bg-liquid-rose/10 rounded-full blur-[120px]"
</div> animate={{
scale: [1.2, 1, 1.2],
x: [0, -50, 0],
y: [0, -30, 0],
}}
transition={{ duration: 18, repeat: Infinity, ease: "easeInOut" }}
/>
<div className="relative z-10 max-w-2xl w-full px-6 text-center">
{/* Large 404 with Liquid Animation */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.1, 0.25, 1] }}
className="relative inline-block mb-8"
>
<h1 className="text-[12rem] md:text-[16rem] font-black text-stone-900/5 dark:text-stone-100/5 select-none leading-none">
404
</h1>
<motion.div
className="absolute inset-0 flex items-center justify-center"
animate={{ y: [0, -10, 0] }}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
>
<Ghost size={120} className="text-stone-800 dark:text-stone-200 opacity-80" />
</motion.div>
</motion.div>
{/* Content Card */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2, ease: [0.25, 0.1, 0.25, 1] }}
className="bg-white/40 dark:bg-stone-900/40 backdrop-blur-2xl border border-white/60 dark:border-white/10 rounded-[2.5rem] p-8 md:p-12 shadow-[0_20px_50px_rgba(0,0,0,0.05)] dark:shadow-none"
>
<h2 className="text-3xl md:text-4xl font-bold text-stone-900 dark:text-stone-50 mb-4 font-sans tracking-tight">
Lost in the Liquid.
</h2>
<p className="text-stone-600 dark:text-stone-400 text-lg md:text-xl font-light leading-relaxed mb-10 max-w-md mx-auto">
The page you are looking for has evaporated or never existed in this dimension.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Link
href="/"
className="flex items-center justify-center gap-3 px-8 py-4 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-2xl font-bold hover:scale-[1.02] active:scale-[0.98] transition-all shadow-lg hover:shadow-xl dark:shadow-none group"
>
<Home size={20} className="group-hover:-translate-y-0.5 transition-transform" />
<span>Back Home</span>
</Link>
<button
onClick={() => router.back()}
className="flex items-center justify-center gap-3 px-8 py-4 bg-white dark:bg-stone-800 text-stone-900 dark:text-stone-100 border border-stone-200 dark:border-stone-700 rounded-2xl font-bold hover:bg-stone-50 dark:hover:bg-stone-700 hover:scale-[1.02] active:scale-[0.98] transition-all shadow-sm group"
>
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
<span>Go Back</span>
</button>
</div> </div>
{/* Terminal Body */} <div className="mt-8 pt-8 border-t border-stone-100 dark:border-stone-800">
<div className="p-6 md:p-8 font-mono text-sm md:text-base"> <Link
<div className="mb-6"> href="/projects"
<div className="text-[#bcaaa4] mb-2">$ cd {mounted ? window.location.pathname : '/unknown'}</div> className="inline-flex items-center gap-2 text-stone-500 dark:text-stone-400 hover:text-stone-900 dark:hover:text-stone-100 transition-colors font-medium group"
<div className="text-[#d84315] mb-4"> >
<span className="mr-2"></span> <Search size={18} className="group-hover:rotate-12 transition-transform" />
Error: ENOENT: no such file or directory <span>Looking for my work? Explore projects</span>
</div> </Link>
<div className="text-[#a1887f] mb-6">
<pre className="whitespace-pre-wrap">
{`
██╗ ██╗ ██████╗ ██╗ ██╗
██║ ██║██╔═████╗██║ ██║
███████║██║██╔██║███████║
╚════██║████╔╝██║╚════██║
██║╚██████╔╝ ██║
╚═╝ ╚═════╝ ╚═╝
`}
</pre>
</div>
<div className="text-[#faf8f3] mb-6">
<p className="mb-3">The page you&apos;re looking for seems to have wandered off.</p>
<p className="text-[#bcaaa4]">Perhaps it never existed, or maybe it&apos;s on a coffee break.</p>
</div>
<div className="mb-6 text-[#a1887f]">
<div className="mb-2">Available commands:</div>
<div className="pl-4 space-y-1 text-sm">
<div> <span className="text-[#faf8f3]">home</span> - Return to homepage</div>
<div> <span className="text-[#faf8f3]">back</span> - Go back to previous page</div>
<div> <span className="text-[#faf8f3]">search</span> - Search the website</div>
</div>
</div>
</div>
{/* Interactive Command Line */}
<div className="flex items-center gap-2 border-t border-[#5d4037] pt-4">
<span className="text-[#a1887f]">$</span>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleCommand(input);
setInput('');
}
}}
placeholder="Type a command..."
className="flex-1 bg-transparent text-[#faf8f3] outline-none placeholder:text-[#795548] font-mono"
autoFocus
/>
</div>
</div> </div>
</div> </motion.div>
{/* Quick Action Buttons */} {/* Floating Help Badge */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4"> <motion.div
<Link initial={{ opacity: 0 }}
href="/" animate={{ opacity: 1 }}
className="flex items-center justify-center gap-2 bg-[#fffcf5] hover:bg-[#faf8f3] border border-[#d7ccc8] rounded-xl px-6 py-4 transition-all hover:shadow-md group" transition={{ delay: 1 }}
className="mt-12"
>
<button
onClick={() => window.location.reload()}
className="inline-flex items-center gap-2 px-4 py-2 bg-stone-100 dark:bg-stone-800 rounded-full text-stone-500 dark:text-stone-400 text-xs font-mono hover:text-stone-800 dark:hover:text-stone-200 transition-colors"
> >
<Home className="w-5 h-5 text-[#5d4037] group-hover:text-[#3e2723]" /> <RefreshCcw size={12} />
<span className="text-[#3e2723] font-medium">Home</span> <span>ERR_PAGE_NOT_FOUND_404</span>
</Link>
<button
onClick={() => router.back()}
className="flex items-center justify-center gap-2 bg-[#fffcf5] hover:bg-[#faf8f3] border border-[#d7ccc8] rounded-xl px-6 py-4 transition-all hover:shadow-md group"
>
<ArrowLeft className="w-5 h-5 text-[#5d4037] group-hover:text-[#3e2723]" />
<span className="text-[#3e2723] font-medium">Go Back</span>
</button> </button>
</motion.div>
<Link
href="/projects"
className="flex items-center justify-center gap-2 bg-[#fffcf5] hover:bg-[#faf8f3] border border-[#d7ccc8] rounded-xl px-6 py-4 transition-all hover:shadow-md group"
>
<Search className="w-5 h-5 text-[#5d4037] group-hover:text-[#3e2723]" />
<span className="text-[#3e2723] font-medium">Explore Projects</span>
</Link>
</div>
</div> </div>
</div> </div>
); );
+109
View File
@@ -659,3 +659,112 @@ export async function getProjects(
return null; return null;
} }
} }
/**
* Get a single project by slug from Directus
*/
export async function getProjectBySlug(
slug: string,
locale: string
): Promise<Project | null> {
const directusLocale = toDirectusLocale(locale);
const query = `
query {
projects(
filter: {
_and: [
{ slug: { _eq: "${slug}" } },
{ status: { _eq: "published" } }
]
}
limit: 1
) {
id
slug
category
difficulty
tags
technologies
challenges
lessons_learned
future_improvements
github
live
image_url
demo_video
date_created
date_updated
featured
status
translations {
title
description
content
meta_description
keywords
languages_code { code }
}
}
}
`;
try {
const result = await directusRequest(
'',
{ body: { query } }
);
const projects = (result as any)?.projects;
if (!projects || projects.length === 0) {
return null;
}
const proj = projects[0];
const trans =
proj.translations?.find((t: any) => t.languages_code?.code === directusLocale) ||
proj.translations?.[0] ||
{};
// Parse JSON string fields if needed
const parseTags = (tags: any) => {
if (!tags) return [];
if (Array.isArray(tags)) return tags;
if (typeof tags === 'string') {
try {
return JSON.parse(tags);
} catch {
return [];
}
}
return [];
};
return {
id: proj.id,
slug: proj.slug,
title: trans.title || proj.slug,
description: trans.description || '',
content: trans.content,
category: proj.category,
difficulty: proj.difficulty,
tags: parseTags(proj.tags),
technologies: parseTags(proj.technologies),
challenges: proj.challenges,
lessons_learned: proj.lessons_learned,
future_improvements: proj.future_improvements,
github_url: proj.github,
live_url: proj.live,
image_url: proj.image_url,
demo_video_url: proj.demo_video,
screenshots: parseTags(proj.screenshots),
featured: proj.featured === 1 || proj.featured === true,
published: proj.status === 'published',
created_at: proj.date_created,
updated_at: proj.date_updated
};
} catch (error) {
console.error(`Failed to fetch project by slug ${slug} (${locale}):`, error);
return null;
}
}
+41 -32
View File
@@ -61,7 +61,7 @@
"@types/react-responsive-masonry": "^2.6.0", "@types/react-responsive-masonry": "^2.6.0",
"@types/react-syntax-highlighter": "^15.5.11", "@types/react-syntax-highlighter": "^15.5.11",
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.16.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.24",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "^15.5.7", "eslint-config-next": "^15.5.7",
@@ -69,7 +69,7 @@
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"nodemailer-mock": "^2.0.9", "nodemailer-mock": "^2.0.9",
"playwright": "^1.57.0", "playwright": "^1.57.0",
"postcss": "^8.4.49", "postcss": "^8.5.6",
"prisma": "^5.22.0", "prisma": "^5.22.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",
@@ -6961,9 +6961,9 @@
} }
}, },
"node_modules/arg": { "node_modules/arg": {
"version": "5.0.2", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -14527,9 +14527,9 @@
} }
}, },
"node_modules/postcss-load-config": { "node_modules/postcss-load-config": {
"version": "6.0.1", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -14543,28 +14543,21 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lilconfig": "^3.1.1" "lilconfig": "^3.0.0",
"yaml": "^2.3.4"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 14"
}, },
"peerDependencies": { "peerDependencies": {
"jiti": ">=1.21.0",
"postcss": ">=8.0.9", "postcss": ">=8.0.9",
"tsx": "^4.8.1", "ts-node": ">=9.0.0"
"yaml": "^2.4.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"jiti": {
"optional": true
},
"postcss": { "postcss": {
"optional": true "optional": true
}, },
"tsx": { "ts-node": {
"optional": true
},
"yaml": {
"optional": true "optional": true
} }
} }
@@ -16530,9 +16523,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.19", "version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -16544,7 +16537,7 @@
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"glob-parent": "^6.0.2", "glob-parent": "^6.0.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"jiti": "^1.21.7", "jiti": "^1.21.6",
"lilconfig": "^3.1.3", "lilconfig": "^3.1.3",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
@@ -16553,7 +16546,7 @@
"postcss": "^8.4.47", "postcss": "^8.4.47",
"postcss-import": "^15.1.0", "postcss-import": "^15.1.0",
"postcss-js": "^4.0.1", "postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-load-config": "^4.0.2",
"postcss-nested": "^6.2.0", "postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2", "postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8", "resolve": "^1.22.8",
@@ -16567,6 +16560,13 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/tailwindcss/node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/tailwindcss/node_modules/fast-glob": { "node_modules/tailwindcss/node_modules/fast-glob": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -17085,13 +17085,6 @@
} }
} }
}, },
"node_modules/ts-node/node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT"
},
"node_modules/tsconfig-paths": { "node_modules/tsconfig-paths": {
"version": "3.15.0", "version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -18222,6 +18215,22 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+2 -2
View File
@@ -105,7 +105,7 @@
"@types/react-responsive-masonry": "^2.6.0", "@types/react-responsive-masonry": "^2.6.0",
"@types/react-syntax-highlighter": "^15.5.11", "@types/react-syntax-highlighter": "^15.5.11",
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.16.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.24",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "^15.5.7", "eslint-config-next": "^15.5.7",
@@ -113,7 +113,7 @@
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"nodemailer-mock": "^2.0.9", "nodemailer-mock": "^2.0.9",
"playwright": "^1.57.0", "playwright": "^1.57.0",
"postcss": "^8.4.49", "postcss": "^8.5.6",
"prisma": "^5.22.0", "prisma": "^5.22.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",