Simplify hero backgrounds and add entrance animations
- Replace animated blob/wave/noise backdrop with clean static gradient (radial ellipse highlights + linear fade) in hero-motion-backdrop - Remove all framer-motion from background layer; delete unused CSS classes: hero-blob, hero-sheet-glow, hero-sheet-wave, hero-sheet-noise, hero-title-shift keyframe, and responsive blob media queries - Add framer-motion stagger entrance to HeroContentMotion, HeroMotionItem, and HeroTitle (slide-up + fade on mount) - Refactor coming-soon page to use HeroContentMotion/HeroMotionItem instead of MotionFade (whileInView unreliable above the fold) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,10 +61,7 @@ export default async function PortfolioItemPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHero
|
<PageHero />
|
||||||
title={getLocalizedValue(item.title, localeKey)}
|
|
||||||
description={getLocalizedValue(item.summary, localeKey)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Container size="wide" className="pb-12 lg:pb-16">
|
<Container size="wide" className="pb-12 lg:pb-16">
|
||||||
<PortfolioProjectDetail
|
<PortfolioProjectDetail
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { Sparkles } from "lucide-react";
|
|||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { FloatingPreferences } from "@/components/layout/floating-preferences";
|
import { FloatingPreferences } from "@/components/layout/floating-preferences";
|
||||||
import { MotionFade } from "@/components/motion-fade";
|
import { HeroContentMotion, HeroMotionItem, HeroShell, HeroTitle } from "@/components/layout/site-hero";
|
||||||
import { HeroShell, HeroTitle } from "@/components/layout/site-hero";
|
|
||||||
import { getSiteSettings } from "@/lib/app-config";
|
import { getSiteSettings } from "@/lib/app-config";
|
||||||
import { buildLocalizedMetadata } from "@/lib/metadata";
|
import { buildLocalizedMetadata } from "@/lib/metadata";
|
||||||
import { resolveLocale } from "@/lib/locale";
|
import { resolveLocale } from "@/lib/locale";
|
||||||
@@ -67,24 +66,30 @@ export default async function ComingSoonPage({
|
|||||||
<FloatingPreferences locale={localeKey} defaultLocale={siteSettings.defaultLocale} />
|
<FloatingPreferences locale={localeKey} defaultLocale={siteSettings.defaultLocale} />
|
||||||
|
|
||||||
<HeroShell className="min-h-screen" showBridges>
|
<HeroShell className="min-h-screen" showBridges>
|
||||||
<MotionFade className="relative z-10 w-full">
|
<HeroContentMotion className="relative z-10 w-full">
|
||||||
<div className="mx-auto max-w-[56rem] text-center">
|
<div className="mx-auto max-w-[56rem] text-center">
|
||||||
<p className="inline-flex items-center gap-2 rounded-pill border border-foreground/8 bg-background/55 px-4 py-2 text-sm font-medium tracking-[0.08em] text-[hsl(var(--hero-ink)/0.84)] shadow-[var(--shadow-sm)] backdrop-blur-[18px] dark:border-foreground/10 dark:bg-background/20 dark:text-foreground/84">
|
<HeroMotionItem>
|
||||||
<Sparkles className="h-3.5 w-3.5 text-brand-secondary" />
|
<p className="inline-flex items-center gap-2 rounded-pill border border-foreground/8 bg-background/55 px-4 py-2 text-sm font-medium tracking-[0.08em] text-[hsl(var(--hero-ink)/0.84)] shadow-[var(--shadow-sm)] backdrop-blur-[18px] dark:border-foreground/10 dark:bg-background/20 dark:text-foreground/84">
|
||||||
{t("kicker")}
|
<Sparkles className="h-3.5 w-3.5 text-brand-secondary" />
|
||||||
</p>
|
{t("kicker")}
|
||||||
|
</p>
|
||||||
|
</HeroMotionItem>
|
||||||
|
|
||||||
<HeroTitle locale={localeKey} lines={lines} className="mx-auto mt-8" />
|
<HeroTitle locale={localeKey} lines={lines} className="mx-auto mt-8" />
|
||||||
|
|
||||||
<p className="mx-auto mt-8 max-w-[34rem] text-sm leading-7 text-foreground/68 sm:text-base">
|
<HeroMotionItem>
|
||||||
{t("description")}
|
<p className="mx-auto mt-8 max-w-[34rem] text-sm leading-7 text-foreground/68 sm:text-base">
|
||||||
</p>
|
{t("description")}
|
||||||
|
</p>
|
||||||
|
</HeroMotionItem>
|
||||||
|
|
||||||
<p className="mx-auto mt-4 max-w-[32rem] text-sm leading-6 text-foreground/54 sm:text-base">
|
<HeroMotionItem>
|
||||||
{t("note")}
|
<p className="mx-auto mt-4 max-w-[32rem] text-sm leading-6 text-foreground/54 sm:text-base">
|
||||||
</p>
|
{t("note")}
|
||||||
|
</p>
|
||||||
|
</HeroMotionItem>
|
||||||
</div>
|
</div>
|
||||||
</MotionFade>
|
</HeroContentMotion>
|
||||||
</HeroShell>
|
</HeroShell>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ export async function deleteProjectAction(formData: FormData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
redirect(withMessage(getAdminAppPath("/portfolio"), "error", "Project not found."));
|
redirect(withMessage(getAdminAppPath("/portfolio"), "error", "Projekt nicht gefunden."));
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.portfolioProject.delete({
|
await prisma.portfolioProject.delete({
|
||||||
|
|||||||
+10
-133
@@ -266,91 +266,21 @@ html[lang="ar"] {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 14% 18%, hsl(var(--hero-left) / 0.42), transparent 26%),
|
radial-gradient(ellipse 90% 55% at 50% -5%, hsl(var(--hero-left) / 0.38), transparent),
|
||||||
radial-gradient(circle at 84% 14%, hsl(var(--hero-right) / 0.28), transparent 24%),
|
radial-gradient(ellipse 55% 40% at 92% 10%, hsl(var(--hero-right) / 0.22), transparent),
|
||||||
radial-gradient(circle at 52% 84%, hsl(var(--hero-bottom) / 0.24), transparent 32%),
|
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)) 88%);
|
linear-gradient(180deg, hsl(var(--hero-base)), hsl(var(--background)) 90%);
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-sheet-veil {
|
.hero-sheet-veil {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background:
|
background: linear-gradient(
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.025) 44%, rgba(255, 255, 255, 0));
|
160deg,
|
||||||
backdrop-filter: blur(20px) saturate(112%);
|
rgba(255, 255, 255, 0.06) 0%,
|
||||||
-webkit-backdrop-filter: blur(20px) saturate(112%);
|
rgba(255, 255, 255, 0.02) 40%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-sheet-fade {
|
.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));
|
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 {
|
@keyframes hero-scroll-bob {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ const sectionTypeOptions: PortfolioSectionType[] = [
|
|||||||
"LINK",
|
"LINK",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const sectionTypeLabels: Record<PortfolioSectionType, string> = {
|
||||||
|
RICH_TEXT: "Rich Text",
|
||||||
|
GALLERY: "Gallery (image)",
|
||||||
|
STATS: "Stats (text)",
|
||||||
|
DELIVERABLES: "Deliverables (text)",
|
||||||
|
LINK: "Link",
|
||||||
|
};
|
||||||
|
|
||||||
const viewModeOptions: Array<{
|
const viewModeOptions: Array<{
|
||||||
value: PortfolioProjectViewMode;
|
value: PortfolioProjectViewMode;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -137,7 +145,7 @@ const viewModeOptions: Array<{
|
|||||||
}> = [
|
}> = [
|
||||||
{ value: "GRID", label: "Grid", description: "Balanced modular layout.", icon: Grid2x2 },
|
{ value: "GRID", label: "Grid", description: "Balanced modular layout.", icon: Grid2x2 },
|
||||||
{ value: "STORY", label: "Story", description: "Narrative section flow.", icon: BookOpenText },
|
{ 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<{
|
const wizardSteps: Array<{
|
||||||
@@ -1046,7 +1054,7 @@ export function PortfolioProjectForm({
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
{sectionTypeOptions.map((type) => (
|
{sectionTypeOptions.map((type) => (
|
||||||
<SelectItem key={type} value={type}>
|
<SelectItem key={type} value={type}>
|
||||||
{type}
|
{sectionTypeLabels[type]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
type HeroMotionBackdropProps = {
|
type HeroMotionBackdropProps = {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
};
|
};
|
||||||
@@ -8,13 +6,6 @@ export function HeroMotionBackdrop({}: HeroMotionBackdropProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="hero-sheet pointer-events-none absolute inset-0 overflow-hidden">
|
<div className="hero-sheet pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
<div className="hero-sheet-base" />
|
<div className="hero-sheet-base" />
|
||||||
<div className="hero-blob hero-blob-left" />
|
|
||||||
<div className="hero-blob hero-blob-right" />
|
|
||||||
<div className="hero-blob hero-blob-bottom" />
|
|
||||||
<div className="hero-sheet-glow" />
|
|
||||||
<div className="hero-sheet-wave" />
|
|
||||||
<div className="hero-sheet-noise" />
|
|
||||||
|
|
||||||
<div className="hero-sheet-veil" />
|
<div className="hero-sheet-veil" />
|
||||||
<div className="hero-sheet-fade" />
|
<div className="hero-sheet-fade" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { HeroContentMotion, HeroMotionItem, HeroShell, HeroTitle, type HeroTitle
|
|||||||
|
|
||||||
type PageHeroProps = {
|
type PageHeroProps = {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
title: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
lines?: HeroTitleLineValue[];
|
lines?: HeroTitleLineValue[];
|
||||||
locale?: string;
|
locale?: string;
|
||||||
@@ -33,13 +33,19 @@ function getDefaultLines(title: string): HeroTitleLineValue[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PageHero({ badge, title, description, lines, locale }: PageHeroProps) {
|
export function PageHero({ badge, title, description, lines, locale }: PageHeroProps) {
|
||||||
|
const resolvedLines = lines ?? (title ? getDefaultLines(title) : undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeroShell compactBackdrop variant="page">
|
<HeroShell compactBackdrop variant="page">
|
||||||
<HeroContentMotion variant="page">
|
<HeroContentMotion variant="page">
|
||||||
<HeroMotionItem variant="page">
|
{(badge ?? title) ? (
|
||||||
<HeroBadge>{badge ?? title}</HeroBadge>
|
<HeroMotionItem variant="page">
|
||||||
</HeroMotionItem>
|
<HeroBadge>{badge ?? title}</HeroBadge>
|
||||||
<HeroTitle lines={lines ?? getDefaultLines(title)} locale={locale} variant="page" />
|
</HeroMotionItem>
|
||||||
|
) : null}
|
||||||
|
{resolvedLines ? (
|
||||||
|
<HeroTitle lines={resolvedLines} locale={locale} variant="page" />
|
||||||
|
) : null}
|
||||||
{description ? (
|
{description ? (
|
||||||
<HeroMotionItem variant="page">
|
<HeroMotionItem variant="page">
|
||||||
<p className="mx-auto mt-6 max-w-[34rem] text-base leading-7 text-foreground/72">
|
<p className="mx-auto mt-6 max-w-[34rem] text-base leading-7 text-foreground/72">
|
||||||
|
|||||||
@@ -3,11 +3,28 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { ArrowDown } from "lucide-react";
|
import { ArrowDown } from "lucide-react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { Container } from "@/components/layout/container";
|
import { Container } from "@/components/layout/container";
|
||||||
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
|
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
|
||||||
import { cn } from "@/lib/utils";
|
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 HeroTone = "default" | "accent" | "soft";
|
||||||
type HeroLineAlign = "start" | "center" | "end";
|
type HeroLineAlign = "start" | "center" | "end";
|
||||||
|
|
||||||
@@ -119,14 +136,27 @@ export function HeroContentMotion({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: HeroContentMotionProps) {
|
}: HeroContentMotionProps) {
|
||||||
return <div className={className}>{children}</div>;
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={className}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
variants={heroContainerVariants}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeroMotionItem({
|
export function HeroMotionItem({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: HeroContentMotionProps) {
|
}: HeroContentMotionProps) {
|
||||||
return <div className={className}>{children}</div>;
|
return (
|
||||||
|
<motion.div className={className} variants={heroItemVariants}>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeroTitle({
|
export function HeroTitle({
|
||||||
@@ -147,7 +177,7 @@ export function HeroTitle({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h1 className={titleClassName}>
|
<motion.h1 className={titleClassName} variants={heroItemVariants}>
|
||||||
{lines.map((line, index) => (
|
{lines.map((line, index) => (
|
||||||
<span
|
<span
|
||||||
key={`${index}-${line.text}`}
|
key={`${index}-${line.text}`}
|
||||||
@@ -172,7 +202,7 @@ export function HeroTitle({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</h1>
|
</motion.h1>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ function PortfolioImage({
|
|||||||
alt={alt}
|
alt={alt}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
unoptimized
|
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -121,11 +120,7 @@ function SectionBlock({
|
|||||||
height={880}
|
height={880}
|
||||||
className="h-72 w-full rounded-surface object-cover"
|
className="h-72 w-full rounded-surface object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : null}
|
||||||
<div className="rounded-surface border border-dashed border-border px-4 py-12 text-center text-sm text-muted-foreground">
|
|
||||||
No image configured.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -216,7 +211,7 @@ function AssetGallery({
|
|||||||
function BadgeCount({ count }: { count: number }) {
|
function BadgeCount({ count }: { count: number }) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-pill border border-border/70 bg-background px-4 py-2 text-sm text-muted-foreground">
|
<div className="rounded-pill border border-border/70 bg-background px-4 py-2 text-sm text-muted-foreground">
|
||||||
{count}
|
{count} {count === 1 ? "file" : "files"}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -6,6 +6,8 @@ import type {
|
|||||||
PortfolioSection,
|
PortfolioSection,
|
||||||
} from "@prisma/client";
|
} from "@prisma/client";
|
||||||
|
|
||||||
|
import { cache } from "react";
|
||||||
|
|
||||||
import { getPortfolioMediaBindings } from "@/lib/media";
|
import { getPortfolioMediaBindings } from "@/lib/media";
|
||||||
import type { AppLocale } from "@/lib/locale";
|
import type { AppLocale } from "@/lib/locale";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
@@ -329,7 +331,7 @@ export async function getPublishedPortfolioProjects(filters?: { categorySlug?: s
|
|||||||
return projects.map((project) => mapProject(project));
|
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({
|
const project = await prisma.portfolioProject.findFirst({
|
||||||
where: {
|
where: {
|
||||||
slug,
|
slug,
|
||||||
@@ -350,7 +352,7 @@ export async function getPublishedPortfolioProjectBySlug(slug: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return project ? mapProject(project) : null;
|
return project ? mapProject(project) : null;
|
||||||
}
|
});
|
||||||
|
|
||||||
export async function getAdminPortfolioProjectById(id: string) {
|
export async function getAdminPortfolioProjectById(id: string) {
|
||||||
const project = await prisma.portfolioProject.findUnique({
|
const project = await prisma.portfolioProject.findUnique({
|
||||||
|
|||||||
Reference in New Issue
Block a user