diff --git a/app/[locale]/(site)/portfolio/[slug]/page.tsx b/app/[locale]/(site)/portfolio/[slug]/page.tsx index f96a499..741b7ac 100644 --- a/app/[locale]/(site)/portfolio/[slug]/page.tsx +++ b/app/[locale]/(site)/portfolio/[slug]/page.tsx @@ -61,10 +61,7 @@ export default async function PortfolioItemPage({ return ( <> - + - +
-

- - {t("kicker")} -

+ +

+ + {t("kicker")} +

+
-

- {t("description")} -

+ +

+ {t("description")} +

+
-

- {t("note")} -

+ +

+ {t("note")} +

+
-
+
); diff --git a/app/_admin/portfolio/actions.ts b/app/_admin/portfolio/actions.ts index 46c10f7..97c2c48 100644 --- a/app/_admin/portfolio/actions.ts +++ b/app/_admin/portfolio/actions.ts @@ -582,7 +582,7 @@ export async function deleteProjectAction(formData: FormData) { }); if (!project) { - redirect(withMessage(getAdminAppPath("/portfolio"), "error", "Project not found.")); + redirect(withMessage(getAdminAppPath("/portfolio"), "error", "Projekt nicht gefunden.")); } await prisma.portfolioProject.delete({ diff --git a/app/globals.css b/app/globals.css index 42648a9..53600e6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -266,91 +266,21 @@ html[lang="ar"] { position: absolute; inset: 0; background: - radial-gradient(circle at 14% 18%, hsl(var(--hero-left) / 0.42), transparent 26%), - radial-gradient(circle at 84% 14%, hsl(var(--hero-right) / 0.28), transparent 24%), - radial-gradient(circle at 52% 84%, hsl(var(--hero-bottom) / 0.24), transparent 32%), - linear-gradient(180deg, hsl(var(--hero-base)), hsl(var(--background)) 88%); - } - - .hero-blob { - position: absolute; - border-radius: 9999px; - filter: blur(110px); - will-change: transform, opacity; - } - - .hero-blob-left { - left: -8%; - top: 8%; - height: 24rem; - width: 24rem; - background: radial-gradient(circle, hsl(var(--hero-left) / 0.48), transparent 68%); - } - - .hero-blob-right { - right: -10%; - top: 2%; - height: 22rem; - width: 22rem; - background: radial-gradient(circle, hsl(var(--hero-right) / 0.36), transparent 70%); - } - - .hero-blob-bottom { - left: 18%; - bottom: -10%; - height: 24rem; - width: 38rem; - background: radial-gradient(circle, hsl(var(--hero-bottom) / 0.32), transparent 70%); - } - - .hero-sheet-glow { - position: absolute; - inset: -10% -8%; - background: - radial-gradient(circle at 18% 24%, hsl(var(--hero-left) / 0.24), transparent 18%), - radial-gradient(circle at 86% 18%, hsl(var(--hero-right) / 0.16), transparent 18%), - radial-gradient(circle at 50% 78%, hsl(var(--hero-bottom) / 0.14), transparent 24%); - filter: blur(36px); - will-change: transform, opacity; - } - - .hero-sheet-wave { - position: absolute; - inset: -16% -10%; - background: - conic-gradient( - from 180deg at 50% 50%, - transparent 0deg, - hsl(var(--hero-left) / 0.08) 58deg, - transparent 120deg, - transparent 228deg, - hsl(var(--hero-right) / 0.1) 300deg, - transparent 360deg - ); - filter: blur(90px); - mix-blend-mode: screen; - will-change: transform, opacity; - } - - .hero-sheet-noise { - position: absolute; - inset: 0; - background-image: - linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)), - url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23noise)' opacity='0.1'/%3E%3C/svg%3E"); - background-size: auto, 160px 160px; - mix-blend-mode: soft-light; - opacity: 0.22; - will-change: background-position, opacity, transform; + radial-gradient(ellipse 90% 55% at 50% -5%, hsl(var(--hero-left) / 0.38), transparent), + radial-gradient(ellipse 55% 40% at 92% 10%, hsl(var(--hero-right) / 0.22), transparent), + radial-gradient(ellipse 60% 35% at 8% 15%, hsl(var(--hero-left) / 0.18), transparent), + linear-gradient(180deg, hsl(var(--hero-base)), hsl(var(--background)) 90%); } .hero-sheet-veil { position: absolute; inset: 0; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.025) 44%, rgba(255, 255, 255, 0)); - backdrop-filter: blur(20px) saturate(112%); - -webkit-backdrop-filter: blur(20px) saturate(112%); + background: linear-gradient( + 160deg, + rgba(255, 255, 255, 0.06) 0%, + rgba(255, 255, 255, 0.02) 40%, + rgba(255, 255, 255, 0) 100% + ); } .hero-sheet-fade { @@ -439,20 +369,6 @@ html[lang="ar"] { background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06)); } - @keyframes hero-title-shift { - 0% { - background-position: 0% 50%; - } - - 50% { - background-position: 100% 50%; - } - - 100% { - background-position: 45% 100%; - } - } - @keyframes hero-scroll-bob { 0%, 100% { @@ -627,42 +543,3 @@ html[lang="ar"] { } } -@media (min-width: 640px) { - .hero-blob-left { - height: 30rem; - width: 30rem; - } - - .hero-blob-right { - height: 28rem; - width: 28rem; - } - - .hero-blob-bottom { - height: 28rem; - width: 44rem; - } -} - -@media (min-width: 1024px) { - .hero-blob-left { - left: -4%; - top: 10%; - height: 36rem; - width: 36rem; - } - - .hero-blob-right { - right: -4%; - top: 0; - height: 34rem; - width: 34rem; - } - - .hero-blob-bottom { - left: 14%; - bottom: -16%; - height: 32rem; - width: 52rem; - } -} diff --git a/components/admin/portfolio-project-form.tsx b/components/admin/portfolio-project-form.tsx index 89cce35..d13d635 100644 --- a/components/admin/portfolio-project-form.tsx +++ b/components/admin/portfolio-project-form.tsx @@ -129,6 +129,14 @@ const sectionTypeOptions: PortfolioSectionType[] = [ "LINK", ]; +const sectionTypeLabels: Record = { + RICH_TEXT: "Rich Text", + GALLERY: "Gallery (image)", + STATS: "Stats (text)", + DELIVERABLES: "Deliverables (text)", + LINK: "Link", +}; + const viewModeOptions: Array<{ value: PortfolioProjectViewMode; label: string; @@ -137,7 +145,7 @@ const viewModeOptions: Array<{ }> = [ { value: "GRID", label: "Grid", description: "Balanced modular layout.", icon: Grid2x2 }, { value: "STORY", label: "Story", description: "Narrative section flow.", icon: BookOpenText }, - { value: "CASE_STUDY", label: "Case Study", description: "Structured challenge and result view.", icon: BriefcaseBusiness }, + { value: "CASE_STUDY", label: "Case Study", description: "Structured challenge/solution/outcome view. Sections 1–3 become the lead panels.", icon: BriefcaseBusiness }, ]; const wizardSteps: Array<{ @@ -1046,7 +1054,7 @@ export function PortfolioProjectForm({ {sectionTypeOptions.map((type) => ( - {type} + {sectionTypeLabels[type]} ))} diff --git a/components/layout/hero-motion-backdrop.tsx b/components/layout/hero-motion-backdrop.tsx index 8c2c44a..221a76b 100644 --- a/components/layout/hero-motion-backdrop.tsx +++ b/components/layout/hero-motion-backdrop.tsx @@ -1,5 +1,3 @@ -"use client"; - type HeroMotionBackdropProps = { compact?: boolean; }; @@ -8,13 +6,6 @@ export function HeroMotionBackdrop({}: HeroMotionBackdropProps) { return (
-
-
-
-
-
-
-
diff --git a/components/layout/page-hero.tsx b/components/layout/page-hero.tsx index 23f6238..8c069d0 100644 --- a/components/layout/page-hero.tsx +++ b/components/layout/page-hero.tsx @@ -3,7 +3,7 @@ import { HeroContentMotion, HeroMotionItem, HeroShell, HeroTitle, type HeroTitle type PageHeroProps = { badge?: string; - title: string; + title?: string; description?: string; lines?: HeroTitleLineValue[]; locale?: string; @@ -33,13 +33,19 @@ function getDefaultLines(title: string): HeroTitleLineValue[] { } export function PageHero({ badge, title, description, lines, locale }: PageHeroProps) { + const resolvedLines = lines ?? (title ? getDefaultLines(title) : undefined); + return ( - - {badge ?? title} - - + {(badge ?? title) ? ( + + {badge ?? title} + + ) : null} + {resolvedLines ? ( + + ) : null} {description ? (

diff --git a/components/layout/site-hero.tsx b/components/layout/site-hero.tsx index a8525b7..772039a 100644 --- a/components/layout/site-hero.tsx +++ b/components/layout/site-hero.tsx @@ -3,11 +3,28 @@ import type { ReactNode } from "react"; import { ArrowDown } from "lucide-react"; +import { motion } from "framer-motion"; import { Container } from "@/components/layout/container"; import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop"; import { cn } from "@/lib/utils"; +const heroItemVariants = { + hidden: { opacity: 0, y: 16 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.54, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] }, + }, +}; + +const heroContainerVariants = { + hidden: {}, + visible: { + transition: { staggerChildren: 0.13, delayChildren: 0.08 }, + }, +}; + type HeroTone = "default" | "accent" | "soft"; type HeroLineAlign = "start" | "center" | "end"; @@ -119,14 +136,27 @@ export function HeroContentMotion({ children, className, }: HeroContentMotionProps) { - return

{children}
; + return ( + + {children} + + ); } export function HeroMotionItem({ children, className, }: HeroContentMotionProps) { - return
{children}
; + return ( + + {children} + + ); } export function HeroTitle({ @@ -147,7 +177,7 @@ export function HeroTitle({ ); return ( -

+ {lines.map((line, index) => ( ))} -

+ ); } diff --git a/components/site/portfolio-project-detail.tsx b/components/site/portfolio-project-detail.tsx index 67bb2c9..e4bd014 100644 --- a/components/site/portfolio-project-detail.tsx +++ b/components/site/portfolio-project-detail.tsx @@ -47,7 +47,6 @@ function PortfolioImage({ alt={alt} width={width} height={height} - unoptimized className={className} /> ); @@ -121,11 +120,7 @@ function SectionBlock({ height={880} className="h-72 w-full rounded-surface object-cover" /> - ) : ( -
- No image configured. -
- )} + ) : null}
); } @@ -216,7 +211,7 @@ function AssetGallery({ function BadgeCount({ count }: { count: number }) { return (
- {count} + {count} {count === 1 ? "file" : "files"}
); } diff --git a/lib/portfolio.ts b/lib/portfolio.ts index c420d46..7a8b887 100644 --- a/lib/portfolio.ts +++ b/lib/portfolio.ts @@ -6,6 +6,8 @@ import type { PortfolioSection, } from "@prisma/client"; +import { cache } from "react"; + import { getPortfolioMediaBindings } from "@/lib/media"; import type { AppLocale } from "@/lib/locale"; import { prisma } from "@/lib/prisma"; @@ -329,7 +331,7 @@ export async function getPublishedPortfolioProjects(filters?: { categorySlug?: s return projects.map((project) => mapProject(project)); } -export async function getPublishedPortfolioProjectBySlug(slug: string) { +export const getPublishedPortfolioProjectBySlug = cache(async function (slug: string) { const project = await prisma.portfolioProject.findFirst({ where: { slug, @@ -350,7 +352,7 @@ export async function getPublishedPortfolioProjectBySlug(slug: string) { }); return project ? mapProject(project) : null; -} +}); export async function getAdminPortfolioProjectById(id: string) { const project = await prisma.portfolioProject.findUnique({