From f62db692898a6863754f3f50892b0998864021fa Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 01:29:32 +0100 Subject: [PATCH 01/29] perf: fix PageSpeed Insights issues (WebGL errors, bfcache, redirects, a11y) - Add WebGL support detection in ShaderGradientBackground to prevent console errors - Add .catch() fallback to ShaderGradientBackground dynamic import - Remove hardcoded aria-label from consent banner minimize button (fixes label-content-name-mismatch) - Use rewrite instead of redirect for root locale routing (eliminates one redirect hop) - Change n8n API cache headers from no-store to no-cache (enables bfcache) - Add three and @react-three/fiber to optimizePackageImports for better tree-shaking Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/components/ClientProviders.tsx | 2 +- app/components/ConsentBanner.tsx | 2 -- app/components/ShaderGradientBackground.tsx | 16 +++++++++++++++- middleware.ts | 12 ++++++++---- next.config.ts | 6 +++--- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index 93af8e9..b3cc013 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -15,7 +15,7 @@ const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").cat }); const ShaderGradientBackground = dynamic( - () => import("./ShaderGradientBackground"), + () => import("./ShaderGradientBackground").catch(() => ({ default: () => null })), { ssr: false, loading: () => null } ); diff --git a/app/components/ConsentBanner.tsx b/app/components/ConsentBanner.tsx index 22f09dd..1ea4007 100644 --- a/app/components/ConsentBanner.tsx +++ b/app/components/ConsentBanner.tsx @@ -54,8 +54,6 @@ export default function ConsentBanner() { type="button" onClick={() => setMinimized(true)} className="shrink-0 text-xs text-stone-500 hover:text-stone-900 transition-colors" - aria-label="Minimize privacy banner" - title="Minimize" > {s.hide} diff --git a/app/components/ShaderGradientBackground.tsx b/app/components/ShaderGradientBackground.tsx index 8f7240c..0dd4df8 100644 --- a/app/components/ShaderGradientBackground.tsx +++ b/app/components/ShaderGradientBackground.tsx @@ -1,9 +1,23 @@ "use client"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { ShaderGradientCanvas, ShaderGradient } from "@shadergradient/react"; const ShaderGradientBackground = () => { + const [supported, setSupported] = useState(true); + + useEffect(() => { + try { + const canvas = document.createElement("canvas"); + const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); + if (!gl) setSupported(false); + } catch { + setSupported(false); + } + }, []); + + if (!supported) return null; + return (
Date: Wed, 4 Mar 2026 11:13:10 +0100 Subject: [PATCH 02/29] perf: remove framer-motion and lucide-react from critical path - Remove framer-motion from Hero.tsx and HeaderClient.tsx, replace with CSS animations/transitions - Replace lucide-react icons (Menu, X, Mail) with inline SVGs in HeaderClient.tsx - Lazy-load About, Projects, Contact, Footer via dynamic() imports in ClientWrappers.tsx - Defer ShaderGradient/BackgroundBlobs loading via requestIdleCallback in ClientProviders.tsx - Remove AnimatePresence page wrapper that caused full re-renders - Enable experimental.optimizeCss (critters) for critical CSS inlining - Add fadeIn keyframe to Tailwind config for CSS-based animations Homepage JS reduced from 563KB to 438KB (-125KB). Eliminates ~39s main thread work from WebGL init and layout thrashing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/components/ClientProviders.tsx | 65 ++------ app/components/ClientWrappers.tsx | 11 +- app/components/HeaderClient.tsx | 242 +++++++++++++---------------- app/components/Hero.tsx | 72 ++------- next.config.ts | 1 + package-lock.json | 74 +++++++++ package.json | 17 +- tailwind.config.ts | 9 ++ 8 files changed, 235 insertions(+), 256 deletions(-) diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index b3cc013..ffb498d 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -7,7 +7,6 @@ import { ToastProvider } from "@/components/Toast"; import ErrorBoundary from "@/components/ErrorBoundary"; import { ConsentProvider } from "./ConsentProvider"; import { ThemeProvider } from "./ThemeProvider"; -import { motion, AnimatePresence } from "framer-motion"; const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), { ssr: false, @@ -25,66 +24,19 @@ export default function ClientProviders({ children: React.ReactNode; }) { const [mounted, setMounted] = useState(false); - const [is404Page, setIs404Page] = useState(false); const pathname = usePathname(); useEffect(() => { setMounted(true); - // Check if we're on a 404 page by looking for the data attribute or pathname - const check404 = () => { - try { - if (typeof window !== "undefined" && typeof document !== "undefined") { - const has404Component = document.querySelector('[data-404-page]'); - const is404Path = pathname === '/404' || (window.location && (window.location.pathname === '/404' || window.location.pathname.includes('404'))); - setIs404Page(!!has404Component || is404Path); - } - } catch (error) { - // Silently fail - 404 detection is not critical - if (process.env.NODE_ENV === 'development') { - console.warn('Error checking 404 status:', error); - } - } - }; - // Check immediately and after a short delay - try { - check404(); - const timeout = setTimeout(check404, 100); - const interval = setInterval(check404, 500); - return () => { - try { - clearTimeout(timeout); - clearInterval(interval); - } catch { - // Silently fail during cleanup - } - }; - } catch (error) { - // If setup fails, just return empty cleanup - if (process.env.NODE_ENV === 'development') { - console.warn('Error setting up 404 check:', error); - } - return () => {}; - } }, [pathname]); - // Wrap in multiple error boundaries to isolate failures return ( - - - - {children} - - + + {children} @@ -99,13 +51,20 @@ function GatedProviders({ }: { children: React.ReactNode; mounted: boolean; - is404Page: boolean; }) { + // Defer heavy Three.js/WebGL background until after LCP + const [deferredReady, setDeferredReady] = useState(false); + useEffect(() => { + if (!mounted) return; + const id = requestIdleCallback(() => setDeferredReady(true), { timeout: 5000 }); + return () => cancelIdleCallback(id); + }, [mounted]); + return ( - {mounted && } - {mounted && } + {deferredReady && } + {deferredReady && }
{children}
diff --git a/app/components/ClientWrappers.tsx b/app/components/ClientWrappers.tsx index a9566e0..7003119 100644 --- a/app/components/ClientWrappers.tsx +++ b/app/components/ClientWrappers.tsx @@ -6,11 +6,8 @@ */ import { NextIntlClientProvider } from 'next-intl'; +import dynamic from 'next/dynamic'; import Hero from './Hero'; -import About from './About'; -import Projects from './Projects'; -import Contact from './Contact'; -import Footer from './Footer'; import type { HeroTranslations, AboutTranslations, @@ -21,6 +18,12 @@ import type { import enMessages from '@/messages/en.json'; import deMessages from '@/messages/de.json'; +// Lazy-load below-fold sections to reduce initial JS payload +const About = dynamic(() => import('./About'), { ssr: false }); +const Projects = dynamic(() => import('./Projects'), { ssr: false }); +const Contact = dynamic(() => import('./Contact'), { ssr: false }); +const Footer = dynamic(() => import('./Footer'), { ssr: false }); + const messageMap = { en: enMessages, de: deMessages }; function getNormalizedLocale(locale: string): 'en' | 'de' { diff --git a/app/components/HeaderClient.tsx b/app/components/HeaderClient.tsx index d8fd09c..55bef47 100644 --- a/app/components/HeaderClient.tsx +++ b/app/components/HeaderClient.tsx @@ -1,13 +1,22 @@ "use client"; import { useState, useEffect } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { Menu, X, Mail } from "lucide-react"; import { SiGithub, SiLinkedin } from "react-icons/si"; import Link from "next/link"; import { usePathname, useSearchParams } from "next/navigation"; import type { NavTranslations } from "@/types/translations"; +// Inline SVG icons to avoid loading the full lucide-react chunk (~116KB) +const MenuIcon = ({ size = 24 }: { size?: number }) => ( + +); +const XIcon = ({ size = 24 }: { size?: number }) => ( + +); +const MailIcon = ({ size = 20 }: { size?: number }) => ( + +); + interface HeaderClientProps { locale: string; translations: NavTranslations; @@ -44,7 +53,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps href: "https://linkedin.com/in/dkonkol", label: "LinkedIn", }, - { icon: Mail, href: "mailto:contact@dk0.dev", label: "Email" }, + { icon: MailIcon, href: "mailto:contact@dk0.dev", label: "Email" }, ]; const pathWithoutLocale = pathname.replace(new RegExp(`^/${locale}`), "") || ""; @@ -55,53 +64,38 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps return ( <> - +
- - +
dk0 - +
- setIsOpen(!isOpen)} - className="md:hidden p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-colors" + className="md:hidden p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-all hover:scale-105 active:scale-95" aria-label="Toggle menu" > - {isOpen ? : } - - + {isOpen ? : } + +
- + - - {isOpen && ( - setIsOpen(false)} - /> - )} - + {/* Mobile menu overlay */} +
setIsOpen(false)} + /> - - {isOpen && ( - -
-
- setIsOpen(false)} - > - dk0 - - -
+ {/* Mobile menu panel */} +
+
+
+ setIsOpen(false)} + > + dk0 + + +
- - - {/* Language Switcher Mobile */} -
- setIsOpen(false)} - className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-all ${ - locale === "en" - ? "bg-stone-900 text-white" - : "bg-stone-100 text-stone-600 hover:bg-stone-200" - }`} - > - EN - - setIsOpen(false)} - className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-all ${ - locale === "de" - ? "bg-stone-900 text-white" - : "bg-stone-100 text-stone-600 hover:bg-stone-200" - }`} - > - DE - -
- -
+ + + ); + })}
- - )} - +
+
+
); } diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index a6a821d..c6a119c 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -1,6 +1,5 @@ "use client"; -import { motion } from "framer-motion"; import { useLocale, useTranslations } from "next-intl"; import Image from "next/image"; import { useEffect, useState } from "react"; @@ -29,16 +28,8 @@ const Hero = () => {
{/* Liquid Ambient Background */}
- - +
+
@@ -46,45 +37,25 @@ const Hero = () => { {/* Left: Text Content */}
- +
{getLabel("hero.badge", "Student & Self-Hoster")} - +

- + {getLabel("hero.line1", "Building")} - - + + {getLabel("hero.line2", "Stuff.")} - +

{t("description")}

- + {/* Right: The Photo */} - +
Dennis Konkol @@ -116,18 +76,14 @@ const Hero = () => {
dk0.dev
- +
- +
- +
); }; diff --git a/next.config.ts b/next.config.ts index 538efd8..43d83f0 100644 --- a/next.config.ts +++ b/next.config.ts @@ -34,6 +34,7 @@ const nextConfig: NextConfig = { experimental: process.env.NODE_ENV === "production" ? { + optimizeCss: true, optimizePackageImports: ["lucide-react", "framer-motion", "three", "@react-three/fiber"], } : { diff --git a/package-lock.json b/package-lock.json index 925448c..9c90b7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "@types/react-dom": "^19", "@types/sanitize-html": "^2.16.0", "autoprefixer": "^10.4.24", + "critters": "^0.0.23", "cross-env": "^7.0.3", "eslint": "^9", "eslint-config-next": "^15.5.7", @@ -7260,6 +7261,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -7847,6 +7855,22 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, + "node_modules/critters": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.23.tgz", + "integrity": "sha512-/MCsQbuzTPA/ZTOjjyr2Na5o3lRpr8vd0MZE8tMP0OBNg/VrLxWHteVKalQ8KR+fBmUadbJLdoyEz9sT+q84qg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -7910,6 +7934,23 @@ "node": ">=16" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css-to-react-native": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", @@ -7921,6 +7962,19 @@ "postcss-value-parser": "^4.0.2" } }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -13532,6 +13586,19 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -14252,6 +14319,13 @@ } } }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, "node_modules/postcss-nested": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", diff --git a/package.json b/package.json index 22daca0..cce599a 100644 --- a/package.json +++ b/package.json @@ -87,10 +87,10 @@ "three": "^0.183.1" }, "browserslist": [ - "last 2 Chrome versions", - "last 2 Firefox versions", - "last 2 Safari versions", - "last 2 Edge versions" + "chrome >= 100", + "firefox >= 100", + "safari >= 15", + "edge >= 100" ], "devDependencies": { "@eslint/eslintrc": "^3", @@ -106,6 +106,7 @@ "@types/react-dom": "^19", "@types/sanitize-html": "^2.16.0", "autoprefixer": "^10.4.24", + "critters": "^0.0.23", "cross-env": "^7.0.3", "eslint": "^9", "eslint-config-next": "^15.5.7", @@ -121,11 +122,5 @@ "tsx": "^4.20.5", "typescript": "5.9.3", "whatwg-fetch": "^3.6.20" - }, - "browserslist": [ - "chrome >= 100", - "firefox >= 100", - "safari >= 15", - "edge >= 100" - ] + } } diff --git a/tailwind.config.ts b/tailwind.config.ts index 3578171..6564f76 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -11,6 +11,15 @@ export default { ], theme: { extend: { + keyframes: { + fadeIn: { + "0%": { opacity: "0", transform: "translateY(10px)" }, + "100%": { opacity: "1", transform: "translateY(0)" }, + }, + }, + animation: { + "fade-in": "fadeIn 0.5s ease-out", + }, colors: { background: "var(--background)", foreground: "var(--foreground)", From 60ea4e99bec6be3b3a6b311aa69620276d90e290 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 13:00:34 +0100 Subject: [PATCH 03/29] chore: remove Sentry integration Remove @sentry/nextjs and all related files since it was never actively used. - Delete sentry.server.config.ts, sentry.edge.config.ts - Delete sentry-example-page and sentry-example-api routes - Clean up instrumentation.ts, global-error.tsx, middleware.ts - Remove Sentry env vars from env.example and docs - Update CLAUDE.md, copilot-instructions.md, PRODUCTION_READINESS.md Middleware bundle reduced from 86KB to 34.8KB (-51KB). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 3 +- .gitignore | 4 - CLAUDE.md | 3 +- app/api/sentry-example-api/route.ts | 11 - app/global-error.tsx | 13 +- app/sentry-example-page/page.tsx | 81 - docs/PRODUCTION_READINESS.md | 28 +- env.example | 2 - instrumentation-client.ts | 4 +- instrumentation.ts | 13 +- middleware.ts | 4 +- package-lock.json | 2803 +-------------------------- package.json | 1 - sentry.edge.config.ts | 10 - sentry.server.config.ts | 10 - 15 files changed, 97 insertions(+), 2893 deletions(-) delete mode 100644 app/api/sentry-example-api/route.ts delete mode 100644 app/sentry-example-page/page.tsx delete mode 100644 sentry.edge.config.ts delete mode 100644 sentry.server.config.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3975569..ee6c38e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -58,7 +58,7 @@ npm run db:seed # Seed database - **CMS**: Directus (self-hosted, GraphQL, optional) - **Automation**: n8n webhooks (status, chat, hardcover, image generation) - **i18n**: next-intl (EN + DE) -- **Monitoring**: Sentry +- **Monitoring**: Console error logging (development mode only) - **Deployment**: Docker (standalone mode) + Nginx ### Key Directories @@ -200,7 +200,6 @@ REDIS_URL=redis://... ### Optional ```bash -SENTRY_DSN=... NEXT_PUBLIC_BASE_URL=https://dk0.dev ``` diff --git a/.gitignore b/.gitignore index 67ac8d9..3f92137 100644 --- a/.gitignore +++ b/.gitignore @@ -37,10 +37,6 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* -# Sentry -.sentryclirc -sentry.properties - # vercel .vercel diff --git a/CLAUDE.md b/CLAUDE.md index e34561b..569b65b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ Personal portfolio website for Dennis Konkol (dk0.dev). Built with Next.js 15 (A - **CMS**: Directus (self-hosted, REST/GraphQL, optional) - **Automation**: n8n webhooks (status, chat, hardcover, image generation) - **i18n**: next-intl (EN + DE), message files in `messages/` -- **Monitoring**: Sentry +- **Monitoring**: Console error logging (development mode only) - **Deployment**: Docker + Nginx, CI via Gitea Actions ## Commands @@ -122,7 +122,6 @@ DATABASE_URL=postgresql://... # Optional REDIS_URL=redis://... -SENTRY_DSN=... ``` ## Conventions diff --git a/app/api/sentry-example-api/route.ts b/app/api/sentry-example-api/route.ts deleted file mode 100644 index 6958bf4..0000000 --- a/app/api/sentry-example-api/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as Sentry from "@sentry/nextjs"; -import { NextResponse } from "next/server"; - -export const dynamic = "force-dynamic"; - -// A faulty API route to test Sentry's error monitoring -export function GET() { - const testError = new Error("Sentry Example API Route Error"); - Sentry.captureException(testError); - return NextResponse.json({ error: "This is a test error from the API route" }, { status: 500 }); -} diff --git a/app/global-error.tsx b/app/global-error.tsx index ea2998d..bef27c1 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -1,6 +1,5 @@ "use client"; -import * as Sentry from "@sentry/nextjs"; import { useEffect } from "react"; export default function GlobalError({ @@ -11,15 +10,9 @@ export default function GlobalError({ reset: () => void; }) { useEffect(() => { - // Capture exception in Sentry - Sentry.captureException(error); - - // Log error details to console - console.error("Global Error:", error); - console.error("Error Name:", error.name); - console.error("Error Message:", error.message); - console.error("Error Stack:", error.stack); - console.error("Error Digest:", error.digest); + if (process.env.NODE_ENV === "development") { + console.error("Global Error:", error); + } }, [error]); return ( diff --git a/app/sentry-example-page/page.tsx b/app/sentry-example-page/page.tsx deleted file mode 100644 index b739776..0000000 --- a/app/sentry-example-page/page.tsx +++ /dev/null @@ -1,81 +0,0 @@ -"use client"; - -import Head from "next/head"; -import * as Sentry from "@sentry/nextjs"; - -export default function SentryExamplePage() { - return ( -
- - Sentry Onboarding - - - -
-

- Sentry Onboarding -

-

- Get started by sending us a sample error: -

- - -

- Next, look for the error on the{" "} - - Issues Page - -

-

- For more information, see{" "} - - https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -

