diff --git a/package.json b/package.json index 9f244f5..8c7c4d0 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,13 @@ }, "dependencies": { "@c2s/payload-contracts": "github:complexcaresolutions/payload-contracts", + "clsx": "^2.1.1", + "framer-motion": "^12.34.0", "next": "16.0.10", "react": "19.2.1", - "react-dom": "19.2.1" + "react-dom": "19.2.1", + "react-icons": "^5.5.0", + "tailwind-merge": "^3.4.1" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1da515..dc8ee7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@c2s/payload-contracts': specifier: github:complexcaresolutions/payload-contracts version: git+https://git@github.com:complexcaresolutions/payload-contracts.git#64847594b2150bfdce09a7bd7f54ad2f52d6f2b7(react@19.2.1) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + framer-motion: + specifier: ^12.34.0 + version: 12.34.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1) next: specifier: 16.0.10 version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -20,6 +26,12 @@ importers: react-dom: specifier: 19.2.1 version: 19.2.1(react@19.2.1) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.2.1) + tailwind-merge: + specifier: ^3.4.1 + version: 3.4.1 devDependencies: '@tailwindcss/postcss': specifier: ^4 @@ -811,6 +823,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1100,6 +1116,20 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + framer-motion@12.34.0: + resolution: {integrity: sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1487,6 +1517,12 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + motion-dom@12.34.0: + resolution: {integrity: sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==} + + motion-utils@12.29.2: + resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1632,6 +1668,11 @@ packages: peerDependencies: react: ^19.2.1 + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -1797,6 +1838,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwind-merge@3.4.1: + resolution: {integrity: sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q==} + tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} @@ -2665,6 +2709,8 @@ snapshots: client-only@0.0.1: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3100,6 +3146,15 @@ snapshots: dependencies: is-callable: 1.2.7 + framer-motion@12.34.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + dependencies: + motion-dom: 12.34.0 + motion-utils: 12.29.2 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3466,6 +3521,12 @@ snapshots: minimist@1.2.8: {} + motion-dom@12.34.0: + dependencies: + motion-utils: 12.29.2 + + motion-utils@12.29.2: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -3611,6 +3672,10 @@ snapshots: react: 19.2.1 scheduler: 0.27.0 + react-icons@5.5.0(react@19.2.1): + dependencies: + react: 19.2.1 + react-is@16.13.1: {} react@19.2.1: {} @@ -3846,6 +3911,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tailwind-merge@3.4.1: {} + tailwindcss@4.1.18: {} tapable@2.3.0: {} diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index 3a27675..216544e 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -3,23 +3,29 @@ import { BlockRenderer } from "@/components/blocks" import { notFound } from "next/navigation" import type { Metadata } from "next" -interface Props { +interface PageProps { params: Promise<{ slug: string }> } export async function generateStaticParams() { - const data = await getPages({ limit: 100 }) - return data.docs - .filter((p) => p.slug !== "home") - .map((p) => ({ slug: p.slug })) + try { + const pages = await getPages({ limit: 100 }) + return (pages?.docs || []) + .filter((p) => p.slug && p.slug !== "home") + .map((p) => ({ slug: p.slug })) + } catch { + return [] + } } -export async function generateMetadata({ params }: Props): Promise { +export async function generateMetadata({ params }: PageProps): Promise { const { slug } = await params const [page, seoSettings] = await Promise.all([getPage(slug), getSeoSettings()]) if (!page) return {} - const titleSuffix = (seoSettings as any)?.metaDefaults?.titleSuffix || "" + const titleSuffix = (seoSettings as Record)?.metaDefaults + ? ((seoSettings as Record).metaDefaults as Record)?.titleSuffix as string || "" + : "" const title = page.seo?.metaTitle || page.title const description = page.seo?.metaDescription || "" @@ -29,17 +35,11 @@ export async function generateMetadata({ params }: Props): Promise { } } -export default async function DynamicPage({ params }: Props) { +export default async function DynamicPage({ params }: PageProps) { const { slug } = await params const page = await getPage(slug) - if (!page) { - notFound() - } + if (!page) notFound() - return ( -
- -
- ) + return } diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..915c579 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,127 @@ -@import "tailwindcss"; +@import 'tailwindcss'; -:root { - --background: #ffffff; - --foreground: #171717; +/* Tailwind v4 Theme Configuration */ +@theme { + /* Colors */ + --color-dark: #111111; + --color-gray: #777777; + --color-gray-light: #999999; + --color-light: #ececec; + --color-light-soft: #f8f8f8; + --color-light-bg: #f4f4f4; + --color-accent: #2CAADF; + --color-accent-dark: #1a8fc4; + --color-error: #e80000; + --color-success: #0F9D58; + --color-footer-bg: #e3e3e3; + --color-footer-copyright: #222222; + --color-border: #dddddd; + --color-border-light: #cccccc; + + /* Fonts */ + --font-heading: var(--font-montserrat), 'Montserrat', 'Open Sans', 'Helvetica Neue', sans-serif; + --font-heading-alt: var(--font-open-sans), 'Open Sans', 'Montserrat', 'Helvetica Neue', sans-serif; + --font-body: var(--font-open-sans), 'Open Sans', 'Helvetica Neue', 'Helvetica', sans-serif; + + /* Spacing */ + --spacing-ws-s: 50px; + --spacing-ws-m: 100px; + --spacing-ws-l: 160px; + + /* Shadows */ + --shadow-card: 0 1px 1px rgba(0, 0, 0, 0.2); + --shadow-card-hover: 0 22px 43px rgba(0, 0, 0, 0.15); } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} +/* Base Styles */ +@layer base { + html { + font-size: 16px; + scroll-behavior: smooth; + } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; + body { + font-family: var(--font-body); + font-weight: 300; + font-size: 14px; + letter-spacing: 0.2px; + line-height: 1.8em; + color: var(--color-gray); + background-color: white; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + /* Headings */ + h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); + font-weight: 700; + text-transform: uppercase; + color: var(--color-dark); + } + + h1 { font-size: 1.7em; letter-spacing: 5px; } + h2 { font-size: 1.5em; letter-spacing: 4px; } + h3 { font-size: 1.3em; letter-spacing: 3.5px; } + h4 { font-size: 1.07em; letter-spacing: 3px; } + h5 { font-size: 1em; letter-spacing: 2px; } + h6 { font-size: 0.85em; letter-spacing: 2px; } + + /* Links */ + a { + color: var(--color-accent); + border-bottom: 1px solid transparent; + transition: all 500ms; + text-decoration: none; + } + + a:hover { + border-bottom-color: var(--color-accent); + } + + /* Selection */ + ::selection { + background: var(--color-dark); + color: white; } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +/* Component Layer */ +@layer components { + .container-def { + max-width: 1200px; + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + } + + .section-spacing { + padding-top: var(--spacing-ws-m); + padding-bottom: var(--spacing-ws-m); + } + + .section-spacing-lg { + padding-top: var(--spacing-ws-l); + padding-bottom: var(--spacing-ws-l); + } + + .h-alt { + font-family: var(--font-heading-alt); + font-weight: 300; + text-transform: uppercase; + } +} + +/* Utility Layer */ +@layer utilities { + .bg-parallax { + background-attachment: fixed; + background-size: cover; + background-position: center; + } + + .transition-def { + transition: all 500ms; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 80274a3..c918e0a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,32 +1,84 @@ -import type { Metadata } from "next" -import { Inter } from "next/font/google" -import "./globals.css" -import { getSiteSettings } from "@/lib/api" +import type { Metadata } from 'next' +import { Montserrat, Open_Sans } from 'next/font/google' +import { Navigation } from '@/components/Navigation' +import { Footer } from '@/components/Footer' +import { getNavigation, getSiteSettings, getSocialLinks } from '@/lib/api' +import './globals.css' -const inter = Inter({ - variable: "--font-inter", - subsets: ["latin"], +const montserrat = Montserrat({ + subsets: ['latin'], + weight: ['400', '700'], + variable: '--font-montserrat', + display: 'swap', }) -export async function generateMetadata(): Promise { - const settings = await getSiteSettings() - return { - title: { - default: settings?.siteName || "Caroline Porwoll", - template: `%s | ${settings?.siteName || "Caroline Porwoll"}`, - }, - description: "Professionelle Portrait- und Businessfotografie", - } +const openSans = Open_Sans({ + subsets: ['latin'], + weight: ['300', '400', '600', '700', '800'], + variable: '--font-open-sans', + display: 'swap', +}) + +export const metadata: Metadata = { + title: 'Martin Porwoll', + description: 'Whistleblower. Unternehmer. Mensch.', } -export default function RootLayout({ +export default async function RootLayout({ children, -}: Readonly<{ +}: { children: React.ReactNode -}>) { +}) { + const [navigation, siteSettings, socialLinks] = await Promise.all([ + getNavigation('header'), + getSiteSettings(), + getSocialLinks(), + ]) + + // Transform CMS navigation items into NavItem format + const nav = navigation as unknown as Record | null + const navItems = nav?.items + ? (nav.items as Array>).map( + (item) => ({ + label: (item.label as string) || '', + href: (item.link as string) || (item.url as string) || '#', + }) + ) + : [ + { label: 'Home', href: '/' }, + { label: 'Der Mensch', href: '/mensch' }, + { label: 'Whistleblowing', href: '/whistleblowing' }, + { label: 'Kontakt', href: '/kontakt' }, + ] + + const settings = siteSettings as unknown as Record | null + const logoMedia = settings?.logo as Record | undefined + const logoUrl = logoMedia?.url as string | undefined + + const contactInfo = settings?.contactInfo as Record | undefined + return ( - - {children} + + + + +
+ {children} +
+ +