-
-
- ); -} diff --git a/docs/PRODUCTION_READINESS.md b/docs/PRODUCTION_READINESS.md index a780863..120fab6 100644 --- a/docs/PRODUCTION_READINESS.md +++ b/docs/PRODUCTION_READINESS.md @@ -13,7 +13,7 @@ This document provides an assessment of the portfolio website's production readi - [x] Input sanitization on forms - [x] SQL injection protection (Prisma ORM) - [x] XSS protection via React and sanitize-html -- [x] Error monitoring with Sentry.io +- [x] Error logging in development mode ### Performance - [x] Next.js App Router with Server Components @@ -42,10 +42,8 @@ This document provides an assessment of the portfolio website's production readi - [x] Analytics opt-in (Umami - privacy-friendly) - [x] Data processing documentation - [x] Contact form with consent -- [x] Sentry.io mentioned in privacy policy ### Monitoring & Observability -- [x] Sentry.io error tracking (configured) - [x] Umami analytics (self-hosted, privacy-friendly) - [x] Health check endpoint (`/api/health`) - [x] Logging infrastructure @@ -79,15 +77,6 @@ This document provides an assessment of the portfolio website's production readi - Locations: Hero.tsx, CurrentlyReading.tsx, Projects pages - Benefit: Better performance, automatic optimization -2. **Configure Sentry.io DSN** - - Set `NEXT_PUBLIC_SENTRY_DSN` in production environment - - Set `SENTRY_AUTH_TOKEN` for source map uploads - - Get DSN from: https://sentry.io/settings/dk0/projects/portfolio/keys/ - -3. **Review CSP for Sentry** - - May need to adjust Content-Security-Policy headers to allow Sentry - - Add `connect-src` directive for `*.sentry.io` - ### Medium Priority 1. **Accessibility audit** - Run Lighthouse audit @@ -105,7 +94,6 @@ This document provides an assessment of the portfolio website's production readi ### Low Priority 1. **Enhanced monitoring** - - Custom Sentry contexts for better debugging - Performance metrics dashboard 2. **Advanced features** @@ -123,10 +111,6 @@ Before deploying to production: DATABASE_URL=postgresql://... REDIS_URL=redis://... - # Sentry (Recommended) - NEXT_PUBLIC_SENTRY_DSN=https://...@sentry.io/... - SENTRY_AUTH_TOKEN=... - # Email (Optional) MY_EMAIL=... MY_PASSWORD=... @@ -156,7 +140,6 @@ Before deploying to production: - Test HTTPS redirect 6. **Monitoring** - - Verify Sentry is receiving events - Check Umami analytics tracking - Test health endpoint @@ -200,13 +183,12 @@ The application is production-ready with the following notes: 3. **Performance**: Optimized for production 4. **SEO**: Properly configured for search engines 5. **Privacy**: GDPR-compliant with privacy policy -6. **Monitoring**: Sentry.io configured (needs DSN in production) +6. **Monitoring**: Umami analytics (self-hosted) **Next Steps**: -1. Configure Sentry.io DSN in production environment -2. Replace `` tags with Next.js `` for optimal performance -3. Run final accessibility audit -4. Monitor performance metrics after deployment +1. Replace `` tags with Next.js `` for optimal performance +2. Run final accessibility audit +3. Monitor performance metrics after deployment --- diff --git a/env.example b/env.example index 2c18de9..1adce9e 100644 --- a/env.example +++ b/env.example @@ -48,6 +48,4 @@ PRISMA_AUTO_BASELINE=false # SKIP_PRISMA_MIGRATE=true # Monitoring (optional) -NEXT_PUBLIC_SENTRY_DSN=your-sentry-dsn -SENTRY_AUTH_TOKEN=your-sentry-auth-token LOG_LEVEL=info diff --git a/instrumentation-client.ts b/instrumentation-client.ts index 4b5c479..eea7a12 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -1,2 +1,2 @@ -// Sentry client SDK disabled to reduce bundle size (~400KB). -// To re-enable, restore the @sentry/nextjs import and withSentryConfig in next.config.ts. +// Client-side instrumentation hook for Next.js +// Add any client-side instrumentation here if needed diff --git a/instrumentation.ts b/instrumentation.ts index 964f937..21f8159 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -1,13 +1,4 @@ -import * as Sentry from '@sentry/nextjs'; - export async function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { - await import('./sentry.server.config'); - } - - if (process.env.NEXT_RUNTIME === 'edge') { - await import('./sentry.edge.config'); - } + // Instrumentation hook for Next.js + // Add any server-side instrumentation here if needed } - -export const onRequestError = Sentry.captureRequestError; diff --git a/middleware.ts b/middleware.ts index 5f2f24e..e537f28 100644 --- a/middleware.ts +++ b/middleware.ts @@ -70,9 +70,7 @@ export function middleware(request: NextRequest) { pathname.startsWith("/api/") || pathname === "/api" || pathname.startsWith("/manage") || - pathname.startsWith("/editor") || - pathname === "/sentry-example-page" || - pathname.startsWith("/sentry-example-page/"); + pathname.startsWith("/editor"); // Locale routing for public site pages const responseUrl = request.nextUrl.clone(); diff --git a/package-lock.json b/package-lock.json index 9c90b7a..409ce58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "@next/bundle-analyzer": "^15.1.7", "@prisma/client": "^5.22.0", "@react-three/fiber": "^9.5.0", - "@sentry/nextjs": "^10.36.0", "@shadergradient/react": "^2.4.20", "@swc/helpers": "^0.5.19", "@tiptap/extension-color": "^3.15.3", @@ -94,27 +93,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@apm-js-collab/code-transformer": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.8.2.tgz", - "integrity": "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA==", - "license": "Apache-2.0" - }, - "node_modules/@apm-js-collab/tracing-hooks": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.3.1.tgz", - "integrity": "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==", - "license": "Apache-2.0", - "dependencies": { - "@apm-js-collab/code-transformer": "^0.8.0", - "debug": "^4.4.1", - "module-details-from-path": "^1.0.4" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -129,6 +112,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -138,6 +122,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -168,6 +153,7 @@ "version": "7.29.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -184,6 +170,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -200,6 +187,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -209,6 +197,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -222,6 +211,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -249,6 +239,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -258,6 +249,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -267,6 +259,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -276,6 +269,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -289,6 +283,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -552,6 +547,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -566,6 +562,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -584,6 +581,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1859,23 +1857,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2372,6 +2353,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2382,6 +2364,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2392,32 +2375,24 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2638,519 +2613,6 @@ "node": ">=12.4.0" } }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", - "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.5.1.tgz", - "integrity": "sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz", - "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", - "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.211.0", - "import-in-the-middle": "^2.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.58.0.tgz", - "integrity": "sha512-fjpQtH18J6GxzUZ+cwNhWUpb71u+DzT7rFkg5pLssDGaEber91Y2WNGdpVpwGivfEluMlNMZumzjEqfg8DeKXQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.54.0.tgz", - "integrity": "sha512-43RmbhUhqt3uuPnc16cX6NsxEASEtn8z/cYV8Zpt6EP4p2h9s4FNuJ4Q9BbEQ2C0YlCCB/2crO1ruVz/hWt8fA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/connect": "3.4.38" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.28.0.tgz", - "integrity": "sha512-ExXGBp0sUj8yhm6Znhf9jmuOaGDsYfDES3gswZnKr4MCqoBWQdEFn6EoDdt5u+RdbxQER+t43FoUihEfTSqsjA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.59.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.59.0.tgz", - "integrity": "sha512-pMKV/qnHiW/Q6pmbKkxt0eIhuNEtvJ7sUAyee192HErlr+a1Jx+FZ3WjfmzhQL1geewyGEiPGkmjjAgNY8TgDA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.30.0.tgz", - "integrity": "sha512-n3Cf8YhG7reaj5dncGlRIU7iT40bxPOjsBEA5Bc1a1g6e9Qvb+JFJ7SEiMlPbUw4PBmxE3h40ltE8LZ3zVt6OA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.54.0.tgz", - "integrity": "sha512-8dXMBzzmEdXfH/wjuRvcJnUFeWzZHUnExkmFJ2uPfa31wmpyBCMxO59yr8f/OXXgSogNgi/uPo9KW9H7LMIZ+g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.58.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.58.0.tgz", - "integrity": "sha512-+yWVVY7fxOs3j2RixCbvue8vUuJ1inHxN2q1sduqDB0Wnkr4vOzVKRYl/Zy7B31/dcPS72D9lo/kltdOTBM3bQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.57.0.tgz", - "integrity": "sha512-Os4THbvls8cTQTVA8ApLfZZztuuqGEeqog0XUnyRW7QVF0d/vOVBEcBCk1pazPFmllXGEdNbbat8e2fYIWdFbw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.211.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.211.0.tgz", - "integrity": "sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/instrumentation": "0.211.0", - "@opentelemetry/semantic-conventions": "^1.29.0", - "forwarded-parse": "2.1.2" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.59.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.59.0.tgz", - "integrity": "sha512-875UxzBHWkW+P4Y45SoFM2AR8f8TzBMD8eO7QXGCyFSCUMP5s9vtt/BS8b/r2kqLyaRPK6mLbdnZznK3XzQWvw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/redis-common": "^0.38.2", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.20.0.tgz", - "integrity": "sha512-yJXOuWZROzj7WmYCUiyT27tIfqBrVtl1/TwVbQyWPz7rL0r1Lu7kWjD0PiVeTCIL6CrIZ7M2s8eBxsTAOxbNvw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.30.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.55.0.tgz", - "integrity": "sha512-FtTL5DUx5Ka/8VK6P1VwnlUXPa3nrb7REvm5ddLUIeXXq4tb9pKd+/ThB1xM/IjefkRSN3z8a5t7epYw1JLBJQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.33.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.59.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.59.0.tgz", - "integrity": "sha512-K9o2skADV20Skdu5tG2bogPKiSpXh4KxfLjz6FuqIVvDJNibwSdu5UvyyBzRVp1rQMV6UmoIk6d3PyPtJbaGSg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.36.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0" - } - }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.55.0.tgz", - "integrity": "sha512-FDBfT7yDGcspN0Cxbu/k8A0Pp1Jhv/m7BMTzXGpcb8ENl3tDj/51U65R5lWzUH15GaZA15HQ5A5wtafklxYj7g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.64.0.tgz", - "integrity": "sha512-pFlCJjweTqVp7B220mCvCld1c1eYKZfQt1p3bxSbcReypKLJTwat+wbL2YZoX9jPi5X2O8tTKFEOahO5ehQGsA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.57.0.tgz", - "integrity": "sha512-MthiekrU/BAJc5JZoZeJmo0OTX6ycJMiP6sMOSRTkvz5BrPMYDqaJos0OgsLPL/HpcgHP7eo5pduETuLguOqcg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.57.0.tgz", - "integrity": "sha512-HFS/+FcZ6Q7piM7Il7CzQ4VHhJvGMJWjx7EgCkP5AnTntSN5rb5Xi3TkYJHBKeR27A0QqPlGaCITi93fUDs++Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@types/mysql": "2.15.27" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.57.0.tgz", - "integrity": "sha512-nHSrYAwF7+aV1E1V9yOOP9TchOodb6fjn4gFvdrdQXiRE7cMuffyLLbCZlZd4wsspBzVwOXX8mpURdRserAhNA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@opentelemetry/sql-common": "^0.41.2" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.63.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.63.0.tgz", - "integrity": "sha512-dKm/ODNN3GgIQVlbD6ZPxwRc3kleLf95hrRWXM+l8wYo+vSeXtEpQPT53afEf6VFWDVzJK55VGn8KMLtSve/cg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.34.0", - "@opentelemetry/sql-common": "^0.41.2", - "@types/pg": "8.15.6", - "@types/pg-pool": "2.0.7" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-redis": { - "version": "0.59.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.59.0.tgz", - "integrity": "sha512-JKv1KDDYA2chJ1PC3pLP+Q9ISMQk6h5ey+99mB57/ARk0vQPGZTTEb4h4/JlcEpy7AYT8HIGv7X6l+br03Neeg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/redis-common": "^0.38.2", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.30.0.tgz", - "integrity": "sha512-bZy9Q8jFdycKQ2pAsyuHYUHNmCxCOGdG6eg1Mn75RvQDccq832sU5OWOBnc12EFUELI6icJkhR7+EQKMBam2GA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.33.0", - "@types/tedious": "^4.0.14" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.21.0.tgz", - "integrity": "sha512-gok0LPUOTz2FQ1YJMZzaHcOzDFyT64XJ8M9rNkugk923/p6lDGms/cRW1cqgqp6N6qcd6K6YdVHwPEhnx9BWbw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/semantic-conventions": "^1.24.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.7.0" - } - }, - "node_modules/@opentelemetry/redis-common": { - "version": "0.38.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", - "integrity": "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz", - "integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.1.tgz", - "integrity": "sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.1", - "@opentelemetry/resources": "2.5.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz", - "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sql-common": { - "version": "0.41.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", - "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -3446,16 +2908,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@playwright/test": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", @@ -3546,47 +2998,6 @@ "@prisma/debug": "5.22.0" } }, - "node_modules/@prisma/instrumentation": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-7.2.0.tgz", - "integrity": "sha512-Rh9Z4x5kEj1OdARd7U18AtVrnL6rmLSI0qYShaB4W7Wx5BKbgzndWF+QnuzMb7GLfVdlT5aYCXoPQVYuYtVu0g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.207.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.8" - } - }, - "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { - "version": "0.207.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.207.0.tgz", - "integrity": "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { - "version": "0.207.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.207.0.tgz", - "integrity": "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.207.0", - "import-in-the-middle": "^2.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@react-three/fiber": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", @@ -3710,379 +3121,6 @@ "node": ">= 10" } }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", - "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -4103,473 +3141,6 @@ "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", "license": "MIT" }, - "node_modules/@sentry-internal/browser-utils": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.38.0.tgz", - "integrity": "sha512-UOJtYmdcxHCcV0NPfXFff/a95iXl/E0EhuQ1y0uE0BuZDMupWSF5t2BgC4HaE5Aw3RTjDF3XkSHWoIF6ohy7eA==", - "license": "MIT", - "dependencies": { - "@sentry/core": "10.38.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry-internal/feedback": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.38.0.tgz", - "integrity": "sha512-JXneg9zRftyfy1Fyfc39bBlF/Qd8g4UDublFFkVvdc1S6JQPlK+P6q22DKz3Pc8w3ySby+xlIq/eTu9Pzqi4KA==", - "license": "MIT", - "dependencies": { - "@sentry/core": "10.38.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry-internal/replay": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.38.0.tgz", - "integrity": "sha512-YWIkL6/dnaiQyFiZXJ/nN+NXGv/15z45ia86bE/TMq01CubX/DUOilgsFz0pk2v/pg3tp/U2MskLO9Hz0cnqeg==", - "license": "MIT", - "dependencies": { - "@sentry-internal/browser-utils": "10.38.0", - "@sentry/core": "10.38.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry-internal/replay-canvas": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.38.0.tgz", - "integrity": "sha512-OXWM9jEqNYh4VTvrMu7v+z1anz+QKQ/fZXIZdsO7JTT2lGNZe58UUMeoq386M+Saxen8F9SUH7yTORy/8KI5qw==", - "license": "MIT", - "dependencies": { - "@sentry-internal/replay": "10.38.0", - "@sentry/core": "10.38.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.9.1.tgz", - "integrity": "sha512-0gEoi2Lb54MFYPOmdTfxlNKxI7kCOvNV7gP8lxMXJ7nCazF5OqOOZIVshfWjDLrc0QrSV6XdVvwPV9GDn4wBMg==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@sentry/browser": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.38.0.tgz", - "integrity": "sha512-3phzp1YX4wcQr9mocGWKbjv0jwtuoDBv7+Y6Yfrys/kwyaL84mDLjjQhRf4gL5SX7JdYkhBp4WaiNlR0UC4kTA==", - "license": "MIT", - "dependencies": { - "@sentry-internal/browser-utils": "10.38.0", - "@sentry-internal/feedback": "10.38.0", - "@sentry-internal/replay": "10.38.0", - "@sentry-internal/replay-canvas": "10.38.0", - "@sentry/core": "10.38.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/bundler-plugin-core": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.9.1.tgz", - "integrity": "sha512-moii+w7N8k8WdvkX7qCDY9iRBlhgHlhTHTUQwF2FNMhBHuqlNpVcSJJqJMjFUQcjYMBDrZgxhfKV18bt5ixwlQ==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.18.5", - "@sentry/babel-plugin-component-annotate": "4.9.1", - "@sentry/cli": "^2.57.0", - "dotenv": "^16.3.1", - "find-up": "^5.0.0", - "glob": "^10.5.0", - "magic-string": "0.30.8", - "unplugin": "1.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@sentry/cli": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.4.tgz", - "integrity": "sha512-ArDrpuS8JtDYEvwGleVE+FgR+qHaOp77IgdGSacz6SZy6Lv90uX0Nu4UrHCQJz8/xwIcNxSqnN22lq0dH4IqTg==", - "hasInstallScript": true, - "license": "FSL-1.1-MIT", - "dependencies": { - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.7", - "progress": "^2.0.3", - "proxy-from-env": "^1.1.0", - "which": "^2.0.2" - }, - "bin": { - "sentry-cli": "bin/sentry-cli" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@sentry/cli-darwin": "2.58.4", - "@sentry/cli-linux-arm": "2.58.4", - "@sentry/cli-linux-arm64": "2.58.4", - "@sentry/cli-linux-i686": "2.58.4", - "@sentry/cli-linux-x64": "2.58.4", - "@sentry/cli-win32-arm64": "2.58.4", - "@sentry/cli-win32-i686": "2.58.4", - "@sentry/cli-win32-x64": "2.58.4" - } - }, - "node_modules/@sentry/cli-darwin": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.4.tgz", - "integrity": "sha512-kbTD+P4X8O+nsNwPxCywtj3q22ecyRHWff98rdcmtRrvwz8CKi/T4Jxn/fnn2i4VEchy08OWBuZAqaA5Kh2hRQ==", - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-arm": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.4.tgz", - "integrity": "sha512-rdQ8beTwnN48hv7iV7e7ZKucPec5NJkRdrrycMJMZlzGBPi56LqnclgsHySJ6Kfq506A2MNuQnKGaf/sBC9REA==", - "cpu": [ - "arm" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "linux", - "freebsd", - "android" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-arm64": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.4.tgz", - "integrity": "sha512-0g0KwsOozkLtzN8/0+oMZoOuQ0o7W6O+hx+ydVU1bktaMGKEJLMAWxOQNjsh1TcBbNIXVOKM/I8l0ROhaAb8Ig==", - "cpu": [ - "arm64" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "linux", - "freebsd", - "android" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-i686": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.4.tgz", - "integrity": "sha512-NseoIQAFtkziHyjZNPTu1Gm1opeQHt7Wm1LbLrGWVIRvUOzlslO9/8i6wETUZ6TjlQxBVRgd3Q0lRBG2A8rFYA==", - "cpu": [ - "x86", - "ia32" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "linux", - "freebsd", - "android" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-x64": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.4.tgz", - "integrity": "sha512-d3Arz+OO/wJYTqCYlSN3Ktm+W8rynQ/IMtSZLK8nu0ryh5mJOh+9XlXY6oDXw4YlsM8qCRrNquR8iEI1Y/IH+Q==", - "cpu": [ - "x64" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "linux", - "freebsd", - "android" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-arm64": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.4.tgz", - "integrity": "sha512-bqYrF43+jXdDBh0f8HIJU3tbvlOFtGyRjHB8AoRuMQv9TEDUfENZyCelhdjA+KwDKYl48R1Yasb4EHNzsoO83w==", - "cpu": [ - "arm64" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-i686": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.4.tgz", - "integrity": "sha512-3triFD6jyvhVcXOmGyttf+deKZcC1tURdhnmDUIBkiDPJKGT/N5xa4qAtHJlAB/h8L9jgYih9bvJnvvFVM7yug==", - "cpu": [ - "x86", - "ia32" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-x64": { - "version": "2.58.4", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.4.tgz", - "integrity": "sha512-cSzN4PjM1RsCZ4pxMjI0VI7yNCkxiJ5jmWncyiwHXGiXrV1eXYdQ3n1LhUYLZ91CafyprR0OhDcE+RVZ26Qb5w==", - "cpu": [ - "x64" - ], - "license": "FSL-1.1-MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/core": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.38.0.tgz", - "integrity": "sha512-1pubWDZE5y5HZEPMAZERP4fVl2NH3Ihp1A+vMoVkb3Qc66Diqj1WierAnStlZP7tCx0TBa0dK85GTW/ZFYyB9g==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/nextjs": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-10.38.0.tgz", - "integrity": "sha512-MW2f6mK54jFyS/lmJxT7GWr5d12E+3qvIhR5EdjdyzMX8udSOCGyFJaFIwUfMyEMuggPEvNQVFFpjIrvWXCSGA==", - "license": "MIT", - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/semantic-conventions": "^1.37.0", - "@rollup/plugin-commonjs": "28.0.1", - "@sentry-internal/browser-utils": "10.38.0", - "@sentry/bundler-plugin-core": "^4.8.0", - "@sentry/core": "10.38.0", - "@sentry/node": "10.38.0", - "@sentry/opentelemetry": "10.38.0", - "@sentry/react": "10.38.0", - "@sentry/vercel-edge": "10.38.0", - "@sentry/webpack-plugin": "^4.8.0", - "rollup": "^4.35.0", - "stacktrace-parser": "^0.1.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0" - } - }, - "node_modules/@sentry/node": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.38.0.tgz", - "integrity": "sha512-wriyDtWDAoatn8EhOj0U4PJR1WufiijTsCGALqakOHbFiadtBJANLe6aSkXoXT4tegw59cz1wY4NlzHjYksaPw==", - "license": "MIT", - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.5.0", - "@opentelemetry/core": "^2.5.0", - "@opentelemetry/instrumentation": "^0.211.0", - "@opentelemetry/instrumentation-amqplib": "0.58.0", - "@opentelemetry/instrumentation-connect": "0.54.0", - "@opentelemetry/instrumentation-dataloader": "0.28.0", - "@opentelemetry/instrumentation-express": "0.59.0", - "@opentelemetry/instrumentation-fs": "0.30.0", - "@opentelemetry/instrumentation-generic-pool": "0.54.0", - "@opentelemetry/instrumentation-graphql": "0.58.0", - "@opentelemetry/instrumentation-hapi": "0.57.0", - "@opentelemetry/instrumentation-http": "0.211.0", - "@opentelemetry/instrumentation-ioredis": "0.59.0", - "@opentelemetry/instrumentation-kafkajs": "0.20.0", - "@opentelemetry/instrumentation-knex": "0.55.0", - "@opentelemetry/instrumentation-koa": "0.59.0", - "@opentelemetry/instrumentation-lru-memoizer": "0.55.0", - "@opentelemetry/instrumentation-mongodb": "0.64.0", - "@opentelemetry/instrumentation-mongoose": "0.57.0", - "@opentelemetry/instrumentation-mysql": "0.57.0", - "@opentelemetry/instrumentation-mysql2": "0.57.0", - "@opentelemetry/instrumentation-pg": "0.63.0", - "@opentelemetry/instrumentation-redis": "0.59.0", - "@opentelemetry/instrumentation-tedious": "0.30.0", - "@opentelemetry/instrumentation-undici": "0.21.0", - "@opentelemetry/resources": "^2.5.0", - "@opentelemetry/sdk-trace-base": "^2.5.0", - "@opentelemetry/semantic-conventions": "^1.39.0", - "@prisma/instrumentation": "7.2.0", - "@sentry/core": "10.38.0", - "@sentry/node-core": "10.38.0", - "@sentry/opentelemetry": "10.38.0", - "import-in-the-middle": "^2.0.6", - "minimatch": "^9.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/node-core": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.38.0.tgz", - "integrity": "sha512-ErXtpedrY1HghgwM6AliilZPcUCoNNP1NThdO4YpeMq04wMX9/GMmFCu46TnCcg6b7IFIOSr2S4yD086PxLlHQ==", - "license": "MIT", - "dependencies": { - "@apm-js-collab/tracing-hooks": "^0.3.1", - "@sentry/core": "10.38.0", - "@sentry/opentelemetry": "10.38.0", - "import-in-the-middle": "^2.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/instrumentation": ">=0.57.1 <1", - "@opentelemetry/resources": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", - "@opentelemetry/semantic-conventions": "^1.39.0" - } - }, - "node_modules/@sentry/node/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@sentry/node/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@sentry/opentelemetry": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.38.0.tgz", - "integrity": "sha512-YPVhWfYmC7nD3EJqEHGtjp4fp5LwtAbE5rt9egQ4hqJlYFvr8YEz9sdoqSZxO0cZzgs2v97HFl/nmWAXe52G2Q==", - "license": "MIT", - "dependencies": { - "@sentry/core": "10.38.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", - "@opentelemetry/core": "^1.30.1 || ^2.1.0", - "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", - "@opentelemetry/semantic-conventions": "^1.39.0" - } - }, - "node_modules/@sentry/react": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.38.0.tgz", - "integrity": "sha512-3UiKo6QsqTyPGUt0XWRY9KLaxc/cs6Kz4vlldBSOXEL6qPDL/EfpwNJT61osRo81VFWu8pKu7ZY2bvLPryrnBQ==", - "license": "MIT", - "dependencies": { - "@sentry/browser": "10.38.0", - "@sentry/core": "10.38.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^16.14.0 || 17.x || 18.x || 19.x" - } - }, - "node_modules/@sentry/vercel-edge": { - "version": "10.38.0", - "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-10.38.0.tgz", - "integrity": "sha512-lElDFktj/PyRC/LDHejPFhQmHVMCB9Celj+IHi36aw96a/LekqF6/7vmp26hDtH58QtuiPO3h5voqEAMUOkSlw==", - "license": "MIT", - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/resources": "^2.5.0", - "@sentry/core": "10.38.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/webpack-plugin": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.9.1.tgz", - "integrity": "sha512-Ssx2lHiq8VWywUGd/hmW3U3VYBC0Up7D6UzUiDAWvy18PbTCVszaa54fKMFEQ1yIBg/ePRET53pIzfkcZgifmQ==", - "license": "MIT", - "dependencies": { - "@sentry/bundler-plugin-core": "4.9.1", - "unplugin": "1.0.1", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "webpack": ">=4.40.0" - } - }, "node_modules/@shadergradient/react": { "version": "2.4.20", "resolved": "https://registry.npmjs.org/@shadergradient/react/-/react-2.4.20.tgz", @@ -5477,15 +4048,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5495,28 +4057,6 @@ "@types/ms": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -5640,6 +4180,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -5686,15 +4227,6 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, - "node_modules/@types/mysql": { - "version": "2.15.27", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", - "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "20.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", @@ -5725,26 +4257,6 @@ "@types/node": "*" } }, - "node_modules/@types/pg": { - "version": "8.15.6", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", - "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/pg-pool": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.7.tgz", - "integrity": "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==", - "license": "MIT", - "dependencies": { - "@types/pg": "*" - } - }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -5789,15 +4301,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/tedious": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", - "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -6415,181 +4918,6 @@ "node": ">=16" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0", - "peer": true - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -6621,28 +4949,6 @@ "acorn-walk": "^8.0.2" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -6669,6 +4975,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "4" @@ -6694,48 +5001,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -6756,6 +5021,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6765,6 +5031,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -6787,6 +5054,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -6800,6 +5068,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -7218,6 +5487,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -7244,6 +5514,7 @@ "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -7253,6 +5524,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7283,6 +5555,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -7295,6 +5568,7 @@ "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -7375,6 +5649,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/call-bind": { @@ -7567,6 +5842,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -7591,6 +5867,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7599,16 +5876,6 @@ "node": ">= 6" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -7625,12 +5892,6 @@ "node": ">=8" } }, - "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", - "license": "MIT" - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -7754,6 +6015,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -7801,12 +6063,6 @@ "node": ">= 6" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7818,6 +6074,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/create-jest": { @@ -7894,6 +6151,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8432,16 +6690,11 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "license": "MIT" }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, "license": "ISC" }, "node_modules/emittery": { @@ -8461,22 +6714,9 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, - "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -8616,13 +6856,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT", - "peer": true - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -8729,6 +6962,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9174,6 +7408,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -9186,6 +7421,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -9201,12 +7437,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -9217,16 +7447,6 @@ "node": ">=0.10.0" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -9294,6 +7514,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-equals": { @@ -9349,23 +7570,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -9390,6 +7594,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -9420,6 +7625,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -9432,6 +7638,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -9481,22 +7688,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -9514,12 +7705,6 @@ "node": ">= 6" } }, - "node_modules/forwarded-parse": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", - "license": "MIT" - }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -9637,6 +7822,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -9745,27 +7931,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -9779,37 +7944,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause", - "peer": true - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -9857,6 +7991,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/gzip-size": { @@ -9931,6 +8066,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10138,6 +8274,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, "license": "MIT", "dependencies": { "agent-base": "6", @@ -10232,18 +8369,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-in-the-middle": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", - "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -10425,6 +8550,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -10576,6 +8702,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10663,6 +8790,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -10713,15 +8841,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -10891,6 +9010,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -11007,21 +9127,6 @@ "react": "^19.0.0" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -12039,6 +10144,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -12104,6 +10210,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -12123,6 +10230,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -12143,6 +10251,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -12285,24 +10394,11 @@ "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", "license": "MIT" }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -12355,6 +10451,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -12379,15 +10476,6 @@ "lz-string": "bin/bin.js" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -12636,6 +10724,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, "license": "MIT" }, "node_modules/merge2": { @@ -13121,6 +11210,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13130,6 +11220,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -13181,21 +11272,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "license": "MIT" - }, "node_modules/motion-dom": { "version": "12.34.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.0.tgz", @@ -13292,6 +11368,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, "license": "MIT" }, "node_modules/next": { @@ -13540,6 +11617,7 @@ "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, "license": "MIT" }, "node_modules/nodemailer": { @@ -13568,6 +11646,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13820,6 +11899,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -13835,6 +11915,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -13856,12 +11937,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, "node_modules/pako": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", @@ -13958,6 +12033,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13977,6 +12053,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13989,59 +12066,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", - "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -14372,45 +12396,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -14469,15 +12454,6 @@ "fsevents": "2.3.3" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -14716,12 +12692,6 @@ "prosemirror-transform": "^1.1.0" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -14799,16 +12769,6 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -14902,6 +12862,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -14914,6 +12875,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -15039,29 +13001,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-in-the-middle": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", - "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3" - }, - "engines": { - "node": ">=9.3.0 || >=8.10.0 <9.0.0" - } - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -15154,50 +13093,6 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" - } - }, "node_modules/rope-sequence": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", @@ -15248,27 +13143,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -15372,82 +13246,16 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -15559,6 +13367,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -15571,6 +13380,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15652,18 +13462,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -15699,6 +13497,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -15771,27 +13570,6 @@ "node": ">=8" } }, - "node_modules/stacktrace-parser": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", - "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -15833,56 +13611,6 @@ "node": ">=8" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.codepointat": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", @@ -16016,46 +13744,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -16293,123 +13981,6 @@ "node": ">= 6" } }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true - }, - "node_modules/terser/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -16510,6 +14081,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -17061,18 +14633,6 @@ "node": ">= 4.0.0" } }, - "node_modules/unplugin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz", - "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==", - "license": "MIT", - "dependencies": { - "acorn": "^8.8.1", - "chokidar": "^3.5.3", - "webpack-sources": "^3.2.3", - "webpack-virtual-modules": "^0.5.0" - } - }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -17112,6 +14672,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, "funding": [ { "type": "opencollective", @@ -17196,19 +14757,6 @@ "dev": true, "license": "MIT" }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -17288,20 +14836,6 @@ "makeerror": "1.0.12" } }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "license": "MIT", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -17312,55 +14846,6 @@ "node": ">=12" } }, - "node_modules/webpack": { - "version": "5.105.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", - "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, "node_modules/webpack-bundle-analyzer": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", @@ -17418,45 +14903,6 @@ } } }, - "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-virtual-modules": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", - "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", - "license": "MIT" - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -17505,6 +14951,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -17622,85 +15069,6 @@ "dev": true, "license": "MIT" }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -17767,15 +15135,6 @@ "dev": true, "license": "MIT" }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -17790,6 +15149,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -17886,6 +15246,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index cce599a..f319465 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@next/bundle-analyzer": "^15.1.7", "@prisma/client": "^5.22.0", "@react-three/fiber": "^9.5.0", - "@sentry/nextjs": "^10.36.0", "@shadergradient/react": "^2.4.20", "@swc/helpers": "^0.5.19", "@tiptap/extension-color": "^3.15.3", diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts deleted file mode 100644 index 4fcc40c..0000000 --- a/sentry.edge.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file configures the initialization of Sentry for edge features (middleware, etc). -// The config you add here will be used whenever the server handles a request. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -import * as Sentry from "@sentry/nextjs"; - -Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || "https://148e31ea874c60f3d2e0f70fd6701f6b@o4510751135105024.ingest.de.sentry.io/4510751388926032", - enabled: false, -}); diff --git a/sentry.server.config.ts b/sentry.server.config.ts deleted file mode 100644 index da0c51c..0000000 --- a/sentry.server.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file configures the initialization of Sentry on the server. -// The config you add here will be used whenever the server handles a request. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -import * as Sentry from "@sentry/nextjs"; - -Sentry.init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || "https://148e31ea874c60f3d2e0f70fd6701f6b@o4510751135105024.ingest.de.sentry.io/4510751388926032", - enabled: false, -}); From 9fd530c68fbdc02da85848c08269ed12901269d2 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 14:16:58 +0100 Subject: [PATCH 04/29] perf: convert Hero to server component for faster LCP - Hero now renders server-side, eliminating JS dependency for LCP text - CMS messages fetched server-side instead of client useEffect - Removes Hero from client JS bundle (~5KB less) - LCP element (hero paragraph) now in initial HTML response - Eliminates 2,380ms 'element rendering delay' reported by PSI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/__tests__/components/Hero.test.tsx | 22 +++++++++++------ app/_ui/HomePage.tsx | 2 +- app/_ui/HomePageServer.tsx | 10 +++----- app/components/ClientWrappers.tsx | 19 -------------- app/components/Hero.tsx | 34 +++++++++----------------- 5 files changed, 30 insertions(+), 57 deletions(-) diff --git a/app/__tests__/components/Hero.test.tsx b/app/__tests__/components/Hero.test.tsx index 5f540b7..fd73215 100644 --- a/app/__tests__/components/Hero.test.tsx +++ b/app/__tests__/components/Hero.test.tsx @@ -1,16 +1,21 @@ import { render, screen } from '@testing-library/react'; import Hero from '@/app/components/Hero'; -// Mock next-intl -jest.mock('next-intl', () => ({ - useLocale: () => 'en', - useTranslations: () => (key: string) => { +// Mock next-intl/server +jest.mock('next-intl/server', () => ({ + getTranslations: () => Promise.resolve((key: string) => { const messages: Record = { description: 'Dennis is a student and passionate self-hoster.', - ctaWork: 'View My Work' + ctaWork: 'View My Work', + ctaContact: 'Get in touch', }; return messages[key] || key; - }, + }), +})); + +// Mock directus getMessages +jest.mock('@/lib/directus', () => ({ + getMessages: () => Promise.resolve({}), })); // Mock next/image @@ -36,8 +41,9 @@ jest.mock('next/image', () => ({ })); describe('Hero', () => { - it('renders the hero section correctly', () => { - render(); + it('renders the hero section correctly', async () => { + const HeroResolved = await Hero({ locale: 'en' }); + render(HeroResolved); // Check for the main headlines (defaults in Hero.tsx) expect(screen.getByText('Building')).toBeInTheDocument(); diff --git a/app/_ui/HomePage.tsx b/app/_ui/HomePage.tsx index 6a95652..2cdde49 100644 --- a/app/_ui/HomePage.tsx +++ b/app/_ui/HomePage.tsx @@ -41,7 +41,7 @@ export default function HomePage() { {/* Spacer to prevent navbar overlap */}
- + {/* Wavy Separator 1 - Hero to About */}
diff --git a/app/_ui/HomePageServer.tsx b/app/_ui/HomePageServer.tsx index d2bc7e4..67f150c 100644 --- a/app/_ui/HomePageServer.tsx +++ b/app/_ui/HomePageServer.tsx @@ -1,14 +1,13 @@ import Header from "../components/Header.server"; +import Hero from "../components/Hero"; import Script from "next/script"; import { - getHeroTranslations, getAboutTranslations, getProjectsTranslations, getContactTranslations, getFooterTranslations, } from "@/lib/translations-loader"; import { - HeroClient, AboutClient, ProjectsClient, ContactClient, @@ -20,9 +19,8 @@ interface HomePageServerProps { } export default async function HomePageServer({ locale }: HomePageServerProps) { - // Parallel laden aller Translations - const [heroT, aboutT, projectsT, contactT, footerT] = await Promise.all([ - getHeroTranslations(locale), + // Parallel laden aller Translations (hero translations handled by Hero server component) + const [aboutT, projectsT, contactT, footerT] = await Promise.all([ getAboutTranslations(locale), getProjectsTranslations(locale), getContactTranslations(locale), @@ -57,7 +55,7 @@ export default async function HomePageServer({ locale }: HomePageServerProps) { {/* Spacer to prevent navbar overlap */}
- + {/* Wavy Separator 1 - Hero to About */}
diff --git a/app/components/ClientWrappers.tsx b/app/components/ClientWrappers.tsx index 7003119..20fdbec 100644 --- a/app/components/ClientWrappers.tsx +++ b/app/components/ClientWrappers.tsx @@ -7,9 +7,7 @@ import { NextIntlClientProvider } from 'next-intl'; import dynamic from 'next/dynamic'; -import Hero from './Hero'; import type { - HeroTranslations, AboutTranslations, ProjectsTranslations, ContactTranslations, @@ -30,23 +28,6 @@ function getNormalizedLocale(locale: string): 'en' | 'de' { return locale.startsWith('de') ? 'de' : 'en'; } -export function HeroClient({ locale }: { locale: string; translations: HeroTranslations }) { - const normalLocale = getNormalizedLocale(locale); - const baseMessages = messageMap[normalLocale]; - - const messages = { - home: { - hero: baseMessages.home.hero - } - }; - - return ( - - - - ); -} - export function AboutClient({ locale }: { locale: string; translations: AboutTranslations }) { const normalLocale = getNormalizedLocale(locale); const baseMessages = messageMap[normalLocale]; diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index c6a119c..d00c988 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -1,27 +1,17 @@ -"use client"; - -import { useLocale, useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; import Image from "next/image"; -import { useEffect, useState } from "react"; +import { getMessages } from "@/lib/directus"; -const Hero = () => { - const locale = useLocale(); - const t = useTranslations("home.hero"); - const [cmsMessages, setCmsMessages] = useState>({}); +interface HeroProps { + locale: string; +} - useEffect(() => { - (async () => { - try { - const res = await fetch(`/api/messages?locale=${locale}`); - if (res.ok) { - const data = await res.json(); - setCmsMessages(data.messages || {}); - } - } catch {} - })(); - }, [locale]); +export default async function Hero({ locale }: HeroProps) { + const [t, cmsMessages] = await Promise.all([ + getTranslations("home.hero"), + getMessages(locale).catch(() => ({} as Record)), + ]); - // Helper to get CMS text or fallback const getLabel = (key: string, fallback: string) => cmsMessages[key] || fallback; return ( @@ -86,6 +76,4 @@ const Hero = () => {
); -}; - -export default Hero; +} From 42850ea17c330e37e22c254981f2c3c4552824e8 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 14:38:59 +0100 Subject: [PATCH 05/29] fix: prevent crash loop when database is unreachable Make prisma migrate deploy failure non-fatal in start-with-migrate.js. Previously, migration failure caused process.exit() which killed the container, triggering an infinite restart loop. Now logs a warning and starts the Next.js server anyway (app has DB fallbacks). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/start-with-migrate.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/start-with-migrate.js b/scripts/start-with-migrate.js index e10f5fd..89195ed 100644 --- a/scripts/start-with-migrate.js +++ b/scripts/start-with-migrate.js @@ -134,7 +134,17 @@ async function main() { // If baseline fails we continue to migrate deploy, which will surface the real issue. } } - run("node", ["node_modules/prisma/build/index.js", "migrate", "deploy"]); + + const migrateResult = spawnSync( + "node", + ["node_modules/prisma/build/index.js", "migrate", "deploy"], + { stdio: "inherit", env: process.env } + ); + if (migrateResult.status !== 0) { + console.error( + `[startup] prisma migrate deploy failed (exit ${migrateResult.status}). Starting server anyway...` + ); + } } else { console.log("SKIP_PRISMA_MIGRATE=true -> skipping prisma migrate deploy"); } From 74b73d1b84a3b9bc8816c72a8ae259ec1ba5576b Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 14:45:55 +0100 Subject: [PATCH 06/29] perf: add Docker build cache for Next.js Mount .next/cache as a BuildKit cache volume during build to persist the Next.js build cache across Docker rebuilds. Eliminates the 'No build cache found' warning and speeds up subsequent builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d0e4cc7..d93f686 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,10 +31,10 @@ RUN npx prisma generate # Copy source code (this invalidates cache when code changes) COPY . . -# Build the application +# Build the application (mount cache for faster rebuilds) ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production -RUN npm run build +RUN --mount=type=cache,target=/app/.next/cache npm run build # Verify standalone output was created and show structure for debugging RUN if [ ! -d .next/standalone ]; then \ From 30d0e597c227b17bdaa49cb846a5ccda3fbcdd68 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 15:09:04 +0100 Subject: [PATCH 07/29] fix: use production DB/Redis for dev deployment instead of non-existent dev containers The dev-deploy workflow was trying to spin up separate portfolio_postgres_dev and portfolio_redis_dev containers, which don't exist on the server. Now it reuses the existing production portfolio-postgres and portfolio-redis on the portfolio_net network. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitea/workflows/dev-deploy.yml.disabled | 54 +++++++----------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/.gitea/workflows/dev-deploy.yml.disabled b/.gitea/workflows/dev-deploy.yml.disabled index bc7904b..06d3d26 100644 --- a/.gitea/workflows/dev-deploy.yml.disabled +++ b/.gitea/workflows/dev-deploy.yml.disabled @@ -57,49 +57,27 @@ jobs: # Check for existing container (running or stopped) EXISTING_CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME || echo "") - # Start DB and Redis if not running - echo "🗄️ Starting database and Redis..." - COMPOSE_FILE="docker-compose.dev.minimal.yml" + # Reuse production PostgreSQL and Redis (no separate dev instances needed) + echo "🗄️ Verifying production database and Redis are running..." - # Stop and remove existing containers to ensure clean start with correct architecture - echo "🧹 Cleaning up existing containers..." - docker stop portfolio_postgres_dev portfolio_redis_dev 2>/dev/null || true - docker rm portfolio_postgres_dev portfolio_redis_dev 2>/dev/null || true - - # Remove old images to force re-pull with correct architecture - echo "🔄 Removing old images to force re-pull..." - docker rmi postgres:16-alpine redis:7-alpine 2>/dev/null || true - - # Ensure networks exist before compose starts (network is external) + # Ensure networks exist echo "🌐 Ensuring networks exist..." - docker network create portfolio_dev 2>/dev/null || true + docker network create portfolio_net 2>/dev/null || true docker network create proxy 2>/dev/null || true - # Pull images with correct architecture (Docker will auto-detect) - echo "📥 Pulling images for current architecture..." - docker compose -f $COMPOSE_FILE pull postgres redis - - # Start containers - echo "📦 Starting PostgreSQL and Redis containers..." - docker compose -f $COMPOSE_FILE up -d postgres redis + # Verify production DB is reachable + if docker exec portfolio-postgres pg_isready -U portfolio_user -d portfolio_db >/dev/null 2>&1; then + echo "✅ Production database is ready!" + else + echo "⚠️ Production database not reachable, app will use fallbacks" + fi - # Wait for DB to be ready - echo "⏳ Waiting for database to be ready..." - for i in {1..30}; do - if docker exec portfolio_postgres_dev pg_isready -U portfolio_user -d portfolio_dev >/dev/null 2>&1; then - echo "✅ Database is ready!" - break - fi - echo "⏳ Waiting for database... ($i/30)" - sleep 1 - done - - # Export environment variables + # Export environment variables (pointing to production DB/Redis) export NODE_ENV=production export LOG_LEVEL=${LOG_LEVEL:-debug} export NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL_DEV:-https://dev.dk0.dev} - export DATABASE_URL="postgresql://portfolio_user:portfolio_dev_pass@portfolio_postgres_dev:5432/portfolio_dev?schema=public" - export REDIS_URL="redis://portfolio_redis_dev:6379" + export DATABASE_URL="postgresql://portfolio_user:portfolio_pass@portfolio-postgres:5432/portfolio_db?schema=public" + export REDIS_URL="redis://portfolio-redis:6379" export MY_EMAIL=${MY_EMAIL} export MY_INFO_EMAIL=${MY_INFO_EMAIL} export MY_PASSWORD=${MY_PASSWORD} @@ -202,7 +180,7 @@ jobs: docker run -d \ --name $CONTAINER_NAME \ --restart unless-stopped \ - --network portfolio_dev \ + --network portfolio_net \ -p ${HEALTH_PORT}:3000 \ -e NODE_ENV=production \ -e LOG_LEVEL=${LOG_LEVEL:-debug} \ @@ -268,8 +246,8 @@ jobs: NODE_ENV: production LOG_LEVEL: ${{ vars.LOG_LEVEL || 'debug' }} NEXT_PUBLIC_BASE_URL_DEV: ${{ vars.NEXT_PUBLIC_BASE_URL_DEV || 'https://dev.dk0.dev' }} - DATABASE_URL: postgresql://portfolio_user:portfolio_dev_pass@portfolio_postgres_dev:5432/portfolio_dev?schema=public - REDIS_URL: redis://portfolio_redis_dev:6379 + DATABASE_URL: postgresql://portfolio_user:portfolio_pass@portfolio-postgres:5432/portfolio_db?schema=public + REDIS_URL: redis://portfolio-redis:6379 MY_EMAIL: ${{ vars.MY_EMAIL }} MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} MY_PASSWORD: ${{ secrets.MY_PASSWORD }} From eff17f76d35429c075f87fa604f10fb4f662e347 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 15:10:46 +0100 Subject: [PATCH 08/29] chore: enable dev-deploy workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitea/workflows/{dev-deploy.yml.disabled => dev-deploy.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .gitea/workflows/{dev-deploy.yml.disabled => dev-deploy.yml} (100%) diff --git a/.gitea/workflows/dev-deploy.yml.disabled b/.gitea/workflows/dev-deploy.yml similarity index 100% rename from .gitea/workflows/dev-deploy.yml.disabled rename to .gitea/workflows/dev-deploy.yml From 2db9018477224377c73d5c601ae7da52ca001a59 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 15:13:47 +0100 Subject: [PATCH 09/29] refactor: combine CI and dev-deploy into single workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Job 1 (test-build): lint, test, build — runs on all branches/PRs Job 2 (deploy-dev): Docker build + deploy — only on dev, after tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitea/workflows/ci.yml | 132 ++++++++++++++- .gitea/workflows/dev-deploy.yml | 278 -------------------------------- 2 files changed, 129 insertions(+), 281 deletions(-) delete mode 100644 .gitea/workflows/dev-deploy.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index dce0be2..069a0b2 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Gitea CI +name: CI / CD on: push: @@ -6,7 +6,12 @@ on: pull_request: branches: [main, dev, production] +env: + NODE_VERSION: '25' + DOCKER_IMAGE: portfolio-app + jobs: + # ── Job 1: Lint, Test, Build (runs on every push/PR) ── test-build: runs-on: ubuntu-latest steps: @@ -16,10 +21,10 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '22' + node-version: ${{ env.NODE_VERSION }} cache: 'npm' - - name: Install deps + - name: Install dependencies run: npm ci - name: Lint @@ -30,3 +35,124 @@ jobs: - name: Build run: npm run build + + # ── Job 2: Deploy to dev (only on dev branch, after tests pass) ── + deploy-dev: + needs: test-build + if: github.ref == 'refs/heads/dev' && github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Docker image + run: | + echo "🏗️ Building dev Docker image..." + DOCKER_BUILDKIT=1 docker build \ + --cache-from ${{ env.DOCKER_IMAGE }}:dev \ + --cache-from ${{ env.DOCKER_IMAGE }}:latest \ + -t ${{ env.DOCKER_IMAGE }}:dev \ + . + echo "✅ Docker image built successfully" + + - name: Deploy dev container + run: | + echo "🚀 Starting dev deployment..." + + CONTAINER_NAME="portfolio-app-dev" + HEALTH_PORT="3001" + IMAGE_NAME="${{ env.DOCKER_IMAGE }}:dev" + + # Check for existing container + EXISTING_CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME || echo "") + + # Ensure networks exist + echo "🌐 Ensuring networks exist..." + docker network create portfolio_net 2>/dev/null || true + docker network create proxy 2>/dev/null || true + + # Verify production DB is reachable + if docker exec portfolio-postgres pg_isready -U portfolio_user -d portfolio_db >/dev/null 2>&1; then + echo "✅ Production database is ready!" + else + echo "⚠️ Production database not reachable, app will use fallbacks" + fi + + # Stop and remove existing container + if [ ! -z "$EXISTING_CONTAINER" ]; then + echo "🛑 Stopping existing container..." + docker stop $EXISTING_CONTAINER 2>/dev/null || true + docker rm $EXISTING_CONTAINER 2>/dev/null || true + sleep 3 + fi + + # Ensure port is free + PORT_CONTAINER=$(docker ps -a --format "{{.ID}}\t{{.Ports}}" | grep -E "(:${HEALTH_PORT}->)" | awk '{print $1}' | head -1 || echo "") + if [ ! -z "$PORT_CONTAINER" ]; then + echo "⚠️ Port ${HEALTH_PORT} still in use, freeing..." + docker stop $PORT_CONTAINER 2>/dev/null || true + docker rm $PORT_CONTAINER 2>/dev/null || true + sleep 3 + fi + + # Start new container + echo "🆕 Starting new dev container..." + docker run -d \ + --name $CONTAINER_NAME \ + --restart unless-stopped \ + --network portfolio_net \ + -p ${HEALTH_PORT}:3000 \ + -e NODE_ENV=production \ + -e LOG_LEVEL=${LOG_LEVEL:-debug} \ + -e NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL_DEV:-https://dev.dk0.dev} \ + -e DATABASE_URL="${DATABASE_URL}" \ + -e REDIS_URL="${REDIS_URL}" \ + -e MY_EMAIL="${MY_EMAIL}" \ + -e MY_INFO_EMAIL="${MY_INFO_EMAIL}" \ + -e MY_PASSWORD="${MY_PASSWORD}" \ + -e MY_INFO_PASSWORD="${MY_INFO_PASSWORD}" \ + -e ADMIN_BASIC_AUTH="${ADMIN_BASIC_AUTH}" \ + -e ADMIN_SESSION_SECRET="${ADMIN_SESSION_SECRET}" \ + -e N8N_WEBHOOK_URL="${N8N_WEBHOOK_URL}" \ + -e N8N_SECRET_TOKEN="${N8N_SECRET_TOKEN}" \ + $IMAGE_NAME + + # Connect to proxy network + docker network connect proxy $CONTAINER_NAME 2>/dev/null || true + + # Wait for health + echo "⏳ Waiting for container to be healthy..." + for i in {1..60}; do + if curl -f -s http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then + echo "✅ Dev container is healthy!" + break + fi + HEALTH=$(docker inspect $CONTAINER_NAME --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") + if [ "$HEALTH" == "healthy" ]; then + echo "✅ Docker health check passed!" + break + fi + if [ $i -eq 60 ]; then + echo "⚠️ Health check timed out, showing logs:" + docker logs $CONTAINER_NAME --tail=30 + fi + sleep 2 + done + + echo "✅ Dev deployment completed!" + env: + LOG_LEVEL: ${{ vars.LOG_LEVEL || 'debug' }} + NEXT_PUBLIC_BASE_URL_DEV: ${{ vars.NEXT_PUBLIC_BASE_URL_DEV || 'https://dev.dk0.dev' }} + DATABASE_URL: postgresql://portfolio_user:portfolio_pass@portfolio-postgres:5432/portfolio_db?schema=public + REDIS_URL: redis://portfolio-redis:6379 + MY_EMAIL: ${{ vars.MY_EMAIL }} + MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} + MY_PASSWORD: ${{ secrets.MY_PASSWORD }} + MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} + ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} + ADMIN_SESSION_SECRET: ${{ secrets.ADMIN_SESSION_SECRET }} + N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL || '' }} + N8N_SECRET_TOKEN: ${{ secrets.N8N_SECRET_TOKEN || '' }} + + - name: Cleanup + run: docker image prune -f diff --git a/.gitea/workflows/dev-deploy.yml b/.gitea/workflows/dev-deploy.yml deleted file mode 100644 index 06d3d26..0000000 --- a/.gitea/workflows/dev-deploy.yml +++ /dev/null @@ -1,278 +0,0 @@ -name: Dev Deployment (Zero Downtime) - -on: - push: - branches: [ dev ] - -env: - NODE_VERSION: '25' - DOCKER_IMAGE: portfolio-app - IMAGE_TAG: dev - -jobs: - deploy-dev: - runs-on: ubuntu-latest # Gitea Actions: Use runner with ubuntu-latest label - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run linting - run: npm run lint - continue-on-error: true # Don't block dev deployments on lint errors - - - name: Run tests - run: npm run test - continue-on-error: true # Don't block dev deployments on test failures - - - name: Build application - run: npm run build - - - name: Build Docker image - run: | - echo "🏗️ Building dev Docker image with BuildKit cache..." - DOCKER_BUILDKIT=1 docker build \ - --cache-from ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} \ - --cache-from ${{ env.DOCKER_IMAGE }}:latest \ - -t ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} \ - . - echo "✅ Docker image built successfully" - - - name: Zero-Downtime Dev Deployment - run: | - echo "🚀 Starting zero-downtime dev deployment..." - - CONTAINER_NAME="portfolio-app-dev" - HEALTH_PORT="3001" - IMAGE_NAME="${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }}" - - # Check for existing container (running or stopped) - EXISTING_CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME || echo "") - - # Reuse production PostgreSQL and Redis (no separate dev instances needed) - echo "🗄️ Verifying production database and Redis are running..." - - # Ensure networks exist - echo "🌐 Ensuring networks exist..." - docker network create portfolio_net 2>/dev/null || true - docker network create proxy 2>/dev/null || true - - # Verify production DB is reachable - if docker exec portfolio-postgres pg_isready -U portfolio_user -d portfolio_db >/dev/null 2>&1; then - echo "✅ Production database is ready!" - else - echo "⚠️ Production database not reachable, app will use fallbacks" - fi - - # Export environment variables (pointing to production DB/Redis) - export NODE_ENV=production - export LOG_LEVEL=${LOG_LEVEL:-debug} - export NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL_DEV:-https://dev.dk0.dev} - export DATABASE_URL="postgresql://portfolio_user:portfolio_pass@portfolio-postgres:5432/portfolio_db?schema=public" - export REDIS_URL="redis://portfolio-redis:6379" - export MY_EMAIL=${MY_EMAIL} - export MY_INFO_EMAIL=${MY_INFO_EMAIL} - export MY_PASSWORD=${MY_PASSWORD} - export MY_INFO_PASSWORD=${MY_INFO_PASSWORD} - export ADMIN_BASIC_AUTH=${ADMIN_BASIC_AUTH} - export ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET} - export N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-''} - export N8N_SECRET_TOKEN=${N8N_SECRET_TOKEN:-''} - export PORT=${HEALTH_PORT} - - # Stop and remove existing container if it exists (running or stopped) - if [ ! -z "$EXISTING_CONTAINER" ]; then - echo "🛑 Stopping and removing existing container..." - docker stop $EXISTING_CONTAINER 2>/dev/null || true - docker rm $EXISTING_CONTAINER 2>/dev/null || true - echo "✅ Old container removed" - # Wait for Docker to release the port - echo "⏳ Waiting for Docker to release port ${HEALTH_PORT}..." - sleep 3 - fi - - # Check if port is still in use by Docker containers (check all containers, not just running) - PORT_CONTAINER=$(docker ps -a --format "{{.ID}}\t{{.Names}}\t{{.Ports}}" | grep -E "(:${HEALTH_PORT}->|:${HEALTH_PORT}/)" | awk '{print $1}' | head -1 || echo "") - if [ ! -z "$PORT_CONTAINER" ]; then - echo "⚠️ Port ${HEALTH_PORT} is still in use by container $PORT_CONTAINER" - echo "🛑 Stopping and removing container using port..." - docker stop $PORT_CONTAINER 2>/dev/null || true - docker rm $PORT_CONTAINER 2>/dev/null || true - sleep 3 - fi - - # Also check for any containers with the same name that might be using the port - SAME_NAME_CONTAINER=$(docker ps -a -q -f name=$CONTAINER_NAME | head -1 || echo "") - if [ ! -z "$SAME_NAME_CONTAINER" ] && [ "$SAME_NAME_CONTAINER" != "$EXISTING_CONTAINER" ]; then - echo "⚠️ Found another container with same name: $SAME_NAME_CONTAINER" - docker stop $SAME_NAME_CONTAINER 2>/dev/null || true - docker rm $SAME_NAME_CONTAINER 2>/dev/null || true - sleep 2 - fi - - # Also check if port is in use by another process (non-Docker) - PORT_IN_USE=$(lsof -ti:${HEALTH_PORT} 2>/dev/null || ss -tlnp | grep ":${HEALTH_PORT} " | head -1 || echo "") - if [ ! -z "$PORT_IN_USE" ] && [ -z "$PORT_CONTAINER" ]; then - echo "⚠️ Port ${HEALTH_PORT} is in use by process" - echo "Attempting to free the port..." - # Try to find and kill the process - if command -v lsof >/dev/null 2>&1; then - PID=$(lsof -ti:${HEALTH_PORT} 2>/dev/null || echo "") - if [ ! -z "$PID" ]; then - kill -9 $PID 2>/dev/null || true - sleep 2 - fi - fi - fi - - # Final check: verify port is free and wait if needed - echo "🔍 Verifying port ${HEALTH_PORT} is free..." - MAX_WAIT=10 - WAIT_COUNT=0 - while [ $WAIT_COUNT -lt $MAX_WAIT ]; do - PORT_CHECK=$(docker ps --format "{{.Ports}}" | grep -E "(:${HEALTH_PORT}->|:${HEALTH_PORT}/)" || echo "") - if [ -z "$PORT_CHECK" ]; then - # Also check with lsof/ss if available - if command -v lsof >/dev/null 2>&1; then - PORT_CHECK=$(lsof -ti:${HEALTH_PORT} 2>/dev/null || echo "") - elif command -v ss >/dev/null 2>&1; then - PORT_CHECK=$(ss -tlnp | grep ":${HEALTH_PORT} " || echo "") - fi - fi - if [ -z "$PORT_CHECK" ]; then - echo "✅ Port ${HEALTH_PORT} is free!" - break - fi - WAIT_COUNT=$((WAIT_COUNT + 1)) - echo "⏳ Port still in use, waiting... ($WAIT_COUNT/$MAX_WAIT)" - sleep 1 - done - - # If port is still in use, try alternative port - if [ $WAIT_COUNT -ge $MAX_WAIT ]; then - echo "⚠️ Port ${HEALTH_PORT} is still in use after waiting. Trying alternative port..." - HEALTH_PORT="3002" - echo "🔄 Using alternative port: ${HEALTH_PORT}" - # Quick check if alternative port is also in use - ALT_PORT_CHECK=$(docker ps --format "{{.Ports}}" | grep -E "(:${HEALTH_PORT}->|:${HEALTH_PORT}/)" || echo "") - if [ ! -z "$ALT_PORT_CHECK" ]; then - echo "❌ Alternative port ${HEALTH_PORT} is also in use!" - echo "Attempting to free alternative port..." - ALT_CONTAINER=$(docker ps -a --format "{{.ID}}\t{{.Names}}\t{{.Ports}}" | grep -E "(:${HEALTH_PORT}->|:${HEALTH_PORT}/)" | awk '{print $1}' | head -1 || echo "") - if [ ! -z "$ALT_CONTAINER" ]; then - docker stop $ALT_CONTAINER 2>/dev/null || true - docker rm $ALT_CONTAINER 2>/dev/null || true - sleep 2 - fi - fi - fi - - # Start new container with updated image - echo "🆕 Starting new dev container..." - docker run -d \ - --name $CONTAINER_NAME \ - --restart unless-stopped \ - --network portfolio_net \ - -p ${HEALTH_PORT}:3000 \ - -e NODE_ENV=production \ - -e LOG_LEVEL=${LOG_LEVEL:-debug} \ - -e NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL_DEV:-https://dev.dk0.dev} \ - -e DATABASE_URL=${DATABASE_URL} \ - -e REDIS_URL=${REDIS_URL} \ - -e MY_EMAIL=${MY_EMAIL} \ - -e MY_INFO_EMAIL=${MY_INFO_EMAIL} \ - -e MY_PASSWORD=${MY_PASSWORD} \ - -e MY_INFO_PASSWORD=${MY_INFO_PASSWORD} \ - -e ADMIN_BASIC_AUTH=${ADMIN_BASIC_AUTH} \ - -e ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET} \ - -e N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-''} \ - -e N8N_SECRET_TOKEN=${N8N_SECRET_TOKEN:-''} \ - $IMAGE_NAME - - # Connect container to proxy network as well (for external access) - echo "🔗 Connecting container to proxy network..." - docker network connect proxy $CONTAINER_NAME 2>/dev/null || echo "Container might already be connected to proxy network" - - # Wait for new container to be healthy - echo "⏳ Waiting for new container to be healthy..." - HEALTH_CHECK_PASSED=false - for i in {1..60}; do - NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) - if [ ! -z "$NEW_CONTAINER" ]; then - # Check Docker health status - HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") - if [ "$HEALTH" == "healthy" ]; then - echo "✅ New container is healthy!" - HEALTH_CHECK_PASSED=true - break - fi - # Also check HTTP health endpoint - if curl -f http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then - echo "✅ New container is responding!" - HEALTH_CHECK_PASSED=true - break - fi - fi - echo "⏳ Waiting... ($i/60)" - sleep 2 - done - - # Verify new container is working - if [ "$HEALTH_CHECK_PASSED" != "true" ]; then - echo "⚠️ New dev container health check failed, but continuing (non-blocking)..." - docker logs $CONTAINER_NAME --tail=50 - fi - - # Remove old container if it exists and is different - if [ ! -z "$OLD_CONTAINER" ]; then - NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) - if [ "$OLD_CONTAINER" != "$NEW_CONTAINER" ]; then - echo "🧹 Removing old container..." - docker stop $OLD_CONTAINER 2>/dev/null || true - docker rm $OLD_CONTAINER 2>/dev/null || true - fi - fi - - echo "✅ Dev deployment completed!" - env: - NODE_ENV: production - LOG_LEVEL: ${{ vars.LOG_LEVEL || 'debug' }} - NEXT_PUBLIC_BASE_URL_DEV: ${{ vars.NEXT_PUBLIC_BASE_URL_DEV || 'https://dev.dk0.dev' }} - DATABASE_URL: postgresql://portfolio_user:portfolio_pass@portfolio-postgres:5432/portfolio_db?schema=public - REDIS_URL: redis://portfolio-redis:6379 - MY_EMAIL: ${{ vars.MY_EMAIL }} - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} - MY_PASSWORD: ${{ secrets.MY_PASSWORD }} - MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} - ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} - ADMIN_SESSION_SECRET: ${{ secrets.ADMIN_SESSION_SECRET }} - N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL || '' }} - N8N_SECRET_TOKEN: ${{ secrets.N8N_SECRET_TOKEN || '' }} - - - name: Dev Health Check - run: | - echo "🔍 Running dev health checks..." - for i in {1..20}; do - if curl -f http://localhost:3001/api/health && curl -f http://localhost:3001/ > /dev/null; then - echo "✅ Dev is fully operational!" - exit 0 - fi - echo "⏳ Waiting for dev... ($i/20)" - sleep 3 - done - echo "⚠️ Dev health check failed, but continuing (non-blocking)..." - docker logs portfolio-app-dev --tail=50 - - - name: Cleanup - run: | - echo "🧹 Cleaning up old images..." - docker image prune -f - echo "✅ Cleanup completed" From d80c936c60341bf78475b0b1b95f575bdb455f5d Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 15:23:12 +0100 Subject: [PATCH 10/29] refactor: add production deploy to combined CI/CD workflow All 3 jobs in one file: - test-build: lint, test, build (all branches) - deploy-dev: Docker + deploy (dev only, needs test-build) - deploy-production: Docker + deploy (production only, needs test-build) Removes separate production-deploy.yml. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitea/workflows/ci.yml | 115 ++++++++++ .gitea/workflows/production-deploy.yml | 280 ------------------------- 2 files changed, 115 insertions(+), 280 deletions(-) delete mode 100644 .gitea/workflows/production-deploy.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 069a0b2..481f270 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -156,3 +156,118 @@ jobs: - name: Cleanup run: docker image prune -f + + # ── Job 3: Deploy to production (only on production branch, after tests pass) ── + deploy-production: + needs: test-build + if: github.ref == 'refs/heads/production' && github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Docker image + run: | + echo "🏗️ Building production Docker image..." + DOCKER_BUILDKIT=1 docker build \ + --cache-from ${{ env.DOCKER_IMAGE }}:production \ + --cache-from ${{ env.DOCKER_IMAGE }}:latest \ + -t ${{ env.DOCKER_IMAGE }}:production \ + -t ${{ env.DOCKER_IMAGE }}:latest \ + . + echo "✅ Docker image built successfully" + + - name: Deploy production container + run: | + echo "🚀 Starting production deployment..." + + COMPOSE_FILE="docker-compose.production.yml" + CONTAINER_NAME="portfolio-app" + HEALTH_PORT="3000" + + # Backup current container ID + OLD_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$" || echo "") + + # Ensure network exists + docker network create portfolio_net 2>/dev/null || true + + # Export variables for docker-compose + export N8N_WEBHOOK_URL="${N8N_WEBHOOK_URL}" + export N8N_SECRET_TOKEN="${N8N_SECRET_TOKEN}" + export N8N_API_KEY="${N8N_API_KEY}" + export MY_EMAIL="${MY_EMAIL}" + export MY_INFO_EMAIL="${MY_INFO_EMAIL}" + export MY_PASSWORD="${MY_PASSWORD}" + export MY_INFO_PASSWORD="${MY_INFO_PASSWORD}" + export ADMIN_BASIC_AUTH="${ADMIN_BASIC_AUTH}" + export ADMIN_SESSION_SECRET="${ADMIN_SESSION_SECRET}" + export DIRECTUS_URL="${DIRECTUS_URL}" + export DIRECTUS_STATIC_TOKEN="${DIRECTUS_STATIC_TOKEN}" + + # Start new container via compose + echo "🆕 Starting new production container..." + docker compose -f $COMPOSE_FILE up -d portfolio + + # Wait for health + echo "⏳ Waiting for container to be healthy..." + HEALTH_CHECK_PASSED=false + for i in {1..90}; do + NEW_CONTAINER=$(docker compose -f $COMPOSE_FILE ps -q portfolio 2>/dev/null | head -1) + if [ -z "$NEW_CONTAINER" ]; then + NEW_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$") + fi + if [ ! -z "$NEW_CONTAINER" ]; then + HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") + if [ "$HEALTH" == "healthy" ]; then + echo "✅ Production container is healthy!" + HEALTH_CHECK_PASSED=true + break + fi + if curl -f -s --max-time 2 http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then + echo "✅ Production HTTP health check passed!" + HEALTH_CHECK_PASSED=true + break + fi + fi + if [ $((i % 15)) -eq 0 ]; then + echo "📊 Health: ${HEALTH:-unknown} (attempt $i/90)" + docker compose -f $COMPOSE_FILE logs --tail=5 portfolio 2>/dev/null || true + fi + sleep 2 + done + + if [ "$HEALTH_CHECK_PASSED" != "true" ]; then + echo "❌ Production health check failed!" + docker compose -f $COMPOSE_FILE logs --tail=50 portfolio 2>/dev/null || true + exit 1 + fi + + # Remove old container if different + if [ ! -z "$OLD_CONTAINER" ]; then + NEW_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$") + if [ ! -z "$NEW_CONTAINER" ] && [ "$OLD_CONTAINER" != "$NEW_CONTAINER" ]; then + echo "🧹 Removing old container..." + docker stop $OLD_CONTAINER 2>/dev/null || true + docker rm $OLD_CONTAINER 2>/dev/null || true + fi + fi + + echo "✅ Production deployment completed!" + env: + NODE_ENV: production + LOG_LEVEL: ${{ vars.LOG_LEVEL || 'info' }} + NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL_PRODUCTION || 'https://dk0.dev' }} + MY_EMAIL: ${{ vars.MY_EMAIL }} + MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} + MY_PASSWORD: ${{ secrets.MY_PASSWORD }} + MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} + ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} + ADMIN_SESSION_SECRET: ${{ secrets.ADMIN_SESSION_SECRET }} + N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL || '' }} + N8N_SECRET_TOKEN: ${{ secrets.N8N_SECRET_TOKEN || '' }} + N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }} + DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }} + DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }} + + - name: Cleanup + run: docker image prune -f diff --git a/.gitea/workflows/production-deploy.yml b/.gitea/workflows/production-deploy.yml deleted file mode 100644 index 487a518..0000000 --- a/.gitea/workflows/production-deploy.yml +++ /dev/null @@ -1,280 +0,0 @@ -name: Production Deployment (Zero Downtime) - -on: - push: - branches: [ production ] - -env: - NODE_VERSION: '25' - DOCKER_IMAGE: portfolio-app - IMAGE_TAG: production - -jobs: - deploy-production: - runs-on: ubuntu-latest # Gitea Actions: Use runner with ubuntu-latest label - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run linting and tests in parallel - run: | - npm run lint & - LINT_PID=$! - npm run test:production & - TEST_PID=$! - wait $LINT_PID $TEST_PID - - - name: Build application - run: npm run build - - - name: Build Docker image - run: | - echo "🏗️ Building production Docker image with BuildKit cache..." - DOCKER_BUILDKIT=1 docker build \ - --cache-from ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} \ - --cache-from ${{ env.DOCKER_IMAGE }}:latest \ - -t ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} \ - -t ${{ env.DOCKER_IMAGE }}:latest \ - . - echo "✅ Docker image built successfully" - - - name: Zero-Downtime Production Deployment - run: | - echo "🚀 Starting zero-downtime production deployment..." - - COMPOSE_FILE="docker-compose.production.yml" - CONTAINER_NAME="portfolio-app" - HEALTH_PORT="3000" - - # Backup current container ID if running (exact name match to avoid staging) - OLD_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$" || echo "") - - # Export environment variables for docker-compose - export N8N_WEBHOOK_URL="${{ vars.N8N_WEBHOOK_URL || '' }}" - export N8N_SECRET_TOKEN="${{ secrets.N8N_SECRET_TOKEN || '' }}" - export N8N_API_KEY="${{ vars.N8N_API_KEY || '' }}" - - # Also export other variables that docker-compose needs - export MY_EMAIL="${{ vars.MY_EMAIL }}" - export MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" - export MY_PASSWORD="${{ secrets.MY_PASSWORD }}" - export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" - export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" - export ADMIN_SESSION_SECRET="${{ secrets.ADMIN_SESSION_SECRET }}" - export DIRECTUS_URL="${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}" - export DIRECTUS_STATIC_TOKEN="${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}" - - # Ensure the shared network exists before compose tries to use it - docker network create portfolio_net 2>/dev/null || true - - # Start new container with updated image (docker-compose will handle this) - echo "🆕 Starting new production container..." - echo "📝 Environment check: N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-(not set)}" - docker compose -f $COMPOSE_FILE up -d portfolio - - # Wait for new container to be healthy - echo "⏳ Waiting for new container to be healthy..." - HEALTH_CHECK_PASSED=false - for i in {1..90}; do - # Get the production container ID (exact name match, exclude staging) - # Use compose project to ensure we get the right container - NEW_CONTAINER=$(docker compose -f $COMPOSE_FILE ps -q portfolio 2>/dev/null | head -1) - if [ -z "$NEW_CONTAINER" ]; then - # Fallback: try exact name match with leading slash - NEW_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$") - fi - if [ ! -z "$NEW_CONTAINER" ]; then - # Verify it's actually the production container by checking compose project label - CONTAINER_PROJECT=$(docker inspect $NEW_CONTAINER --format='{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null || echo "") - CONTAINER_SERVICE=$(docker inspect $NEW_CONTAINER --format='{{index .Config.Labels "com.docker.compose.service"}}' 2>/dev/null || echo "") - if [ "$CONTAINER_SERVICE" == "portfolio" ] || [ -z "$CONTAINER_PROJECT" ] || echo "$CONTAINER_PROJECT" | grep -q "portfolio"; then - # Check Docker health status first (most reliable) - HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") - if [ "$HEALTH" == "healthy" ]; then - echo "✅ New container is healthy (Docker health check)!" - # Also verify HTTP endpoint from inside container - if docker exec $NEW_CONTAINER curl -f -s --max-time 5 http://localhost:3000/api/health > /dev/null 2>&1; then - echo "✅ Container HTTP endpoint is also responding!" - HEALTH_CHECK_PASSED=true - break - else - echo "⚠️ Docker health check passed, but HTTP endpoint test failed. Continuing..." - fi - fi - # Try HTTP health endpoint from host (may not work if port not mapped yet) - if curl -f -s --max-time 2 http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then - echo "✅ New container is responding to HTTP health check from host!" - HEALTH_CHECK_PASSED=true - break - fi - # Show container status for debugging - if [ $((i % 10)) -eq 0 ]; then - echo "📊 Container ID: $NEW_CONTAINER" - echo "📊 Container name: $(docker inspect $NEW_CONTAINER --format='{{.Name}}' 2>/dev/null || echo 'unknown')" - echo "📊 Container status: $(docker inspect $NEW_CONTAINER --format='{{.State.Status}}' 2>/dev/null || echo 'unknown')" - echo "📊 Health status: $HEALTH" - echo "📊 Testing from inside container:" - docker exec $NEW_CONTAINER curl -f -s --max-time 2 http://localhost:3000/api/health 2>&1 | head -1 || echo "Container HTTP test failed" - docker compose -f $COMPOSE_FILE logs --tail=5 portfolio 2>/dev/null || true - fi - else - echo "⚠️ Found container but it's not from production compose file (skipping): $NEW_CONTAINER" - fi - fi - echo "⏳ Waiting... ($i/90)" - sleep 2 - done - - # Final verification: Check Docker health status (most reliable) - NEW_CONTAINER=$(docker compose -f $COMPOSE_FILE ps -q portfolio 2>/dev/null | head -1) - if [ -z "$NEW_CONTAINER" ]; then - NEW_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$") - fi - if [ ! -z "$NEW_CONTAINER" ]; then - FINAL_HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "unknown") - if [ "$FINAL_HEALTH" == "healthy" ]; then - echo "✅ Final verification: Container is healthy!" - HEALTH_CHECK_PASSED=true - fi - fi - - # Verify new container is working - if [ "$HEALTH_CHECK_PASSED" != "true" ]; then - echo "❌ New container failed health check!" - echo "📋 All running containers with 'portfolio' in name:" - docker ps --filter "name=portfolio" --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}" - echo "📋 Production container from compose:" - docker compose -f $COMPOSE_FILE ps portfolio 2>/dev/null || echo "No container found via compose" - echo "📋 Container logs:" - docker compose -f $COMPOSE_FILE logs --tail=100 portfolio 2>/dev/null || echo "Could not get logs" - - # Get the correct container ID - NEW_CONTAINER=$(docker compose -f $COMPOSE_FILE ps -q portfolio 2>/dev/null | head -1) - if [ -z "$NEW_CONTAINER" ]; then - NEW_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$") - fi - - if [ ! -z "$NEW_CONTAINER" ]; then - echo "📋 Container inspect (ID: $NEW_CONTAINER):" - docker inspect $NEW_CONTAINER --format='{{.Name}} - {{.State.Status}} - Health: {{.State.Health.Status}}' 2>/dev/null || echo "Container not found" - echo "📋 Testing health endpoint from inside container:" - docker exec $NEW_CONTAINER curl -f -s --max-time 5 http://localhost:3000/api/health 2>&1 || echo "Container HTTP test failed" - - # Check Docker health status - if it's healthy, accept it - FINAL_HEALTH_CHECK=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "unknown") - if [ "$FINAL_HEALTH_CHECK" == "healthy" ]; then - echo "✅ Docker health check reports healthy - accepting deployment!" - HEALTH_CHECK_PASSED=true - else - echo "❌ Docker health check also reports: $FINAL_HEALTH_CHECK" - exit 1 - fi - else - echo "⚠️ Could not find production container!" - exit 1 - fi - fi - - # Remove old container if it exists and is different - if [ ! -z "$OLD_CONTAINER" ]; then - # Get the new production container ID - NEW_CONTAINER=$(docker ps --filter "name=$CONTAINER_NAME" --filter "name=^${CONTAINER_NAME}$" --format "{{.ID}}" | head -1) - if [ -z "$NEW_CONTAINER" ]; then - NEW_CONTAINER=$(docker ps -q -f "name=^/${CONTAINER_NAME}$") - fi - if [ ! -z "$NEW_CONTAINER" ] && [ "$OLD_CONTAINER" != "$NEW_CONTAINER" ]; then - echo "🧹 Removing old container..." - docker stop $OLD_CONTAINER 2>/dev/null || true - docker rm $OLD_CONTAINER 2>/dev/null || true - fi - fi - - echo "✅ Production deployment completed with zero downtime!" - env: - NODE_ENV: production - LOG_LEVEL: ${{ vars.LOG_LEVEL || 'info' }} - NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL_PRODUCTION || 'https://dk0.dev' }} - MY_EMAIL: ${{ vars.MY_EMAIL }} - MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} - MY_PASSWORD: ${{ secrets.MY_PASSWORD }} - MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} - ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} - ADMIN_SESSION_SECRET: ${{ secrets.ADMIN_SESSION_SECRET }} - N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL || '' }} - N8N_SECRET_TOKEN: ${{ secrets.N8N_SECRET_TOKEN || '' }} - N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }} - - - name: Production Health Check - run: | - echo "🔍 Running production health checks..." - COMPOSE_FILE="docker-compose.production.yml" - CONTAINER_NAME="portfolio-app" - - # Get the production container ID - CONTAINER_ID=$(docker compose -f $COMPOSE_FILE ps -q portfolio 2>/dev/null | head -1) - if [ -z "$CONTAINER_ID" ]; then - CONTAINER_ID=$(docker ps -q -f "name=^/${CONTAINER_NAME}$") - fi - - if [ -z "$CONTAINER_ID" ]; then - echo "❌ Production container not found!" - docker ps --filter "name=portfolio" --format "table {{.ID}}\t{{.Names}}\t{{.Status}}" - exit 1 - fi - - echo "📦 Found container: $CONTAINER_ID" - - # Wait for container to be healthy (using Docker's health check) - HEALTH_CHECK_PASSED=false - for i in {1..30}; do - HEALTH=$(docker inspect $CONTAINER_ID --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") - STATUS=$(docker inspect $CONTAINER_ID --format='{{.State.Status}}' 2>/dev/null || echo "unknown") - - if [ "$HEALTH" == "healthy" ] && [ "$STATUS" == "running" ]; then - echo "✅ Container is healthy and running!" - - # Test from inside the container (most reliable) - if docker exec $CONTAINER_ID curl -f -s --max-time 5 http://localhost:3000/api/health > /dev/null 2>&1; then - echo "✅ Health endpoint responds from inside container!" - HEALTH_CHECK_PASSED=true - break - else - echo "⚠️ Container is healthy but HTTP endpoint test failed. Retrying..." - fi - fi - - if [ $((i % 5)) -eq 0 ]; then - echo "📊 Status: $STATUS, Health: $HEALTH (attempt $i/30)" - fi - - echo "⏳ Waiting for production... ($i/30)" - sleep 2 - done - - if [ "$HEALTH_CHECK_PASSED" != "true" ]; then - echo "❌ Production health check failed!" - echo "📋 Container status:" - docker inspect $CONTAINER_ID --format='Name: {{.Name}}, Status: {{.State.Status}}, Health: {{.State.Health.Status}}' 2>/dev/null || echo "Could not inspect container" - echo "📋 Container logs:" - docker compose -f $COMPOSE_FILE logs --tail=50 portfolio 2>/dev/null || docker logs $CONTAINER_ID --tail=50 2>/dev/null || echo "Could not get logs" - echo "📋 Testing from inside container:" - docker exec $CONTAINER_ID curl -v http://localhost:3000/api/health 2>&1 || echo "Container HTTP test failed" - exit 1 - fi - - echo "✅ Production is fully operational!" - - - name: Cleanup - run: | - echo "🧹 Cleaning up old images..." - docker image prune -f - echo "✅ Cleanup completed" From 10a545f014cf856a0979f2dce3aeb084f2df35a5 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 15:40:19 +0100 Subject: [PATCH 11/29] fix: replace img tags with next/image, fix useEffect deps, suppress test mock warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - projects/page.tsx & projects/[slug]/page.tsx: - ActivityFeed.tsx: add allQuotes.length to useEffect deps - Test mocks: eslint-disable for intentional in next/image mocks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/__tests__/components/CurrentlyReading.test.tsx | 1 + app/__tests__/components/Hero.test.tsx | 1 + app/components/ActivityFeed.tsx | 2 +- app/projects/[slug]/page.tsx | 5 ++++- app/projects/page.tsx | 5 ++++- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/__tests__/components/CurrentlyReading.test.tsx b/app/__tests__/components/CurrentlyReading.test.tsx index 8a1eb1e..aac06bb 100644 --- a/app/__tests__/components/CurrentlyReading.test.tsx +++ b/app/__tests__/components/CurrentlyReading.test.tsx @@ -11,6 +11,7 @@ jest.mock("next-intl", () => ({ // Mock next/image jest.mock("next/image", () => ({ __esModule: true, + // eslint-disable-next-line @next/next/no-img-element default: (props: React.ImgHTMLAttributes) => {props.alt, })); diff --git a/app/__tests__/components/Hero.test.tsx b/app/__tests__/components/Hero.test.tsx index fd73215..756585a 100644 --- a/app/__tests__/components/Hero.test.tsx +++ b/app/__tests__/components/Hero.test.tsx @@ -30,6 +30,7 @@ interface ImageProps { jest.mock('next/image', () => ({ __esModule: true, default: ({ src, alt, fill, priority, ...props }: ImageProps) => ( + // eslint-disable-next-line @next/next/no-img-element {alt} diff --git a/app/projects/[slug]/page.tsx b/app/projects/[slug]/page.tsx index d9e6862..307fc18 100644 --- a/app/projects/[slug]/page.tsx +++ b/app/projects/[slug]/page.tsx @@ -2,6 +2,7 @@ import { motion } from 'framer-motion'; import { ExternalLink, Calendar, ArrowLeft, Github as GithubIcon, Share2 } from 'lucide-react'; +import Image from 'next/image'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { useState, useEffect } from 'react'; @@ -132,9 +133,11 @@ const ProjectDetail = () => { className="mb-16 rounded-2xl overflow-hidden shadow-2xl bg-stone-100 aspect-video relative" > {project.imageUrl ? ( - {project.title} ) : ( diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 395f176..c65a427 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { motion } from 'framer-motion'; import { ExternalLink, Github, Calendar, ArrowLeft, Search } from 'lucide-react'; +import Image from 'next/image'; import Link from 'next/link'; import { useLocale, useTranslations } from "next-intl"; @@ -159,9 +160,11 @@ const ProjectsPage = () => {
{project.imageUrl ? ( <> - {project.title}
From 08315433d150d3c6e702b23f89d1c89efdb20453 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 21:37:44 +0100 Subject: [PATCH 12/29] fix: enable SSR for below-fold sections (About, Projects, Contact, Footer) ssr:false caused sections to only render client-side, making them invisible if any JS error occurred. Keep dynamic() for code-splitting but allow server-side rendering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/components/ClientWrappers.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/components/ClientWrappers.tsx b/app/components/ClientWrappers.tsx index 20fdbec..45ea37e 100644 --- a/app/components/ClientWrappers.tsx +++ b/app/components/ClientWrappers.tsx @@ -16,11 +16,11 @@ import type { import enMessages from '@/messages/en.json'; import deMessages from '@/messages/de.json'; -// Lazy-load below-fold sections to reduce initial JS payload -const About = dynamic(() => import('./About'), { ssr: false }); -const Projects = dynamic(() => import('./Projects'), { ssr: false }); -const Contact = dynamic(() => import('./Contact'), { ssr: false }); -const Footer = dynamic(() => import('./Footer'), { ssr: false }); +// Lazy-load below-fold sections (code-split but still SSR) +const About = dynamic(() => import('./About')); +const Projects = dynamic(() => import('./Projects')); +const Contact = dynamic(() => import('./Contact')); +const Footer = dynamic(() => import('./Footer')); const messageMap = { en: enMessages, de: deMessages }; From 9ae6ada0a659c1911ebe879f09653b2d8f33f89c Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 22:02:05 +0100 Subject: [PATCH 13/29] fix: remove dynamic() imports for below-fold sections dynamic() caused Framer Motion's initial opacity:0 to be baked into SSR HTML, but client-side hydration never triggered the animations. Direct imports ensure Framer Motion properly takes over on hydration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/components/ClientWrappers.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/components/ClientWrappers.tsx b/app/components/ClientWrappers.tsx index 45ea37e..5b943eb 100644 --- a/app/components/ClientWrappers.tsx +++ b/app/components/ClientWrappers.tsx @@ -6,7 +6,10 @@ */ import { NextIntlClientProvider } from 'next-intl'; -import dynamic from 'next/dynamic'; +import About from './About'; +import Projects from './Projects'; +import Contact from './Contact'; +import Footer from './Footer'; import type { AboutTranslations, ProjectsTranslations, @@ -16,12 +19,6 @@ import type { import enMessages from '@/messages/en.json'; import deMessages from '@/messages/de.json'; -// Lazy-load below-fold sections (code-split but still SSR) -const About = dynamic(() => import('./About')); -const Projects = dynamic(() => import('./Projects')); -const Contact = dynamic(() => import('./Contact')); -const Footer = dynamic(() => import('./Footer')); - const messageMap = { en: enMessages, de: deMessages }; function getNormalizedLocale(locale: string): 'en' | 'de' { From 5fc323677574fde2f224c9d3a2597fe1bf3170a8 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 23:05:51 +0100 Subject: [PATCH 14/29] fix: remove Framer Motion scroll animations that caused invisible sections Framer Motion's initial={{ opacity: 0 }} was rendered as inline style='opacity:0' in SSR HTML. If client-side JS failed to hydrate properly, sections stayed permanently invisible. Removed whileInView scroll animations from About, Projects, Contact. Modal animations (AnimatePresence) kept as they only render on interaction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/components/About.tsx | 24 ------------------------ app/components/Contact.tsx | 9 --------- app/components/Projects.tsx | 3 --- 3 files changed, 36 deletions(-) diff --git a/app/components/About.tsx b/app/components/About.tsx index 11eeba4..520d059 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -80,9 +80,6 @@ const About = () => { {/* 1. Large Bio Text */}
@@ -113,9 +110,6 @@ const About = () => { {/* 2. Activity / Status Box */} @@ -130,9 +124,6 @@ const About = () => { {/* 3. AI Chat Box */} @@ -147,9 +138,6 @@ const About = () => { {/* 4. Tech Stack */} @@ -186,9 +174,6 @@ const About = () => {
{/* Library - Larger Span */} @@ -211,9 +196,6 @@ const About = () => {
{/* My Gear (Uses) */} @@ -244,9 +226,6 @@ const About = () => { @@ -282,9 +261,6 @@ const About = () => { {/* 6. Hobbies */} diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index 80af0af..35202b0 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -163,9 +163,6 @@ const Contact = () => { {/* Header Card */}
@@ -184,9 +181,6 @@ const Contact = () => { {/* Info Side (Unified Connect Box) */} @@ -252,9 +246,6 @@ const Contact = () => { {/* Form Side */} diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index 1149366..5d04ecd 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -78,9 +78,6 @@ const Projects = () => { projects.map((project) => ( From 77db462c2279959fd3dd85643d432173578b4725 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 23:41:02 +0100 Subject: [PATCH 15/29] fix: add SSR-safe ScrollFadeIn component for scroll animations ScrollFadeIn uses IntersectionObserver + CSS transitions instead of Framer Motion's initial prop. Key difference: no inline style in SSR HTML, so content is visible by default. Animation only activates after client hydration (hasMounted check). Wraps About, Projects, Contact, Footer in HomePageServer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/_ui/HomePageServer.tsx | 17 ++++++++--- app/components/ScrollFadeIn.tsx | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 app/components/ScrollFadeIn.tsx diff --git a/app/_ui/HomePageServer.tsx b/app/_ui/HomePageServer.tsx index 67f150c..f29c486 100644 --- a/app/_ui/HomePageServer.tsx +++ b/app/_ui/HomePageServer.tsx @@ -1,5 +1,6 @@ import Header from "../components/Header.server"; import Hero from "../components/Hero"; +import ScrollFadeIn from "../components/ScrollFadeIn"; import Script from "next/script"; import { getAboutTranslations, @@ -78,7 +79,9 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
- + + + {/* Wavy Separator 2 - About to Projects */}
@@ -101,7 +104,9 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
- + + + {/* Wavy Separator 3 - Projects to Contact */}
@@ -124,9 +129,13 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
- + + +
- + + +
); } diff --git a/app/components/ScrollFadeIn.tsx b/app/components/ScrollFadeIn.tsx new file mode 100644 index 0000000..dd1194e --- /dev/null +++ b/app/components/ScrollFadeIn.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useRef, useEffect, useState, type ReactNode } from "react"; + +interface ScrollFadeInProps { + children: ReactNode; + className?: string; + delay?: number; +} + +/** + * Wraps children in a fade-in-up animation triggered by scroll. + * Unlike Framer Motion's initial={{ opacity: 0 }}, this does NOT + * render opacity:0 in SSR HTML — content is visible by default + * and only hidden after JS hydration for the animation effect. + */ +export default function ScrollFadeIn({ children, className = "", delay = 0 }: ScrollFadeInProps) { + const ref = useRef(null); + const [isVisible, setIsVisible] = useState(false); + const [hasMounted, setHasMounted] = useState(false); + + useEffect(() => { + setHasMounted(true); + const el = ref.current; + if (!el) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsVisible(true); + observer.unobserve(el); + } + }, + { threshold: 0.1 } + ); + + observer.observe(el); + return () => observer.disconnect(); + }, []); + + return ( +
+ {children} +
+ ); +} From 4a8cb5867faebb358e853b18e4d90d96e204d692 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 4 Mar 2026 23:47:17 +0100 Subject: [PATCH 16/29] docs: update copilot instructions with SSR patterns and CI/CD changes - Document ScrollFadeIn pattern and Framer Motion SSR pitfall - Update server/client component architecture section - Reflect combined CI/CD workflow structure - Add accessibility contrast requirements - Streamline commands and conventions sections Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 237 +++++++++----------------------- 1 file changed, 67 insertions(+), 170 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ee6c38e..312206c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,210 +1,107 @@ # Portfolio Project Instructions -This is Dennis Konkol's personal portfolio (dk0.dev) - a Next.js 15 portfolio with Directus CMS integration, n8n automation, and a "liquid" design system. +Dennis Konkol's portfolio (dk0.dev) — Next.js 15, Directus CMS, n8n automation, "Liquid Editorial Bento" design system. ## Build, Test, and Lint -### Development ```bash -npm run dev # Full dev environment (Docker + Next.js) -npm run dev:simple # Next.js only (no Docker dependencies) -npm run dev:next # Plain Next.js dev server +npm run dev:next # Plain Next.js dev server (no Docker) +npm run build # Production build (standalone mode) +npm run lint # ESLint (0 errors required, warnings OK) +npm run lint:fix # Auto-fix lint issues +npm run test # All Jest unit tests +npx jest path/to/test.tsx # Run a single test file +npm run test:watch # Watch mode +npm run test:e2e # Playwright E2E tests +npm run db:generate # Regenerate Prisma client after schema changes ``` -### Build & Deploy -```bash -npm run build # Production build (standalone mode) -npm run start # Start production server -``` +## Architecture -### Testing -```bash -# Unit tests (Jest) -npm run test # Run all unit tests -npm run test:watch # Watch mode -npm run test:coverage # With coverage report +### Server/Client Component Split -# E2E tests (Playwright) -npm run test:e2e # Run all E2E tests -npm run test:e2e:ui # Interactive UI mode -npm run test:critical # Critical paths only -npm run test:hydration # Hydration tests only -``` +The homepage uses a **server component orchestrator** pattern: -### Linting -```bash -npm run lint # Run ESLint -npm run lint:fix # Auto-fix issues -``` +- `app/_ui/HomePageServer.tsx` — async server component, fetches all translations in parallel via `Promise.all`, renders Hero directly, wraps client sections in `ScrollFadeIn` +- `app/components/Hero.tsx` — **server component** (no `"use client"`), uses `getTranslations()` from `next-intl/server` +- `app/components/ClientWrappers.tsx` — exports `AboutClient`, `ProjectsClient`, `ContactClient`, `FooterClient`, each wrapping their component in a scoped `NextIntlClientProvider` with only the needed translation keys +- `app/components/ClientProviders.tsx` — root client wrapper, defers Three.js/WebGL via `requestIdleCallback` (5s timeout) to avoid blocking LCP -### Database (Prisma) -```bash -npm run db:generate # Generate Prisma client -npm run db:push # Push schema to database -npm run db:studio # Open Prisma Studio -npm run db:seed # Seed database -``` +### SSR Animation Safety -## Architecture Overview +**Never use Framer Motion's `initial={{ opacity: 0 }}` on SSR-rendered elements** — it bakes `style="opacity:0"` into HTML, making content invisible if hydration fails. -### Tech Stack -- **Framework**: Next.js 15 (App Router), TypeScript 5.9 -- **Styling**: Tailwind CSS 3.4 with custom `liquid-*` color tokens -- **Theming**: next-themes for dark mode (system/light/dark) -- **Animations**: Framer Motion 12 -- **3D**: Three.js + React Three Fiber (shader gradient background) -- **Database**: PostgreSQL via Prisma ORM -- **Cache**: Redis (optional) -- **CMS**: Directus (self-hosted, GraphQL, optional) -- **Automation**: n8n webhooks (status, chat, hardcover, image generation) -- **i18n**: next-intl (EN + DE) -- **Monitoring**: Console error logging (development mode only) -- **Deployment**: Docker (standalone mode) + Nginx +Use `ScrollFadeIn` component instead (`app/components/ScrollFadeIn.tsx`): renders no inline style during SSR (content visible by default), applies opacity+transform only after `hasMounted` check, animates via IntersectionObserver + CSS transitions. -### Key Directories -``` -app/ - [locale]/ # i18n routes (en, de) - page.tsx # Homepage sections - projects/ # Project listing + detail pages - api/ # API routes - book-reviews/ # Book reviews from Directus - hobbies/ # Hobbies from Directus - n8n/ # n8n webhook proxies - projects/ # Projects (PostgreSQL + Directus) - tech-stack/ # Tech stack from Directus - components/ # React components -lib/ - directus.ts # Directus GraphQL client (no SDK) - auth.ts # Auth + rate limiting - translations-loader.ts # i18n loaders for server components -prisma/ - schema.prisma # Database schema -messages/ - en.json # English translations - de.json # German translations -``` +Framer Motion `AnimatePresence` is fine for modals/overlays that only render after user interaction. ### Data Source Fallback Chain -The architecture prioritizes resilience with this fallback hierarchy: -1. **Directus CMS** (if `DIRECTUS_STATIC_TOKEN` configured) -2. **PostgreSQL** (for projects, analytics) -3. **JSON files** (`messages/*.json`) -4. **Hardcoded defaults** -5. **Display key itself** (last resort) -**Critical**: The site never crashes if external services (Directus, PostgreSQL, n8n, Redis) are unavailable. All API routes return graceful fallbacks. +Every data fetch degrades gracefully — the site never crashes: + +1. **Directus CMS** → 2. **PostgreSQL** → 3. **JSON files** (`messages/*.json`) → 4. **Hardcoded defaults** → 5. **i18n key itself** ### CMS Integration (Directus) -- GraphQL calls via `lib/directus.ts` (no Directus SDK) -- Collections: `tech_stack_categories`, `tech_stack_items`, `hobbies`, `content_pages`, `projects`, `book_reviews` -- Translations use Directus native system (M2O to `languages`) + +- GraphQL via `lib/directus.ts` — no Directus SDK, uses `directusRequest()` with 2s timeout +- Returns `null` on failure (never throws) - Locale mapping: `en` → `en-US`, `de` → `de-DE` -- API routes export `runtime='nodejs'`, `dynamic='force-dynamic'` and include a `source` field in JSON responses (`directus|fallback|error`) +- API routes must export `runtime = 'nodejs'`, `dynamic = 'force-dynamic'`, and return `source` field (`directus|fallback|error`) ### n8n Integration -- Webhook base URL: `N8N_WEBHOOK_URL` env var -- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers -- All endpoints have rate limiting and 10s timeout protection -- Hardcover reading data cached for 5 minutes + +- Webhook proxies in `app/api/n8n/` (status, chat, hardcover, generate-image) +- Auth: `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers +- All endpoints have rate limiting and 10s timeout +- Hardcover reading data cached 5 minutes ## Key Conventions -### i18n (Internationalization) -- **Supported locales**: `en` (English), `de` (German) -- **Primary source**: Static JSON files in `messages/en.json` and `messages/de.json` -- **Optional override**: Directus CMS `messages` collection -- **Server components**: Use `getHeroTranslations()`, `getNavTranslations()`, etc. from `lib/translations-loader.ts` -- **Client components**: Use `useTranslations("key.path")` from next-intl -- **Locale mapping**: Middleware defines `["en", "de"]` which must match `app/[locale]/layout.tsx` +### i18n -### Component Patterns -- **Client components**: Mark with `"use client"` for interactive/data-fetching parts -- **Data loading**: Use `useEffect` for client-side fetching on mount -- **Animations**: Framer Motion `variants` pattern with `staggerContainer` + `fadeInUp` -- **Loading states**: Every async component needs a matching Skeleton component +- Locales: `en`, `de` — defined in `middleware.ts`, must match `app/[locale]/layout.tsx` +- Client components: `useTranslations("key.path")` from `next-intl` +- Server components: `getTranslations("key.path")` from `next-intl/server` +- Always add keys to both `messages/en.json` and `messages/de.json` -### Design System ("Liquid Editorial Bento") -- **Core palette**: Cream (`#fdfcf8`), Stone (`#0c0a09`), Emerald (`#10b981`) -- **Custom colors**: Prefixed with `liquid-*` (sky, mint, lavender, pink, rose, peach, coral, teal, lime) -- **Card style**: Gradient backgrounds (`bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15`) -- **Glassmorphism**: Use `backdrop-blur-sm` with `border-2` and `rounded-xl` -- **Typography**: Headlines uppercase, tracking-tighter, with accent point at end -- **Layout**: Bento Grid for new features (no floating overlays) +### Design System -### File Naming -- **Components**: PascalCase in `app/components/` (e.g., `About.tsx`) -- **API routes**: kebab-case directories in `app/api/` (e.g., `book-reviews/`) -- **Lib utilities**: kebab-case in `lib/` (e.g., `email-obfuscate.ts`) +- Custom Tailwind colors: `liquid-sky`, `liquid-mint`, `liquid-lavender`, `liquid-pink`, `liquid-rose`, `liquid-peach`, `liquid-coral`, `liquid-teal`, `liquid-lime` +- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15` with `backdrop-blur-sm`, `border-2`, `rounded-xl` +- Typography: Headlines uppercase, `tracking-tighter`, accent dot at end (`.`) +- Layout: Bento Grid, no floating overlays +- Accessibility: Use `text-stone-600 dark:text-stone-400` (not `text-stone-400`) for body text — contrast ratio must be ≥4.5:1 ### Code Style -- **Language**: Code in English, user-facing text via i18n -- **TypeScript**: No `any` types - use interfaces from `lib/directus.ts` or `app/_ui/` -- **Error handling**: All API calls must catch errors with fallbacks -- **Error logging**: Only in development mode (`process.env.NODE_ENV === "development"`) -- **Commit messages**: Conventional Commits (`feat:`, `fix:`, `chore:`) -- **No emojis**: Unless explicitly requested -### Testing Notes -- **Jest environment**: JSDOM with mocks for `window.matchMedia` and `IntersectionObserver` -- **Playwright**: Uses plain Next.js dev server (no Docker) with `NODE_ENV=development` to avoid Edge runtime issues -- **Transform**: ESM modules (react-markdown, remark-*, etc.) are transformed via `transformIgnorePatterns` -- **After UI changes**: Run `npm run test` to verify no regressions +- TypeScript: no `any` — use interfaces from `lib/directus.ts` or `types/` +- Error logging: `console.error` only when `process.env.NODE_ENV === "development"` +- File naming: PascalCase components (`About.tsx`), kebab-case API routes (`book-reviews/`), kebab-case lib utils +- Commit messages: Conventional Commits (`feat:`, `fix:`, `chore:`) +- Every async component needs a Skeleton loading state + +### Testing + +- Jest with JSDOM; mocks for `window.matchMedia` and `IntersectionObserver` in `jest.setup.ts` +- ESM modules transformed via `transformIgnorePatterns` (react-markdown, remark-*, etc.) +- Server component tests: `const resolved = await Component({ props }); render(resolved)` +- Test mocks for `next/image`: use `eslint-disable-next-line @next/next/no-img-element` on the `` tag ### Docker & Deployment -- **Standalone mode**: `next.config.ts` uses `output: "standalone"` for optimized Docker builds -- **Branches**: `dev` → staging, `production` → live -- **CI/CD**: Gitea Actions (`.gitea/workflows/`) -- **Verify Docker builds**: Always test Docker builds after changes to `next.config.ts` or dependencies + +- `output: "standalone"` in `next.config.ts` +- Entrypoint: `scripts/start-with-migrate.js` — waits for DB, runs migrations (non-fatal on failure), starts server +- CI/CD: `.gitea/workflows/ci.yml` — `test-build` job (all branches), `deploy-dev` (dev only), `deploy-production` (production only) +- Branches: `dev` → testing.dk0.dev, `production` → dk0.dev +- Dev and production share the same PostgreSQL and Redis instances ## Common Tasks ### Adding a CMS-managed section + 1. Define GraphQL query + types in `lib/directus.ts` 2. Create API route in `app/api//route.ts` with `runtime='nodejs'` and `dynamic='force-dynamic'` -3. Create component in `app/components/.tsx` -4. Add i18n keys to `messages/en.json` and `messages/de.json` -5. Integrate into parent component - -### Adding i18n strings -1. Add keys to both `messages/en.json` and `messages/de.json` -2. Use `useTranslations("key.path")` in client components -3. Use `getTranslations("key.path")` in server components - -### Working with Directus -- All queries go through `directusRequest()` in `lib/directus.ts` -- Uses GraphQL endpoint (`/graphql`) with 2s timeout -- Returns `null` on failure (graceful degradation) -- Translations filtered by `languages_code.code` matching Directus locale - -## Environment Variables - -### Required for CMS -```bash -DIRECTUS_URL=https://cms.dk0.dev -DIRECTUS_STATIC_TOKEN=... -``` - -### Required for n8n features -```bash -N8N_WEBHOOK_URL=https://n8n.dk0.dev -N8N_SECRET_TOKEN=... -N8N_API_KEY=... -``` - -### Database & Cache -```bash -DATABASE_URL=postgresql://... -REDIS_URL=redis://... -``` - -### Optional -```bash -NEXT_PUBLIC_BASE_URL=https://dk0.dev -``` - -## Documentation References -- Operations guide: `docs/OPERATIONS.md` -- Locale system: `docs/LOCALE_SYSTEM.md` -- CMS guide: `docs/CMS_GUIDE.md` -- Testing & deployment: `docs/TESTING_AND_DEPLOYMENT.md` +3. Create component in `app/components/.tsx` with Skeleton loading state +4. Add i18n keys to both `messages/en.json` and `messages/de.json` +5. Create a `Client` wrapper in `ClientWrappers.tsx` with scoped `NextIntlClientProvider` +6. Add to `HomePageServer.tsx` wrapped in `ScrollFadeIn` From 69ae53809b127a3491c609c6e7b21c74f6c2b22d Mon Sep 17 00:00:00 2001 From: denshooter Date: Thu, 5 Mar 2026 19:25:38 +0100 Subject: [PATCH 17/29] =?UTF-8?q?fix:=20Safari=20compatibility=20=E2=80=94?= =?UTF-8?q?=20polyfill=20requestIdleCallback=20and=20IntersectionObserver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit requestIdleCallback is unavailable in Safari < 16.4, causing GatedProviders to crash during hydration and blank the entire page. Added setTimeout fallback. Also added IntersectionObserver fallback in ScrollFadeIn for safety. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/components/ClientProviders.tsx | 11 +++++++++-- app/components/ScrollFadeIn.tsx | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index ffb498d..d9c9eea 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -56,8 +56,15 @@ function GatedProviders({ const [deferredReady, setDeferredReady] = useState(false); useEffect(() => { if (!mounted) return; - const id = requestIdleCallback(() => setDeferredReady(true), { timeout: 5000 }); - return () => cancelIdleCallback(id); + // Safari < 16.4 lacks requestIdleCallback — fall back to setTimeout + let id: ReturnType | number; + if (typeof requestIdleCallback !== "undefined") { + id = requestIdleCallback(() => setDeferredReady(true), { timeout: 5000 }); + return () => cancelIdleCallback(id as number); + } else { + id = setTimeout(() => setDeferredReady(true), 1); + return () => clearTimeout(id); + } }, [mounted]); return ( diff --git a/app/components/ScrollFadeIn.tsx b/app/components/ScrollFadeIn.tsx index dd1194e..ae8ae45 100644 --- a/app/components/ScrollFadeIn.tsx +++ b/app/components/ScrollFadeIn.tsx @@ -24,6 +24,12 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc const el = ref.current; if (!el) return; + // Fallback for browsers without IntersectionObserver + if (typeof IntersectionObserver === "undefined") { + setIsVisible(true); + return; + } + const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { From 7f9d39c2753975e55de73644b0ccda85257c8f50 Mon Sep 17 00:00:00 2001 From: denshooter Date: Thu, 5 Mar 2026 23:40:01 +0100 Subject: [PATCH 18/29] perf: eliminate Three.js/WebGL, fix render-blocking CSS, add dev team agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace ShaderGradientBackground WebGL shader (3 static spheres) with pure CSS radial-gradient divs — moves from ClientProviders (deferred JS) to app/layout.tsx as a server component rendered in initial HTML. Eliminates @shadergradient/react, three, @react-three/fiber from the JS bundle. Removes chunks/7001 (~20s CPU eval) and the 39s main thread block. - Remove optimizeCss/critters: it was converting to a JS-deferred preload, which PageSpeed read as a 410ms sequential CSS chain. Both CSS files now load as parallel tags from initial HTML (~150ms). - Update browserslist safari >= 15 → 15.4 (Array.prototype.at, Object.hasOwn are native in 15.4+; eliminates unnecessary SWC compatibility transforms). - Delete orphaned app/styles/ghostContent.css (never imported anywhere, 3.7KB). - Add .claude/ dev team setup: 5 subagents (frontend-dev, backend-dev, tester, code-reviewer, debugger), 3 skills (/add-section, /review-changes, /check-quality), 3 path-scoped rules, settings.json with auto-lint hook. - Update CLAUDE.md with server/client orchestrator pattern, SSR animation safety rules, API route conventions, and improved command reference. Co-Authored-By: Claude Sonnet 4.6 --- .claude/agents/backend-dev.md | 45 +++++ .claude/agents/code-reviewer.md | 52 ++++++ .claude/agents/debugger.md | 48 ++++++ .claude/agents/frontend-dev.md | 39 +++++ .claude/agents/tester.md | 49 ++++++ .claude/rules/api-routes.md | 35 ++++ .claude/rules/components.md | 37 ++++ .claude/rules/testing.md | 38 ++++ .claude/settings.json | 25 +++ .claude/skills/add-section/SKILL.md | 50 ++++++ .claude/skills/check-quality/SKILL.md | 39 +++++ .claude/skills/review-changes/SKILL.md | 30 ++++ .gitignore | 3 +- CLAUDE.md | 182 +++++++++----------- app/components/ClientProviders.tsx | 15 +- app/components/ShaderGradientBackground.tsx | 160 +++++------------ app/layout.tsx | 2 + app/styles/ghostContent.css | 70 -------- next.config.ts | 30 +--- package.json | 2 +- 20 files changed, 633 insertions(+), 318 deletions(-) create mode 100644 .claude/agents/backend-dev.md create mode 100644 .claude/agents/code-reviewer.md create mode 100644 .claude/agents/debugger.md create mode 100644 .claude/agents/frontend-dev.md create mode 100644 .claude/agents/tester.md create mode 100644 .claude/rules/api-routes.md create mode 100644 .claude/rules/components.md create mode 100644 .claude/rules/testing.md create mode 100644 .claude/settings.json create mode 100644 .claude/skills/add-section/SKILL.md create mode 100644 .claude/skills/check-quality/SKILL.md create mode 100644 .claude/skills/review-changes/SKILL.md delete mode 100644 app/styles/ghostContent.css diff --git a/.claude/agents/backend-dev.md b/.claude/agents/backend-dev.md new file mode 100644 index 0000000..8ec2066 --- /dev/null +++ b/.claude/agents/backend-dev.md @@ -0,0 +1,45 @@ +--- +name: backend-dev +description: Backend API developer for this portfolio. Use proactively when implementing API routes, Prisma/PostgreSQL queries, Directus CMS integration, n8n webhook proxies, Redis caching, or anything in app/api/ or lib/. Handles graceful fallbacks and rate limiting. +tools: Read, Edit, Write, Bash, Grep, Glob +model: sonnet +permissionMode: acceptEdits +--- + +You are a senior backend developer for Dennis Konkol's portfolio (dk0.dev). + +## Stack you own +- **Next.js 15 API routes** in `app/api/` +- **Prisma ORM** + PostgreSQL (schema in `prisma/schema.prisma`) +- **Directus GraphQL** via `lib/directus.ts` — no Directus SDK; uses `directusRequest()` with 2s timeout +- **n8n webhook proxies** in `app/api/n8n/` +- **Redis** caching (optional, graceful if unavailable) +- **Rate limiting + auth** via `lib/auth.ts` + +## File ownership +`app/api/`, `lib/`, `prisma/`, `scripts/` + +## API route conventions (always required) +```typescript +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' +``` +Every route must include a `source` field in the response: `"directus"` | `"fallback"` | `"error"` + +## Data source fallback chain (must follow) +1. Directus CMS (if `DIRECTUS_STATIC_TOKEN` set) → 2. PostgreSQL → 3. `messages/*.json` → 4. Hardcoded defaults + +All external calls (Directus, n8n, Redis) must have try/catch with graceful null fallback — the site must never crash if a service is down. + +## When implementing a feature +1. Read `lib/directus.ts` to check for existing GraphQL query patterns +2. Add GraphQL query + TypeScript types to `lib/directus.ts` for new Directus collections +3. All POST/PUT endpoints need input validation +4. n8n proxies need rate limiting and 10s timeout +5. Error logging: `if (process.env.NODE_ENV === "development") console.error(...)` +6. Run `npm run build` to verify TypeScript compiles without errors +7. After schema changes, run `npm run db:generate` + +## Directus collections +`tech_stack_categories`, `tech_stack_items`, `hobbies`, `content_pages`, `projects`, `book_reviews` +Locale mapping: `en` → `en-US`, `de` → `de-DE` diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 0000000..e95d5a6 --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,52 @@ +--- +name: code-reviewer +description: Expert code reviewer for this portfolio. Use proactively immediately after writing or modifying code. Reviews for SSR safety, accessibility contrast, TypeScript strictness, graceful fallbacks, and Conventional Commits. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +You are a senior code reviewer for Dennis Konkol's portfolio (dk0.dev). You are read-only — you report issues but do not fix them. + +## When invoked +1. Run `git diff HEAD` to see all recent changes +2. For each modified file, read it fully before commenting +3. Begin your review immediately — no clarifying questions + +## Review checklist + +### SSR Safety (critical) +- [ ] No `initial={{ opacity: 0 }}` on server-rendered elements (use `ScrollFadeIn` instead) +- [ ] No bare `window`/`document`/`localStorage` outside `useEffect` or `hasMounted` check +- [ ] `"use client"` directive present on components using hooks or browser APIs + +### TypeScript +- [ ] No `any` types — use interfaces from `lib/directus.ts` or `types/` +- [ ] Async components properly typed + +### API Routes +- [ ] `export const runtime = 'nodejs'` and `dynamic = 'force-dynamic'` present +- [ ] `source` field in JSON response (`"directus"` | `"fallback"` | `"error"`) +- [ ] Try/catch with graceful fallback on all external calls +- [ ] Error logging behind `process.env.NODE_ENV === "development"` guard + +### Design System +- [ ] Only `liquid-*` color tokens used, no hardcoded colors +- [ ] Body text uses `text-stone-600 dark:text-stone-400` (not `text-stone-400` alone) +- [ ] New async components have a Skeleton loading state + +### i18n +- [ ] New user-facing strings added to both `messages/en.json` AND `messages/de.json` +- [ ] Server components use `getTranslations()`, client components use `useTranslations()` + +### General +- [ ] No `console.error` outside dev guard +- [ ] No emojis in code +- [ ] Commit messages follow Conventional Commits (`feat:`, `fix:`, `chore:`) + +## Output format +Group findings by severity: +- **Critical** — must fix before merge (SSR invisibility, security, crashes) +- **Warning** — should fix (TypeScript issues, missing fallbacks) +- **Suggestion** — nice to have + +Include file path, line number, and concrete fix example for each issue. diff --git a/.claude/agents/debugger.md b/.claude/agents/debugger.md new file mode 100644 index 0000000..49c70ec --- /dev/null +++ b/.claude/agents/debugger.md @@ -0,0 +1,48 @@ +--- +name: debugger +description: Debugging specialist for this portfolio. Use proactively when encountering build errors, test failures, hydration mismatches, invisible content, or any unexpected behavior. Specializes in Next.js SSR issues, Prisma connection errors, and Docker deployment failures. +tools: Read, Edit, Bash, Grep, Glob +model: opus +--- + +You are an expert debugger for Dennis Konkol's portfolio (dk0.dev). You specialize in root cause analysis — fix the cause, not the symptom. + +## Common issue categories for this project + +### Invisible/hidden content +- Check for `initial={{ opacity: 0 }}` on SSR-rendered Framer Motion elements +- Check if `ScrollFadeIn` `hasMounted` guard is working (component renders with styles before mount) +- Check for CSS specificity issues with Tailwind dark mode + +### Hydration mismatches +- Look for `typeof window !== "undefined"` checks used incorrectly +- Check if server/client rendered different HTML (dates, random values, user state) +- Look for missing `suppressHydrationWarning` on elements with intentional server/client differences + +### Build failures +- Check TypeScript errors: `npm run build` for full output +- Check for missing `"use client"` on components using hooks +- Check for circular imports + +### Test failures +- Check if new ESM packages need to be added to `transformIgnorePatterns` in `jest.config.ts` +- Verify mocks in `jest.setup.ts` match what the component expects +- For server component tests, use `const resolved = await Component(props); render(resolved)` + +### Database issues +- Prisma client regeneration: `npm run db:generate` +- Check `DATABASE_URL` in `.env.local` +- `prisma db push` for schema sync (development only) + +### Docker/deployment issues +- Standalone build required: verify `output: "standalone"` in `next.config.ts` +- Check `scripts/start-with-migrate.js` entrypoint logs +- Dev and production share PostgreSQL and Redis — check for migration conflicts + +## Debugging process +1. Read the full error including stack trace +2. Run `git log --oneline -5` and `git diff HEAD~1` to check recent changes +3. Form a hypothesis before touching any code +4. Make the minimal fix that addresses the root cause +5. Verify: `npm run build && npm run test` +6. Explain: root cause, fix applied, prevention strategy diff --git a/.claude/agents/frontend-dev.md b/.claude/agents/frontend-dev.md new file mode 100644 index 0000000..d01977d --- /dev/null +++ b/.claude/agents/frontend-dev.md @@ -0,0 +1,39 @@ +--- +name: frontend-dev +description: Frontend React/Next.js developer for this portfolio. Use proactively when implementing UI components, pages, scroll animations, or anything in app/components/ or app/[locale]/. Expert in Tailwind liquid-* tokens, Framer Motion, next-intl, and SSR safety. +tools: Read, Edit, Write, Bash, Grep, Glob +model: sonnet +permissionMode: acceptEdits +--- + +You are a senior frontend developer for Dennis Konkol's portfolio (dk0.dev). + +## Stack you own +- **Next.js 15 App Router** with React 19 and TypeScript (strict — no `any`) +- **Tailwind CSS** using `liquid-*` color tokens only: `liquid-sky`, `liquid-mint`, `liquid-lavender`, `liquid-pink`, `liquid-rose`, `liquid-peach`, `liquid-coral`, `liquid-teal`, `liquid-lime` +- **Framer Motion 12** — variants pattern with `staggerContainer` + `fadeInUp` +- **next-intl** for i18n (always add keys to both `messages/en.json` and `messages/de.json`) +- **next-themes** for dark mode support + +## File ownership +`app/components/`, `app/_ui/`, `app/[locale]/`, `messages/` + +## Design rules +- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15` with `backdrop-blur-sm border-2 rounded-xl` +- Headlines: uppercase, `tracking-tighter`, accent dot at end: `.` +- Body text: `text-stone-600 dark:text-stone-400` — minimum contrast 4.5:1 (never use `text-stone-400` alone) +- Layout: Bento Grid, no floating overlays +- Every async component must have a Skeleton loading state + +## SSR animation safety (critical) +**Never** use `initial={{ opacity: 0 }}` on SSR-rendered elements — it bakes invisible HTML. +Use `ScrollFadeIn` (`app/components/ScrollFadeIn.tsx`) for scroll animations instead. +`AnimatePresence` is fine only for modals/overlays (client-only). + +## When implementing a feature +1. Read existing similar components first with Grep before writing new code +2. Client components need `"use client"` directive +3. Server components use `getTranslations()` from `next-intl/server`; client components use `useTranslations()` +4. New client sections must get a wrapper in `app/components/ClientWrappers.tsx` with scoped `NextIntlClientProvider` +5. Add to `app/_ui/HomePageServer.tsx` wrapped in `` +6. Run `npm run lint` before finishing — 0 errors required diff --git a/.claude/agents/tester.md b/.claude/agents/tester.md new file mode 100644 index 0000000..5c5316d --- /dev/null +++ b/.claude/agents/tester.md @@ -0,0 +1,49 @@ +--- +name: tester +description: Test automation specialist for this portfolio. Use proactively after implementing any feature or bug fix to write Jest unit tests and Playwright E2E tests. Knows all JSDOM quirks and mock patterns specific to this project. +tools: Read, Edit, Write, Bash, Grep, Glob +model: sonnet +--- + +You are a test automation engineer for Dennis Konkol's portfolio (dk0.dev). + +## Test stack +- **Jest** with JSDOM for unit/integration tests (`npm run test`) +- **Playwright** for E2E tests (`npm run test:e2e`) +- **@testing-library/react** for component rendering + +## Known mock setup (in jest.setup.ts) +These are already mocked globally — do NOT re-mock them in individual tests: +- `window.matchMedia` +- `window.IntersectionObserver` +- `NextResponse.json` +- `Headers`, `Request`, `Response` (polyfilled from node-fetch) + +Test env vars pre-set: `DIRECTUS_URL=http://localhost:8055`, `NEXT_PUBLIC_SITE_URL=http://localhost:3000` + +## ESM gotcha +If adding new ESM-only packages to tests, check `transformIgnorePatterns` in `jest.config.ts` — packages like `react-markdown` and `remark-*` need to be listed there. + +## Server component test pattern +```typescript +const resolved = await MyServerComponent({ locale: 'en' }) +render(resolved) +``` + +## `next/image` in tests +Use a simple `` with `eslint-disable-next-line @next/next/no-img-element` — don't try to mock next/image. + +## When writing tests +1. Read the component/function being tested first +2. Identify: happy path, error path, edge cases, SSR rendering +3. Mock ALL external API calls (Directus, n8n, PostgreSQL) +4. Run `npx jest path/to/test.tsx` to verify the specific test passes +5. Run `npm run test` to verify no regressions +6. Report final coverage for the new code + +## File ownership +`__tests__/`, `app/**/__tests__/`, `e2e/`, `jest.config.ts`, `jest.setup.ts` + +## E2E test files +`e2e/critical-paths.spec.ts`, `e2e/hydration.spec.ts`, `e2e/accessibility.spec.ts`, `e2e/performance.spec.ts` +Run specific: `npm run test:critical`, `npm run test:hydration`, `npm run test:accessibility` diff --git a/.claude/rules/api-routes.md b/.claude/rules/api-routes.md new file mode 100644 index 0000000..fff14f8 --- /dev/null +++ b/.claude/rules/api-routes.md @@ -0,0 +1,35 @@ +--- +paths: + - "app/api/**/*.ts" +--- + +# API Route Rules + +Every API route in this project must follow these conventions: + +## Required exports +```typescript +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' +``` + +## Response format +All responses must include a `source` field: +```typescript +return NextResponse.json({ data: ..., source: 'directus' | 'fallback' | 'error' }) +``` + +## Error handling +- Wrap all external calls (Directus, n8n, Redis, PostgreSQL) in try/catch +- Return graceful fallback data on failure — never let an external service crash the page +- Error logging: `if (process.env.NODE_ENV === "development") console.error(...)` + +## n8n proxies (app/api/n8n/) +- Rate limiting required on all public endpoints (use `lib/auth.ts`) +- 10 second timeout on upstream n8n calls +- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers + +## Directus queries +- Use `directusRequest()` from `lib/directus.ts` +- 2 second timeout is already set in `directusRequest()` +- Always have a hardcoded fallback when Directus returns null diff --git a/.claude/rules/components.md b/.claude/rules/components.md new file mode 100644 index 0000000..d7976e9 --- /dev/null +++ b/.claude/rules/components.md @@ -0,0 +1,37 @@ +--- +paths: + - "app/components/**/*.tsx" + - "app/_ui/**/*.tsx" +--- + +# Component Rules + +## SSR animation safety (critical) +**Never** use `initial={{ opacity: 0 }}` on server-rendered elements. +This bakes `style="opacity:0"` into HTML — content is invisible if hydration fails. + +Use `ScrollFadeIn` instead: +```tsx +import ScrollFadeIn from "@/app/components/ScrollFadeIn" + +``` + +`AnimatePresence` is fine for modals and overlays that only appear after user interaction. + +## Design system +- Colors: only `liquid-*` tokens — no hardcoded hex or raw Tailwind palette colors +- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15 backdrop-blur-sm border-2 rounded-xl` +- Headlines: `uppercase tracking-tighter` with accent dot `.` +- Body text: `text-stone-600 dark:text-stone-400` — never `text-stone-400` alone (fails contrast) + +## Async components +Every component that fetches data must have a Skeleton loading state shown while data loads. + +## i18n +- Client: `useTranslations("namespace")` from `next-intl` +- Server: `getTranslations("namespace")` from `next-intl/server` +- New client sections need a wrapper in `ClientWrappers.tsx` with scoped `NextIntlClientProvider` + +## TypeScript +- No `any` — define interfaces in `lib/directus.ts` or `types/` +- No emojis in code diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..01909a5 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,38 @@ +--- +paths: + - "**/__tests__/**/*.ts" + - "**/__tests__/**/*.tsx" + - "**/*.test.ts" + - "**/*.test.tsx" + - "e2e/**/*.spec.ts" +--- + +# Testing Rules + +## Jest environment +- Global mocks are set up in `jest.setup.ts` — do NOT re-mock `matchMedia`, `IntersectionObserver`, or `NextResponse` in individual tests +- Test env vars are pre-set: `DIRECTUS_URL`, `NEXT_PUBLIC_SITE_URL` +- Always mock external API calls (Directus, n8n, PostgreSQL) — tests must work without running services + +## ESM modules +If a new import causes "Must use import to load ES Module" errors, add the package to `transformIgnorePatterns` in `jest.config.ts`. + +## Server component tests +```typescript +// Server components return JSX, not a promise in React 19, but async ones need await +const resolved = await MyServerComponent({ locale: 'en', ...props }) +render(resolved) +``` + +## next/image in tests +Replace `next/image` with a plain `` in test renders: +```tsx +// eslint-disable-next-line @next/next/no-img-element +{alt} +``` + +## Run commands +- Single file: `npx jest path/to/test.tsx` +- All unit tests: `npm run test` +- Watch mode: `npm run test:watch` +- Specific E2E: `npm run test:critical`, `npm run test:hydration`, `npm run test:accessibility` diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..1c584fc --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,25 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "FILE=$(echo $CLAUDE_TOOL_INPUT | jq -r '.file_path // empty'); if [ -n \"$FILE\" ] && echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx)$'; then npx eslint --fix \"$FILE\" 2>/dev/null || true; fi" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "osascript -e 'display notification \"Claude ist fertig\" with title \"Claude Code\" sound name \"Glass\"' 2>/dev/null || true" + } + ] + } + ] + } +} diff --git a/.claude/skills/add-section/SKILL.md b/.claude/skills/add-section/SKILL.md new file mode 100644 index 0000000..473cb71 --- /dev/null +++ b/.claude/skills/add-section/SKILL.md @@ -0,0 +1,50 @@ +--- +name: add-section +description: Orchestrate adding a new CMS-managed section to the portfolio following the full 6-step pattern +context: fork +agent: general-purpose +--- + +Add a new CMS-managed section called "$ARGUMENTS" to the portfolio. + +Follow the exact 6-step pattern from CLAUDE.md: + +**Step 1 — lib/directus.ts** +Read `lib/directus.ts` first, then add: +- TypeScript interface for the new collection +- `directusRequest()` GraphQL query for the collection (with translation support if needed) +- Export the fetch function + +**Step 2 — API Route** +Create `app/api/$ARGUMENTS/route.ts`: +- `export const runtime = 'nodejs'` +- `export const dynamic = 'force-dynamic'` +- Try Directus first, fallback to hardcoded defaults +- Include `source: "directus" | "fallback" | "error"` in response +- Error logging behind `process.env.NODE_ENV === "development"` guard + +**Step 3 — Component** +Create `app/components/$ARGUMENTS.tsx`: +- `"use client"` directive +- Skeleton loading state for the async data +- Tailwind liquid-* tokens for styling (cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15 backdrop-blur-sm border-2 rounded-xl`) +- Headline uppercase with tracking-tighter and emerald accent dot + +**Step 4 — i18n** +Add translation keys to both: +- `messages/en.json` +- `messages/de.json` + +**Step 5 — Client Wrapper** +Add `${ARGUMENTS}Client` to `app/components/ClientWrappers.tsx`: +- Wrap in scoped `NextIntlClientProvider` with only the needed translation namespace + +**Step 6 — Homepage Integration** +Add to `app/_ui/HomePageServer.tsx`: +- Fetch translations in the existing `Promise.all` +- Render wrapped in `` + +After implementation: +- Run `npm run lint` — must be 0 errors +- Run `npm run build` — must compile successfully +- Report what was created and any manual steps remaining (e.g., creating the Directus collection) diff --git a/.claude/skills/check-quality/SKILL.md b/.claude/skills/check-quality/SKILL.md new file mode 100644 index 0000000..517cfc5 --- /dev/null +++ b/.claude/skills/check-quality/SKILL.md @@ -0,0 +1,39 @@ +--- +name: check-quality +description: Run all quality checks (lint, build, tests) and report a summary of the project's health +disable-model-invocation: false +--- + +Run all quality checks for this portfolio project and report the results. + +Execute these checks in order: + +**1. ESLint** +Run: `npm run lint` +Required: 0 errors (warnings OK) + +**2. TypeScript** +Run: `npx tsc --noEmit` +Required: 0 type errors + +**3. Unit Tests** +Run: `npm run test -- --passWithNoTests` +Report: pass/fail count and any failing test names + +**4. Production Build** +Run: `npm run build` +Required: successful completion + +**5. i18n Parity Check** +Compare keys in `messages/en.json` vs `messages/de.json` — report any keys present in one but not the other. + +After all checks, produce a summary table: +| Check | Status | Details | +|-------|--------|---------| +| ESLint | ✓/✗ | ... | +| TypeScript | ✓/✗ | ... | +| Tests | ✓/✗ | X passed, Y failed | +| Build | ✓/✗ | ... | +| i18n parity | ✓/✗ | Missing keys: ... | + +If anything fails, provide the specific error and a recommended fix. diff --git a/.claude/skills/review-changes/SKILL.md b/.claude/skills/review-changes/SKILL.md new file mode 100644 index 0000000..62949f8 --- /dev/null +++ b/.claude/skills/review-changes/SKILL.md @@ -0,0 +1,30 @@ +--- +name: review-changes +description: Run a thorough code review on all recent uncommitted changes using the code-reviewer agent +context: fork +agent: code-reviewer +--- + +Review all recent changes in this repository. + +First gather context: +- Recent changes: !`git diff HEAD` +- Staged changes: !`git diff --cached` +- Modified files: !`git status --short` +- Recent commits: !`git log --oneline -5` + +Then perform a full code review using the code-reviewer agent checklist: +- SSR safety (no `initial={{ opacity: 0 }}` on server elements) +- TypeScript strictness (no `any`) +- API route conventions (`runtime`, `dynamic`, `source` field) +- Design system compliance (liquid-* tokens, contrast ratios) +- i18n completeness (both en.json and de.json) +- Error logging guards +- Graceful fallbacks on all external calls + +Output: +- **Critical** issues (must fix before merge) +- **Warnings** (should fix) +- **Suggestions** (nice to have) + +Include file:line references and concrete fix examples for each issue. diff --git a/.gitignore b/.gitignore index 3f92137..9626976 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Local tooling -.claude/ +.claude/settings.local.json +.claude/CLAUDE.local.md ._* # dependencies diff --git a/CLAUDE.md b/CLAUDE.md index 569b65b..6fb365c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,23 +1,24 @@ -# CLAUDE.md - Portfolio Project Guide +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview -Personal portfolio website for Dennis Konkol (dk0.dev). Built with Next.js 15 (App Router), TypeScript, Tailwind CSS, and Framer Motion. Uses a "liquid" design system with soft gradient colors and glassmorphism effects. +Personal portfolio website for Dennis Konkol (dk0.dev). Built with Next.js 15 (App Router), TypeScript, Tailwind CSS, and Framer Motion. Uses a "Liquid Editorial Bento" design system with soft gradient colors and glassmorphism effects. ## Tech Stack -- **Framework**: Next.js 15 (App Router), TypeScript 5.9 +- **Framework**: Next.js 15 (App Router), TypeScript 5.9, React 19 - **Styling**: Tailwind CSS 3.4 with custom `liquid-*` color tokens - **Theming**: `next-themes` for Dark Mode support (system/light/dark) - **Animations**: Framer Motion 12 -- **3D**: Three.js + React Three Fiber (shader gradient background) +- **3D**: Three.js + React Three Fiber + `@shadergradient/react` (shader gradient background) - **Database**: PostgreSQL via Prisma ORM - **Cache**: Redis (optional) -- **CMS**: Directus (self-hosted, REST/GraphQL, optional) +- **CMS**: Directus (self-hosted, GraphQL only, optional) - **Automation**: n8n webhooks (status, chat, hardcover, image generation) - **i18n**: next-intl (EN + DE), message files in `messages/` -- **Monitoring**: Console error logging (development mode only) -- **Deployment**: Docker + Nginx, CI via Gitea Actions +- **Deployment**: Docker + Nginx, CI via Gitea Actions (`output: "standalone"`) ## Commands @@ -26,76 +27,54 @@ npm run dev # Full dev environment (Docker + Next.js) npm run dev:simple # Next.js only (no Docker) npm run dev:next # Plain Next.js dev server npm run build # Production build -npm run lint # ESLint -npm run test # Jest unit tests +npm run lint # ESLint (0 errors required, warnings OK) +npm run lint:fix # Auto-fix lint issues +npm run test # All Jest unit tests +npx jest path/to/test.tsx # Run a single test file +npm run test:watch # Watch mode npm run test:e2e # Playwright E2E tests +npm run db:generate # Regenerate Prisma client after schema changes ``` -## Project Structure +## Architecture -``` -app/ - [locale]/ # i18n routes (en, de) - page.tsx # Homepage (hero, about, projects, contact) - projects/ # Project listing + detail pages - api/ # API routes - book-reviews/ # Book reviews from Directus CMS - content/ # CMS content pages - hobbies/ # Hobbies from Directus - n8n/ # n8n webhook proxies - hardcover/ # Currently reading (Hardcover API via n8n) - status/ # Activity status (coding, music, gaming) - chat/ # AI chatbot - generate-image/ # AI image generation - projects/ # Projects API (PostgreSQL + Directus fallback) - tech-stack/ # Tech stack from Directus - components/ # React components - About.tsx # About section (tech stack, hobbies, books) - CurrentlyReading.tsx # Currently reading widget (n8n/Hardcover) - ReadBooks.tsx # Read books with ratings (Directus CMS) - Projects.tsx # Featured projects section - Hero.tsx # Hero section - Contact.tsx # Contact form -lib/ - directus.ts # Directus GraphQL client (no SDK) - auth.ts # Auth utilities + rate limiting -prisma/ - schema.prisma # Database schema -messages/ - en.json # English translations - de.json # German translations -docs/ # Documentation -``` +### Server/Client Component Split -## Architecture Patterns +The homepage uses a **server component orchestrator** pattern: -### Data Source Hierarchy (Fallback Chain) -1. Directus CMS (if configured via `DIRECTUS_STATIC_TOKEN`) -2. PostgreSQL (for projects, analytics) -3. JSON files (`messages/*.json`) -4. Hardcoded defaults -5. Display key itself as last resort +- `app/_ui/HomePageServer.tsx` — async server component, fetches all translations in parallel via `Promise.all`, renders Hero directly, wraps below-fold sections in `ScrollFadeIn` +- `app/components/Hero.tsx` — **server component** (no `"use client"`), uses `getTranslations()` from `next-intl/server` +- `app/components/ClientWrappers.tsx` — exports `AboutClient`, `ProjectsClient`, `ContactClient`, `FooterClient`; each wraps its component in a scoped `NextIntlClientProvider` with only the needed translation namespace +- `app/components/ClientProviders.tsx` — root client wrapper, defers Three.js/WebGL via `requestIdleCallback` (5s timeout) to avoid blocking LCP -All external data sources fail gracefully - the site never crashes if Directus, PostgreSQL, n8n, or Redis are unavailable. +### SSR Animation Safety + +**Never use Framer Motion's `initial={{ opacity: 0 }}` on SSR-rendered elements** — it bakes `style="opacity:0"` into HTML, making content invisible if JS hydration fails or is slow. + +Use `ScrollFadeIn` (`app/components/ScrollFadeIn.tsx`) instead: renders no inline style during SSR, applies opacity+transform only after `hasMounted` check via IntersectionObserver + CSS transitions. + +`AnimatePresence` is fine for modals/overlays that only render after user interaction. + +### Data Source Fallback Chain + +Every data fetch degrades gracefully — the site never crashes: + +1. **Directus CMS** (if `DIRECTUS_STATIC_TOKEN` configured) → 2. **PostgreSQL** → 3. **JSON files** (`messages/*.json`) → 4. **Hardcoded defaults** → 5. **i18n key itself** ### CMS Integration (Directus) -- REST/GraphQL calls via `lib/directus.ts` (no Directus SDK) + +- GraphQL via `lib/directus.ts` — no Directus SDK, uses `directusRequest()` with 2s timeout +- Returns `null` on failure, never throws - Collections: `tech_stack_categories`, `tech_stack_items`, `hobbies`, `content_pages`, `projects`, `book_reviews` -- Translations use Directus native translation system (M2O to `languages`) -- Locale mapping: `en` -> `en-US`, `de` -> `de-DE` +- Translations use Directus native M2O system; locale mapping: `en` → `en-US`, `de` → `de-DE` +- API routes must export `runtime = 'nodejs'`, `dynamic = 'force-dynamic'`, and include a `source` field in the response (`"directus"` | `"fallback"` | `"error"`) ### n8n Integration -- Webhook base URL: `N8N_WEBHOOK_URL` env var -- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers -- All n8n endpoints have rate limiting and timeout protection (10s) -- Hardcover data cached for 5 minutes -### Component Patterns -- Client components with `"use client"` for interactive/data-fetching parts -- `useEffect` for data loading on mount -- `useTranslations` from next-intl for i18n -- Framer Motion `variants` pattern with `staggerContainer` + `fadeInUp` -- Gradient cards with `liquid-*` color tokens and `backdrop-blur-sm` +- Webhook proxies in `app/api/n8n/` (status, chat, hardcover, generate-image) +- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers +- All endpoints have rate limiting and 10s timeout +- Hardcover reading data cached 5 minutes ## Design System @@ -103,53 +82,54 @@ Custom Tailwind colors prefixed with `liquid-`: - `liquid-sky`, `liquid-mint`, `liquid-lavender`, `liquid-pink` - `liquid-rose`, `liquid-peach`, `liquid-coral`, `liquid-teal`, `liquid-lime` -Cards use gradient backgrounds (`bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15`) with `border-2` and `rounded-xl`. +Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15` with `backdrop-blur-sm`, `border-2`, `rounded-xl`. + +Typography: Headlines uppercase, `tracking-tighter`, accent dot at end (`.`). + +Accessibility: Use `text-stone-600 dark:text-stone-400` (not `text-stone-400` alone) for body text — contrast ratio must be ≥4.5:1. + +## Conventions + +- **TypeScript**: No `any` — use interfaces from `lib/directus.ts` or `types/` +- **Components**: PascalCase files in `app/components/`; every async component needs a Skeleton loading state +- **API routes**: kebab-case directories in `app/api/` +- **i18n**: Always add keys to both `messages/en.json` and `messages/de.json`; `useTranslations()` in client, `getTranslations()` in server components +- **Error logging**: `console.error` only when `process.env.NODE_ENV === "development"` +- **Commit messages**: Conventional Commits (`feat:`, `fix:`, `chore:`) +- **No emojis** in code unless explicitly requested + +## Testing Notes + +- Jest with JSDOM; `jest.setup.ts` mocks `window.matchMedia`, `IntersectionObserver`, and `NextResponse` +- ESM modules (react-markdown, remark-*, etc.) handled via `transformIgnorePatterns` in `jest.config.ts` +- Server component tests: `const resolved = await Component({ props }); render(resolved)` +- Test mocks for `next/image`: use `eslint-disable-next-line @next/next/no-img-element` on the `` tag + +## Deployment & CI/CD + +- `output: "standalone"` in `next.config.ts` +- Entrypoint: `scripts/start-with-migrate.js` — waits for DB, runs migrations (non-fatal), starts server +- CI/CD: `.gitea/workflows/ci.yml` — `test-build` (all branches), `deploy-dev` (dev branch only), `deploy-production` (production branch only) +- **Branches**: `dev` → testing.dk0.dev | `production` → dk0.dev +- Dev and production share the same PostgreSQL and Redis instances ## Key Environment Variables ```bash -# Required for CMS DIRECTUS_URL=https://cms.dk0.dev DIRECTUS_STATIC_TOKEN=... - -# Required for n8n features N8N_WEBHOOK_URL=https://n8n.dk0.dev N8N_SECRET_TOKEN=... N8N_API_KEY=... - -# Database DATABASE_URL=postgresql://... - -# Optional -REDIS_URL=redis://... +REDIS_URL=redis://... # optional ``` -## Conventions +## Adding a CMS-managed Section -- Language: Code in English, user-facing text via i18n (EN + DE) -- Commit messages: Conventional Commits (`feat:`, `fix:`, `chore:`) -- Components: PascalCase files in `app/components/` -- API routes: kebab-case directories in `app/api/` -- CMS data always has a static fallback - never rely solely on Directus -- Error logging: Only in `development` mode (`process.env.NODE_ENV === "development"`) -- No emojis in code unless explicitly requested - -## Common Tasks - -### Adding a new CMS-managed section -1. Define the GraphQL query + types in `lib/directus.ts` -2. Create an API route in `app/api//route.ts` -3. Create a component in `app/components/.tsx` -4. Add i18n keys to `messages/en.json` and `messages/de.json` -5. Integrate into the parent component (usually `About.tsx`) - -### Adding i18n strings -1. Add keys to `messages/en.json` and `messages/de.json` -2. Access via `useTranslations("key.path")` in client components -3. Or `getTranslations("key.path")` in server components - -### Working with Directus collections -- All queries go through `directusRequest()` in `lib/directus.ts` -- Uses GraphQL endpoint (`/graphql`) -- 2-second timeout, graceful null fallback -- Translations filtered by `languages_code.code` matching Directus locale +1. Define GraphQL query + types in `lib/directus.ts` +2. Create API route `app/api//route.ts` with `runtime='nodejs'`, `dynamic='force-dynamic'`, and `source` field in response +3. Create component `app/components/.tsx` with Skeleton loading state +4. Add i18n keys to both `messages/en.json` and `messages/de.json` +5. Create `Client` wrapper in `app/components/ClientWrappers.tsx` with scoped `NextIntlClientProvider` +6. Add to `app/_ui/HomePageServer.tsx` wrapped in `` diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index d9c9eea..11d06a1 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -8,13 +8,8 @@ import ErrorBoundary from "@/components/ErrorBoundary"; import { ConsentProvider } from "./ConsentProvider"; import { ThemeProvider } from "./ThemeProvider"; -const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), { - ssr: false, - loading: () => null, -}); - -const ShaderGradientBackground = dynamic( - () => import("./ShaderGradientBackground").catch(() => ({ default: () => null })), +const BackgroundBlobs = dynamic( + () => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), { ssr: false, loading: () => null } ); @@ -52,17 +47,16 @@ function GatedProviders({ children: React.ReactNode; mounted: boolean; }) { - // Defer heavy Three.js/WebGL background until after LCP + // Defer animated background blobs until after LCP const [deferredReady, setDeferredReady] = useState(false); useEffect(() => { if (!mounted) return; - // Safari < 16.4 lacks requestIdleCallback — fall back to setTimeout let id: ReturnType | number; if (typeof requestIdleCallback !== "undefined") { id = requestIdleCallback(() => setDeferredReady(true), { timeout: 5000 }); return () => cancelIdleCallback(id as number); } else { - id = setTimeout(() => setDeferredReady(true), 1); + id = setTimeout(() => setDeferredReady(true), 200); return () => clearTimeout(id); } }, [mounted]); @@ -71,7 +65,6 @@ function GatedProviders({ {deferredReady && } - {deferredReady && }
{children}
diff --git a/app/components/ShaderGradientBackground.tsx b/app/components/ShaderGradientBackground.tsx index 0dd4df8..56e4b88 100644 --- a/app/components/ShaderGradientBackground.tsx +++ b/app/components/ShaderGradientBackground.tsx @@ -1,126 +1,60 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import { ShaderGradientCanvas, ShaderGradient } from "@shadergradient/react"; - -const ShaderGradientBackground = () => { - const [supported, setSupported] = useState(true); - - useEffect(() => { - try { - const canvas = document.createElement("canvas"); - const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); - if (!gl) setSupported(false); - } catch { - setSupported(false); - } - }, []); - - if (!supported) return null; - +// Pure CSS gradient background — replaces the Three.js/WebGL shader gradient. +// Server component: no "use client", zero JS bundle cost, renders in initial HTML. +// Visual result is identical since all original spheres had animate="off" (static). +export default function ShaderGradientBackground() { return (
-
+
{/* Left: Text Content */} -
+
{getLabel("hero.badge", "Student & Self-Hoster")} @@ -41,26 +41,26 @@ export default async function Hero({ locale }: HeroProps) { -

+

{t("description")}

-
+ {/* Right: The Photo */} -
+
- Dennis Konkol + Dennis Konkol
diff --git a/next.config.ts b/next.config.ts index b0493f8..251413f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -34,10 +34,12 @@ const nextConfig: NextConfig = { experimental: { // Tree-shake barrel-file packages in both dev and production optimizePackageImports: ["lucide-react", "framer-motion", "react-icons", "@tiptap/react"], + // Merge all CSS into a single chunk to eliminate the render-blocking CSS chain + // (84dc7384.css → 3aefc04b.css sequential dependency reported by PageSpeed). + cssChunking: "loose", // Note: optimizeCss (critters) is intentionally disabled — it converts the main // to a JS-deferred preload, which PageSpeed reads as a - // sequential CSS chain and reports 410ms of render-blocking. Without it both CSS - // files load as parallel tags discovered from the initial HTML (~150ms total). + // sequential CSS chain and reports 410ms of render-blocking. ...(process.env.NODE_ENV !== "production" ? { webpackBuildWorker: true } : {}), }, diff --git a/package-lock.json b/package-lock.json index 409ce58..7512de5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,6 @@ "dependencies": { "@next/bundle-analyzer": "^15.1.7", "@prisma/client": "^5.22.0", - "@react-three/fiber": "^9.5.0", - "@shadergradient/react": "^2.4.20", "@swc/helpers": "^0.5.19", "@tiptap/extension-color": "^3.15.3", "@tiptap/extension-highlight": "^3.15.3", @@ -38,8 +36,7 @@ "react-markdown": "^10.1.0", "redis": "^5.8.2", "sanitize-html": "^2.17.0", - "tailwind-merge": "^2.6.0", - "three": "^0.183.1" + "tailwind-merge": "^2.6.0" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -538,6 +535,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2998,54 +2996,6 @@ "@prisma/debug": "5.22.0" } }, - "node_modules/@react-three/fiber": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", - "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.17.8", - "@types/webxr": "*", - "base64-js": "^1.5.1", - "buffer": "^6.0.3", - "its-fine": "^2.0.0", - "react-use-measure": "^2.1.7", - "scheduler": "^0.27.0", - "suspend-react": "^0.1.3", - "use-sync-external-store": "^1.4.0", - "zustand": "^5.0.3" - }, - "peerDependencies": { - "expo": ">=43.0", - "expo-asset": ">=8.4", - "expo-file-system": ">=11.0", - "expo-gl": ">=11.0", - "react": ">=19 <19.3", - "react-dom": ">=19 <19.3", - "react-native": ">=0.78", - "three": ">=0.156" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - }, - "expo-asset": { - "optional": true - }, - "expo-file-system": { - "optional": true - }, - "expo-gl": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/@redis/bloom": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.10.0.tgz", @@ -3141,16 +3091,6 @@ "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", "license": "MIT" }, - "node_modules/@shadergradient/react": { - "version": "2.4.20", - "resolved": "https://registry.npmjs.org/@shadergradient/react/-/react-2.4.20.tgz", - "integrity": "sha512-MVYvYgTHK3d36C2jNKzt4OzWeyNyc/bWI0zKRyv5EgI2hmUee8nD0cmuNX9X1lw1RCcuof6n1Rnj63jWepZHJA==", - "license": "MIT", - "peerDependencies": { - "react": "^18.2.0 || ^19.0.0", - "react-dom": "^18.2.0 || ^19.0.0" - } - }, "node_modules/@shuding/opentype.js": { "version": "1.4.0-beta.0", "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", @@ -4275,15 +4215,6 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/react-reconciler": { - "version": "0.28.9", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", - "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, "node_modules/@types/sanitize-html": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.0.tgz", @@ -4320,12 +4251,6 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "license": "MIT" - }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -5490,26 +5415,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -5621,30 +5526,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -8322,26 +8203,6 @@ "@formatjs/icu-messageformat-parser": "^3.4.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -9115,18 +8976,6 @@ "node": ">= 0.4" } }, - "node_modules/its-fine": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", - "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", - "license": "MIT", - "dependencies": { - "@types/react-reconciler": "^0.28.9" - }, - "peerDependencies": { - "react": "^19.0.0" - } - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -12833,21 +12682,6 @@ "react": ">=18" } }, - "node_modules/react-use-measure": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", - "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.13", - "react-dom": ">=16.13" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -13880,15 +13714,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/suspend-react": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", - "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", - "license": "MIT", - "peerDependencies": { - "react": ">=17.0" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -14041,12 +13866,6 @@ "node": ">=0.8" } }, - "node_modules/three": { - "version": "0.183.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.183.1.tgz", - "integrity": "sha512-Psv6bbd3d/M/01MT2zZ+VmD0Vj2dbWTNhfe4CuSg7w5TuW96M3NOyCVuh9SZQ05CpGmD7NEcJhZw4GVjhCYxfQ==", - "license": "MIT" - }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -15261,35 +15080,6 @@ "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", "license": "MIT" }, - "node_modules/zustand": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", - "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index b63d6cb..363d0e2 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,6 @@ "dependencies": { "@next/bundle-analyzer": "^15.1.7", "@prisma/client": "^5.22.0", - "@react-three/fiber": "^9.5.0", - "@shadergradient/react": "^2.4.20", "@swc/helpers": "^0.5.19", "@tiptap/extension-color": "^3.15.3", "@tiptap/extension-highlight": "^3.15.3", @@ -82,8 +80,7 @@ "react-markdown": "^10.1.0", "redis": "^5.8.2", "sanitize-html": "^2.17.0", - "tailwind-merge": "^2.6.0", - "three": "^0.183.1" + "tailwind-merge": "^2.6.0" }, "browserslist": [ "chrome >= 100", From 34a81a6437b9a8af36ddc168d59cc0c2e5c3ba8c Mon Sep 17 00:00:00 2001 From: denshooter Date: Fri, 6 Mar 2026 12:31:09 +0100 Subject: [PATCH 22/29] fix: resolve TypeScript errors in CI type-check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - next.config.ts: cssChunking 'loose' → false ('loose' not in type) - ActivityFeed.test.tsx: remove always-truthy TS2872 literal expression Co-Authored-By: Claude Sonnet 4.6 --- app/__tests__/components/ActivityFeed.test.tsx | 2 +- next.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/__tests__/components/ActivityFeed.test.tsx b/app/__tests__/components/ActivityFeed.test.tsx index a8f33e3..b606581 100644 --- a/app/__tests__/components/ActivityFeed.test.tsx +++ b/app/__tests__/components/ActivityFeed.test.tsx @@ -58,7 +58,7 @@ describe('ActivityFeed NaN Handling', () => { }); it('should convert gaming.name to string safely', () => { - const validName = String('Test Game' || ''); + const validName = String('Test Game'); expect(validName).toBe('Test Game'); expect(typeof validName).toBe('string'); diff --git a/next.config.ts b/next.config.ts index 251413f..2e49e52 100644 --- a/next.config.ts +++ b/next.config.ts @@ -36,7 +36,7 @@ const nextConfig: NextConfig = { optimizePackageImports: ["lucide-react", "framer-motion", "react-icons", "@tiptap/react"], // Merge all CSS into a single chunk to eliminate the render-blocking CSS chain // (84dc7384.css → 3aefc04b.css sequential dependency reported by PageSpeed). - cssChunking: "loose", + cssChunking: false, // Note: optimizeCss (critters) is intentionally disabled — it converts the main // to a JS-deferred preload, which PageSpeed reads as a // sequential CSS chain and reports 410ms of render-blocking. From 1c49289386f0141844d028fc7cd579f6496816e7 Mon Sep 17 00:00:00 2001 From: denshooter Date: Fri, 6 Mar 2026 14:57:36 +0100 Subject: [PATCH 23/29] perf: remove TipTap/ProseMirror from client bundle, lazy-load below-fold sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TipTap (ProseMirror) was causing: - chunks 1007 (85 KiB) and 3207 (58 KiB) in the initial bundle - Array.prototype.at/flat/flatMap, Object.fromEntries/hasOwn polyfills (ProseMirror bundles core-js for these — the 12 KiB legacy JS flag) - 2+ seconds of main thread blocking on mobile Fix: move HTML conversion to the server (API route) and pass the resulting HTML string to the client, eliminating the need to import richTextToSafeHtml (and transitively TipTap) in any client component. Changes: - app/api/content/page/route.ts: call richTextToSafeHtml server-side, add html: string to response alongside existing content - app/components/RichTextClient.tsx: accept html string, remove all TipTap imports — TipTap/ProseMirror now has zero client bundle cost - app/components/About.tsx, Contact.tsx: use cmsHtml from API - app/legal-notice/page.tsx, privacy-policy/page.tsx: same - app/components/ClientWrappers.tsx: change static imports of About, Projects, Contact, Footer to next/dynamic so their JS is in separate lazy-loaded chunks, not in the initial bundle Co-Authored-By: Claude Sonnet 4.6 --- app/api/content/page/route.ts | 4 ++++ app/components/About.tsx | 9 ++++----- app/components/ClientWrappers.tsx | 12 ++++++++---- app/components/Contact.tsx | 15 +++++++-------- app/components/RichTextClient.tsx | 13 +++++-------- app/legal-notice/page.tsx | 11 +++++------ app/privacy-policy/page.tsx | 11 +++++------ 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/app/api/content/page/route.ts b/app/api/content/page/route.ts index 4e89980..4bdab1c 100644 --- a/app/api/content/page/route.ts +++ b/app/api/content/page/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getContentByKey } from "@/lib/content"; import { getContentPage } from "@/lib/directus"; +import { richTextToSafeHtml } from "@/lib/richtext"; const CACHE_TTL = 300; // 5 minutes @@ -17,6 +18,8 @@ export async function GET(request: NextRequest) { // 1) Try Directus first const directusPage = await getContentPage(key, locale); if (directusPage) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const html = directusPage.content ? richTextToSafeHtml(directusPage.content as any) : ""; return NextResponse.json( { content: { @@ -24,6 +27,7 @@ export async function GET(request: NextRequest) { slug: directusPage.slug, locale: directusPage.locale || locale, content: directusPage.content, + html, }, source: "directus", }, diff --git a/app/components/About.tsx b/app/components/About.tsx index 520d059..a6a523f 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -3,7 +3,6 @@ import { useState, useEffect } from "react"; import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, BookOpen, MessageSquare, ArrowRight, Tv, Plane, Camera, Stars, Music, Terminal, Cpu } from "lucide-react"; import { useLocale, useTranslations } from "next-intl"; -import type { JSONContent } from "@tiptap/react"; import dynamic from "next/dynamic"; const RichTextClient = dynamic(() => import("./RichTextClient"), { ssr: false }); import CurrentlyReading from "./CurrentlyReading"; @@ -23,7 +22,7 @@ const iconMap: Record = { const About = () => { const locale = useLocale(); const t = useTranslations("home.about"); - const [cmsDoc, setCmsDoc] = useState(null); + const [cmsHtml, setCmsHtml] = useState(null); const [techStack, setTechStack] = useState([]); const [hobbies, setHobbies] = useState([]); const [snippets, setSnippets] = useState([]); @@ -44,7 +43,7 @@ const About = () => { ]); const cmsData = await cmsRes.json(); - if (cmsData?.content?.content) setCmsDoc(cmsData.content.content as JSONContent); + if (cmsData?.content?.html) setCmsHtml(cmsData.content.html as string); const techData = await techRes.json(); if (techData?.techStack) setTechStack(techData.techStack); @@ -93,8 +92,8 @@ const About = () => {
- ) : cmsDoc ? ( - + ) : cmsHtml ? ( + ) : (

{t("p1")} {t("p2")}

)} diff --git a/app/components/ClientWrappers.tsx b/app/components/ClientWrappers.tsx index 5b943eb..70d6b6e 100644 --- a/app/components/ClientWrappers.tsx +++ b/app/components/ClientWrappers.tsx @@ -6,10 +6,14 @@ */ import { NextIntlClientProvider } from 'next-intl'; -import About from './About'; -import Projects from './Projects'; -import Contact from './Contact'; -import Footer from './Footer'; +import dynamic from 'next/dynamic'; + +// Lazy-load below-fold components so their JS doesn't block initial paint / LCP. +// SSR stays on (default) so content is in the initial HTML for SEO. +const About = dynamic(() => import('./About')); +const Projects = dynamic(() => import('./Projects')); +const Contact = dynamic(() => import('./Contact')); +const Footer = dynamic(() => import('./Footer')); import type { AboutTranslations, ProjectsTranslations, diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index 35202b0..0fed48b 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -5,7 +5,6 @@ import { motion } from "framer-motion"; import { Mail, MapPin, Send, Github, Linkedin } from "lucide-react"; import { useToast } from "@/components/Toast"; import { useLocale, useTranslations } from "next-intl"; -import type { JSONContent } from "@tiptap/react"; import dynamic from "next/dynamic"; const RichTextClient = dynamic(() => import("./RichTextClient"), { ssr: false }); @@ -15,7 +14,7 @@ const Contact = () => { const t = useTranslations("home.contact"); const tForm = useTranslations("home.contact.form"); const tInfo = useTranslations("home.contact.info"); - const [cmsDoc, setCmsDoc] = useState(null); + const [cmsHtml, setCmsHtml] = useState(null); useEffect(() => { (async () => { @@ -25,14 +24,14 @@ const Contact = () => { ); const data = await res.json(); // Only use CMS content if it exists for the active locale. - if (data?.content?.content && data?.content?.locale === locale) { - setCmsDoc(data.content.content as JSONContent); + if (data?.content?.html && data?.content?.locale === locale) { + setCmsHtml(data.content.html as string); } else { - setCmsDoc(null); + setCmsHtml(null); } } catch { // ignore; fallback to static - setCmsDoc(null); + setCmsHtml(null); } })(); }, [locale]); @@ -169,8 +168,8 @@ const Contact = () => {

{t("title")}.

- {cmsDoc ? ( - + {cmsHtml ? ( + ) : (

{t("subtitle")} diff --git a/app/components/RichTextClient.tsx b/app/components/RichTextClient.tsx index 1813c51..e9abf7d 100644 --- a/app/components/RichTextClient.tsx +++ b/app/components/RichTextClient.tsx @@ -1,22 +1,19 @@ "use client"; -import React, { useMemo } from "react"; -import type { JSONContent } from "@tiptap/react"; -import { richTextToSafeHtml } from "@/lib/richtext"; +import React from "react"; +// Accepts pre-sanitized HTML string (converted server-side via richTextToSafeHtml). +// This keeps TipTap/ProseMirror out of the client bundle entirely. export default function RichTextClient({ - doc, + html, className, }: { - doc: JSONContent; + html: string; className?: string; }) { - const html = useMemo(() => richTextToSafeHtml(doc), [doc]); - return (

); diff --git a/app/legal-notice/page.tsx b/app/legal-notice/page.tsx index 9278a9f..31e55b0 100644 --- a/app/legal-notice/page.tsx +++ b/app/legal-notice/page.tsx @@ -8,13 +8,12 @@ import Footer from "../components/Footer"; import Link from "next/link"; import { useLocale, useTranslations } from "next-intl"; import { useEffect, useState } from "react"; -import type { JSONContent } from "@tiptap/react"; import RichTextClient from "../components/RichTextClient"; export default function LegalNotice() { const locale = useLocale(); const t = useTranslations("common"); - const [cmsDoc, setCmsDoc] = useState(null); + const [cmsHtml, setCmsHtml] = useState(null); useEffect(() => { (async () => { @@ -23,8 +22,8 @@ export default function LegalNotice() { `/api/content/page?key=${encodeURIComponent("legal-notice")}&locale=${encodeURIComponent(locale)}`, ); const data = await res.json(); - if (data?.content?.content && data?.content?.locale === locale) { - setCmsDoc(data.content.content as JSONContent); + if (data?.content?.html && data?.content?.locale === locale) { + setCmsHtml(data.content.html as string); } } catch {} })(); @@ -64,9 +63,9 @@ export default function LegalNotice() { transition={{ delay: 0.1 }} className="lg:col-span-8 bg-white dark:bg-stone-900 rounded-[3rem] p-10 md:p-16 border border-stone-200/60 dark:border-stone-800/60 shadow-sm" > - {cmsDoc ? ( + {cmsHtml ? (
- +
) : (
diff --git a/app/privacy-policy/page.tsx b/app/privacy-policy/page.tsx index 8579224..e0648ab 100644 --- a/app/privacy-policy/page.tsx +++ b/app/privacy-policy/page.tsx @@ -8,13 +8,12 @@ import Footer from "../components/Footer"; import Link from "next/link"; import { useLocale, useTranslations } from "next-intl"; import { useEffect, useState } from "react"; -import type { JSONContent } from "@tiptap/react"; import RichTextClient from "../components/RichTextClient"; export default function PrivacyPolicy() { const locale = useLocale(); const t = useTranslations("common"); - const [cmsDoc, setCmsDoc] = useState(null); + const [cmsHtml, setCmsHtml] = useState(null); useEffect(() => { (async () => { @@ -23,8 +22,8 @@ export default function PrivacyPolicy() { `/api/content/page?key=${encodeURIComponent("privacy-policy")}&locale=${encodeURIComponent(locale)}`, ); const data = await res.json(); - if (data?.content?.content && data?.content?.locale === locale) { - setCmsDoc(data.content.content as JSONContent); + if (data?.content?.html && data?.content?.locale === locale) { + setCmsHtml(data.content.html as string); } } catch {} })(); @@ -64,9 +63,9 @@ export default function PrivacyPolicy() { transition={{ delay: 0.1 }} className="lg:col-span-8 bg-white dark:bg-stone-900 rounded-[3rem] p-10 md:p-16 border border-stone-200/60 dark:border-stone-800/60 shadow-sm" > - {cmsDoc ? ( + {cmsHtml ? (
- +
) : (
From 7f7ed39b0e1986f6770f697f282047d96024a46d Mon Sep 17 00:00:00 2001 From: denshooter Date: Fri, 6 Mar 2026 15:14:30 +0100 Subject: [PATCH 24/29] fix: prevent image/badge cutoff on iPad in Hero section overflow-hidden on the
was clipping the -bottom-6 badge and the image bottom on iPad viewports where content sits near the section edge. Move overflow-hidden to the blobs container (absolute inset-0) so the blobs are still clipped but the image and badge can render freely. Add pb-10 sm:pb-16 bottom padding so the badge always has clearance. Co-Authored-By: Claude Sonnet 4.6 --- app/components/Hero.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index d3339c0..600b0e8 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -15,14 +15,14 @@ export default async function Hero({ locale }: HeroProps) { const getLabel = (key: string, fallback: string) => cmsMessages[key] || fallback; return ( -
- {/* Liquid Ambient Background */} -
+
+ {/* Liquid Ambient Background — overflow-hidden here so the blobs are clipped, not the image/badge */} +
-
+
{/* Left: Text Content */} From dacec1895636d3e07b71dbf216b4972edcabfe14 Mon Sep 17 00:00:00 2001 From: denshooter Date: Fri, 6 Mar 2026 17:39:29 +0100 Subject: [PATCH 25/29] perf: eliminate next-themes and framer-motion from initial JS bundle - Replace next-themes (38 KiB) with a tiny custom ThemeProvider (~< 1 KiB) using localStorage + classList.toggle for theme management - Add FOUC-prevention inline script in layout.tsx to apply saved theme before React hydrates - Remove framer-motion from Header.tsx: nav entry now uses CSS slideDown keyframe, mobile menu uses CSS opacity/translate transitions - Remove framer-motion from ThemeToggle.tsx: use Tailwind hover/active scale - Remove framer-motion from legal-notice and privacy-policy pages - Update useTheme import in ThemeToggle to use custom ThemeProvider - Add slideDown keyframe to tailwind.config.ts - Update tests to mock custom ThemeProvider instead of next-themes Result: framer-motion moves from "First Load JS shared by all" to lazy chunks; next-themes chunk eliminated entirely; -38 KiB from initial bundle Co-Authored-By: Claude Sonnet 4.6 --- app/__tests__/components/Header.test.tsx | 10 ++-- app/__tests__/components/ThemeToggle.test.tsx | 5 +- app/components/ClientProviders.tsx | 2 +- app/components/Header.tsx | 54 ++++++++----------- app/components/ThemeProvider.tsx | 41 +++++++++++--- app/components/ThemeToggle.tsx | 11 ++-- app/layout.tsx | 2 + app/legal-notice/page.tsx | 26 +++------ app/privacy-policy/page.tsx | 28 ++++------ package.json | 1 - tailwind.config.ts | 5 ++ 11 files changed, 94 insertions(+), 91 deletions(-) diff --git a/app/__tests__/components/Header.test.tsx b/app/__tests__/components/Header.test.tsx index 64ab5e8..1855e36 100644 --- a/app/__tests__/components/Header.test.tsx +++ b/app/__tests__/components/Header.test.tsx @@ -25,10 +25,10 @@ describe('Header', () => { render(
); expect(screen.getByText('dk')).toBeInTheDocument(); - // Check for navigation links - expect(screen.getByText('Home')).toBeInTheDocument(); - expect(screen.getByText('About')).toBeInTheDocument(); - expect(screen.getByText('Projects')).toBeInTheDocument(); - expect(screen.getByText('Contact')).toBeInTheDocument(); + // Check for navigation links (appear in both desktop and mobile menus) + expect(screen.getAllByText('Home').length).toBeGreaterThan(0); + expect(screen.getAllByText('About').length).toBeGreaterThan(0); + expect(screen.getAllByText('Projects').length).toBeGreaterThan(0); + expect(screen.getAllByText('Contact').length).toBeGreaterThan(0); }); }); diff --git a/app/__tests__/components/ThemeToggle.test.tsx b/app/__tests__/components/ThemeToggle.test.tsx index 0b16990..ad5a92c 100644 --- a/app/__tests__/components/ThemeToggle.test.tsx +++ b/app/__tests__/components/ThemeToggle.test.tsx @@ -1,12 +1,13 @@ import { render, screen } from "@testing-library/react"; import { ThemeToggle } from "@/app/components/ThemeToggle"; -// Mock next-themes -jest.mock("next-themes", () => ({ +// Mock custom ThemeProvider +jest.mock("@/app/components/ThemeProvider", () => ({ useTheme: () => ({ theme: "light", setTheme: jest.fn(), }), + ThemeProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })); describe("ThemeToggle Component", () => { diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index 11d06a1..7aaa57a 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -29,7 +29,7 @@ export default function ClientProviders({ - + {children} diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 0cdfdee..607239c 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import { motion, AnimatePresence } from "framer-motion"; import { Menu, X } from "lucide-react"; import Link from "next/link"; import { useLocale, useTranslations } from "next-intl"; @@ -26,11 +25,7 @@ const Header = () => { return ( <>
- +
- +
{/* Mobile Menu Overlay */} - - {isOpen && ( - -
- {navItems.map((item) => ( - setIsOpen(false)} - className="px-6 py-4 text-sm font-black uppercase tracking-[0.2em] text-stone-900 dark:text-stone-100 bg-stone-50 dark:bg-white/5 rounded-2xl transition-colors hover:bg-liquid-mint/10" - > - {item.name} - - ))} -
-
- )} -
+
+
+ {navItems.map((item) => ( + setIsOpen(false)} + className="px-6 py-4 text-sm font-black uppercase tracking-[0.2em] text-stone-900 dark:text-stone-100 bg-stone-50 dark:bg-white/5 rounded-2xl transition-colors hover:bg-liquid-mint/10" + > + {item.name} + + ))} +
+
); }; diff --git a/app/components/ThemeProvider.tsx b/app/components/ThemeProvider.tsx index 189a2b1..eef9dac 100644 --- a/app/components/ThemeProvider.tsx +++ b/app/components/ThemeProvider.tsx @@ -1,11 +1,38 @@ "use client"; -import * as React from "react"; -import { ThemeProvider as NextThemesProvider } from "next-themes"; +import React, { createContext, useContext, useEffect, useState } from "react"; -export function ThemeProvider({ - children, - ...props -}: React.ComponentProps) { - return {children}; +type Theme = "light" | "dark"; + +const ThemeCtx = createContext<{ theme: Theme; setTheme: (t: Theme) => void }>({ + theme: "light", + setTheme: () => {}, +}); + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setThemeState] = useState("light"); + + useEffect(() => { + try { + const stored = localStorage.getItem("theme") as Theme | null; + if (stored === "dark" || stored === "light") { + setThemeState(stored); + document.documentElement.classList.toggle("dark", stored === "dark"); + } + } catch {} + }, []); + + const setTheme = (t: Theme) => { + setThemeState(t); + try { + localStorage.setItem("theme", t); + } catch {} + document.documentElement.classList.toggle("dark", t === "dark"); + }; + + return {children}; +} + +export function useTheme() { + return useContext(ThemeCtx); } diff --git a/app/components/ThemeToggle.tsx b/app/components/ThemeToggle.tsx index 0d61707..7f096d7 100644 --- a/app/components/ThemeToggle.tsx +++ b/app/components/ThemeToggle.tsx @@ -2,8 +2,7 @@ import * as React from "react"; import { Moon, Sun } from "lucide-react"; -import { useTheme } from "next-themes"; -import { motion } from "framer-motion"; +import { useTheme } from "./ThemeProvider"; export function ThemeToggle() { const { theme, setTheme } = useTheme(); @@ -18,11 +17,9 @@ export function ThemeToggle() { } return ( - setTheme(theme === "dark" ? "light" : "dark")} - className="p-2 rounded-full bg-stone-100 dark:bg-stone-800 text-stone-800 dark:text-stone-100 hover:bg-stone-200 dark:hover:bg-stone-700 transition-colors border border-stone-200 dark:border-stone-700 shadow-sm" + className="p-2 rounded-full bg-stone-100 dark:bg-stone-800 text-stone-800 dark:text-stone-100 hover:bg-stone-200 dark:hover:bg-stone-700 transition-colors border border-stone-200 dark:border-stone-700 shadow-sm hover:scale-105 active:scale-95 transition-transform" aria-label="Toggle theme" > {theme === "dark" ? ( @@ -30,6 +27,6 @@ export function ThemeToggle() { ) : ( )} - + ); } diff --git a/app/layout.tsx b/app/layout.tsx index 67ab29f..4c16aae 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -32,6 +32,8 @@ export default async function RootLayout({ + {/* Prevent flash of unstyled theme — reads localStorage before React hydrates */} +