Add marketing motion reveals to blog and occasions

This commit is contained in:
Codex Agent
2026-01-21 15:22:39 +01:00
parent 5eb0941512
commit a01a7ec399
16 changed files with 1869 additions and 781 deletions

View File

@@ -7,6 +7,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Button } from '@/components/ui/button';
import { motion, useReducedMotion } from 'framer-motion';
interface AdjacentPost {
slug: string;
@@ -58,6 +59,20 @@ const BlogShow: React.FC<Props> = ({ post }) => {
const [copied, setCopied] = React.useState(false);
const locale = i18n.language || 'de';
const dateLocale = locale === 'en' ? 'en-US' : 'de-DE';
const shouldReduceMotion = useReducedMotion();
const viewportOnce = { once: true, amount: 0.25 };
const revealUp = {
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 18 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
},
};
const stagger = {
hidden: {},
visible: { transition: { staggerChildren: 0.12 } },
};
const formattedDate = React.useMemo(() => {
try {
@@ -121,9 +136,9 @@ const BlogShow: React.FC<Props> = ({ post }) => {
<MarketingLayout title={`${post.title} ${t('title_suffix')}`}>
<Head title={`${post.title} ${t('title_suffix')}`} />
<section className="bg-gradient-to-br from-pink-50 via-white to-amber-50 px-4 py-8 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 md:py-10">
<div className="container mx-auto max-w-5xl space-y-5">
<div className="flex flex-wrap items-center justify-between gap-3 text-sm text-gray-500 dark:text-gray-400">
<section className="bg-gradient-to-br from-pink-50 via-white to-amber-50 px-4 py-8 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 md:py-10 [data-slot=card]:transition-transform [data-slot=card]:duration-200 [data-slot=card]:ease-out [data-slot=card]:hover:-translate-y-1">
<motion.div className="container mx-auto max-w-5xl space-y-5" variants={stagger} initial="hidden" animate="visible">
<motion.div className="flex flex-wrap items-center justify-between gap-3 text-sm text-gray-500 dark:text-gray-400" variants={revealUp}>
<nav className="flex flex-wrap items-center gap-2">
<Link href={localizedPath('/')} className="hover:text-gray-900 dark:hover:text-gray-100">
{t('breadcrumb_home')}
@@ -138,51 +153,59 @@ const BlogShow: React.FC<Props> = ({ post }) => {
<Link href={localizedPath('/blog')} className="text-pink-600 hover:text-pink-700">
{t('back_to_blog')}
</Link>
</div>
</motion.div>
<Card className="rounded-[32px] border border-white/60 bg-white/80 text-gray-900 shadow-xl backdrop-blur dark:border-gray-800 dark:bg-gray-900/80 dark:text-gray-50">
<CardContent className="space-y-6 p-6 md:p-8">
<div className="flex flex-col gap-6 md:grid md:grid-cols-[minmax(0,1fr)_240px] md:items-center md:gap-8">
<div className="space-y-4 text-left">
<Badge variant="outline" className="border-pink-200 bg-white/70 text-pink-600 dark:border-pink-500/60 dark:bg-gray-900/80 dark:text-pink-200">
Fotospiel Stories
</Badge>
<h1 className="text-3xl font-bold leading-tight text-gray-900 dark:text-gray-50 md:text-4xl">{post.title}</h1>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-gray-600 dark:text-gray-300">
<span>{t('by_author')} {post.author?.name || t('team')}</span>
<span className="hidden text-gray-400 sm:inline"></span>
<span>{t('published_on')} {formattedDate}</span>
</div>
<MarkdownPreview
html={post.excerpt_html}
fallback={post.excerpt}
className="text-base leading-relaxed text-gray-700 dark:text-gray-100"
/>
</div>
<div className="relative">
{post.featured_image ? (
<div className="rounded-3xl border border-white/80 bg-white/60 p-2 shadow-2xl dark:border-gray-800/80 dark:bg-gray-900/80">
<img
src={post.featured_image}
alt={post.title}
className="h-full w-full rounded-2xl object-cover"
/>
</div>
) : (
<div className="rounded-3xl border border-dashed border-pink-200 bg-gradient-to-br from-pink-100 via-white to-amber-100 p-8 text-center text-lg font-semibold text-pink-700 shadow-lg dark:border-pink-500/40 dark:from-pink-900/30 dark:via-gray-900 dark:to-gray-900">
<motion.div variants={revealUp}>
<Card className="rounded-[32px] border border-white/60 bg-white/80 text-gray-900 shadow-xl backdrop-blur dark:border-gray-800 dark:bg-gray-900/80 dark:text-gray-50">
<CardContent className="space-y-6 p-6 md:p-8">
<div className="flex flex-col gap-6 md:grid md:grid-cols-[minmax(0,1fr)_240px] md:items-center md:gap-8">
<div className="space-y-4 text-left">
<Badge variant="outline" className="border-pink-200 bg-white/70 text-pink-600 dark:border-pink-500/60 dark:bg-gray-900/80 dark:text-pink-200">
Fotospiel Stories
</Badge>
<h1 className="text-3xl font-bold leading-tight text-gray-900 dark:text-gray-50 md:text-4xl">{post.title}</h1>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-gray-600 dark:text-gray-300">
<span>{t('by_author')} {post.author?.name || t('team')}</span>
<span className="hidden text-gray-400 sm:inline"></span>
<span>{t('published_on')} {formattedDate}</span>
</div>
)}
<MarkdownPreview
html={post.excerpt_html}
fallback={post.excerpt}
className="text-base leading-relaxed text-gray-700 dark:text-gray-100"
/>
</div>
<div className="relative transition-transform duration-200 ease-out hover:-translate-y-1">
{post.featured_image ? (
<div className="rounded-3xl border border-white/80 bg-white/60 p-2 shadow-2xl dark:border-gray-800/80 dark:bg-gray-900/80">
<img
src={post.featured_image}
alt={post.title}
className="h-full w-full rounded-2xl object-cover"
/>
</div>
) : (
<div className="rounded-3xl border border-dashed border-pink-200 bg-gradient-to-br from-pink-100 via-white to-amber-100 p-8 text-center text-lg font-semibold text-pink-700 shadow-lg dark:border-pink-500/40 dark:from-pink-900/30 dark:via-gray-900 dark:to-gray-900">
Fotospiel Stories
</div>
)}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</motion.div>
</motion.div>
</section>
<section className="bg-white px-4 py-12 dark:bg-gray-950">
<div className="container mx-auto max-w-6xl gap-10 lg:grid lg:grid-cols-[minmax(0,1fr)_320px]">
<article className="rounded-[32px] border border-gray-100 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<section className="bg-white px-4 py-12 dark:bg-gray-950 [data-slot=card]:transition-transform [data-slot=card]:duration-200 [data-slot=card]:ease-out [data-slot=card]:hover:-translate-y-1">
<motion.div
className="container mx-auto max-w-6xl gap-10 lg:grid lg:grid-cols-[minmax(0,1fr)_320px]"
variants={stagger}
initial="hidden"
whileInView="visible"
viewport={viewportOnce}
>
<motion.article className="rounded-[32px] border border-gray-100 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900" variants={revealUp}>
<div
className="prose prose-lg prose-slate max-w-none
prose-headings:text-slate-900 prose-headings:font-semibold
@@ -200,139 +223,151 @@ const BlogShow: React.FC<Props> = ({ post }) => {
dark:prose-li:text-gray-100 dark:prose-blockquote:border-pink-400"
dangerouslySetInnerHTML={{ __html: post.content_html }}
/>
</article>
</motion.article>
<aside className="mt-10 space-y-6 lg:mt-0 lg:sticky lg:top-24">
<Card className="border-gray-100 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<CardContent className="space-y-3 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t('summary_title')}
</p>
<MarkdownPreview
html={post.excerpt_html}
fallback={post.excerpt}
className="text-base leading-relaxed text-gray-700 dark:text-gray-100"
/>
</CardContent>
</Card>
<Card className="border-gray-100 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<CardContent className="space-y-4 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t('toc_title')}
</p>
{post.headings && post.headings.length > 0 ? (
<ul className="space-y-3 text-sm text-gray-700 dark:text-gray-100">
{post.headings.map((heading) => (
<li key={heading.slug} className="border-l-2 border-pink-100 pl-3 dark:border-pink-500/40">
<a href={`#${heading.slug}`} className="hover:text-pink-600">
{heading.text}
</a>
</li>
))}
</ul>
) : (
<p className="text-sm text-gray-500 dark:text-gray-400">{t('toc_empty')}</p>
)}
</CardContent>
</Card>
<Card className="border-gray-100 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<CardContent className="space-y-3 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t('sidebar_author_title')}
</p>
<p className="text-lg font-semibold text-gray-900 dark:text-gray-50">{post.author?.name || t('team')}</p>
<p className="text-sm text-gray-600 dark:text-gray-300">{t('sidebar_author_description')}</p>
</CardContent>
</Card>
<Card className="border-gray-100 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<CardContent className="space-y-4 p-6">
<div>
<motion.aside className="mt-10 space-y-6 lg:mt-0 lg:sticky lg:top-24" variants={stagger}>
<motion.div variants={revealUp}>
<Card className="border-gray-100 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<CardContent className="space-y-3 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t('share_title')}
{t('summary_title')}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{t('share_hint')}</p>
</div>
<div className="flex flex-wrap gap-3">
<Button variant="outline" size="sm" onClick={handleCopyLink} className="border-pink-200 text-pink-600 dark:border-pink-500/50 dark:text-pink-200">
{copied ? t('share_copied') : t('share_copy')}
</Button>
{canUseNativeShare && (
<Button
variant="ghost"
size="sm"
className="text-gray-700 hover:text-pink-600 dark:text-gray-200"
onClick={() => navigator.share?.({ title: post.title, url: shareUrl })}
>
{t('share_native')}
</Button>
<MarkdownPreview
html={post.excerpt_html}
fallback={post.excerpt}
className="text-base leading-relaxed text-gray-700 dark:text-gray-100"
/>
</CardContent>
</Card>
</motion.div>
<motion.div variants={revealUp}>
<Card className="border-gray-100 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<CardContent className="space-y-4 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t('toc_title')}
</p>
{post.headings && post.headings.length > 0 ? (
<ul className="space-y-3 text-sm text-gray-700 dark:text-gray-100">
{post.headings.map((heading) => (
<li key={heading.slug} className="border-l-2 border-pink-100 pl-3 dark:border-pink-500/40">
<a href={`#${heading.slug}`} className="hover:text-pink-600">
{heading.text}
</a>
</li>
))}
</ul>
) : (
<p className="text-sm text-gray-500 dark:text-gray-400">{t('toc_empty')}</p>
)}
</div>
<Separator className="my-2" />
<div className="flex flex-col gap-2">
{shareLinks.map((link) => (
<Button key={link.key} variant="outline" size="sm" asChild className="justify-start border-gray-200 text-gray-700 hover:border-pink-200 hover:text-pink-600 dark:border-gray-700 dark:text-gray-100">
<a href={link.href} target="_blank" rel="noreferrer">
{link.label}
</a>
</CardContent>
</Card>
</motion.div>
<motion.div variants={revealUp}>
<Card className="border-gray-100 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<CardContent className="space-y-3 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t('sidebar_author_title')}
</p>
<p className="text-lg font-semibold text-gray-900 dark:text-gray-50">{post.author?.name || t('team')}</p>
<p className="text-sm text-gray-600 dark:text-gray-300">{t('sidebar_author_description')}</p>
</CardContent>
</Card>
</motion.div>
<motion.div variants={revealUp}>
<Card className="border-gray-100 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<CardContent className="space-y-4 p-6">
<div>
<p className="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t('share_title')}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{t('share_hint')}</p>
</div>
<div className="flex flex-wrap gap-3">
<Button variant="outline" size="sm" onClick={handleCopyLink} className="border-pink-200 text-pink-600 dark:border-pink-500/50 dark:text-pink-200">
{copied ? t('share_copied') : t('share_copy')}
</Button>
))}
</div>
</CardContent>
</Card>
</aside>
</div>
{canUseNativeShare && (
<Button
variant="ghost"
size="sm"
className="text-gray-700 hover:text-pink-600 dark:text-gray-200"
onClick={() => navigator.share?.({ title: post.title, url: shareUrl })}
>
{t('share_native')}
</Button>
)}
</div>
<Separator className="my-2" />
<div className="flex flex-col gap-2">
{shareLinks.map((link) => (
<Button key={link.key} variant="outline" size="sm" asChild className="justify-start border-gray-200 text-gray-700 hover:border-pink-200 hover:text-pink-600 dark:border-gray-700 dark:text-gray-100">
<a href={link.href} target="_blank" rel="noreferrer">
{link.label}
</a>
</Button>
))}
</div>
</CardContent>
</Card>
</motion.div>
</motion.aside>
</motion.div>
</section>
{(post.previous_post || post.next_post) && (
<section className="bg-gradient-to-br from-pink-50 via-white to-amber-50 px-4 py-12 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
<div className="container mx-auto max-w-6xl">
<section className="bg-gradient-to-br from-pink-50 via-white to-amber-50 px-4 py-12 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 [data-slot=card]:transition-transform [data-slot=card]:duration-200 [data-slot=card]:ease-out [data-slot=card]:hover:-translate-y-1">
<motion.div className="container mx-auto max-w-6xl" variants={stagger} initial="hidden" whileInView="visible" viewport={viewportOnce}>
<div className="grid gap-6 md:grid-cols-2">
{post.previous_post && (
<Card className="border-none bg-gradient-to-br from-pink-500 to-pink-400 text-white shadow-lg">
<CardContent className="space-y-4 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-white/80">
{t('previous_post')}
</p>
<h3 className="text-2xl font-semibold">{post.previous_post.title}</h3>
<MarkdownPreview
html={post.previous_post.excerpt_html}
fallback={post.previous_post.excerpt}
className="text-white/90"
/>
<Button asChild variant="secondary" className="bg-white/20 text-white hover:bg-white/30">
<Link href={buildArticleHref(post.previous_post.slug)}>
{t('read_story')}
</Link>
</Button>
</CardContent>
</Card>
<motion.div variants={revealUp}>
<Card className="border-none bg-gradient-to-br from-pink-500 to-pink-400 text-white shadow-lg">
<CardContent className="space-y-4 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-white/80">
{t('previous_post')}
</p>
<h3 className="text-2xl font-semibold">{post.previous_post.title}</h3>
<MarkdownPreview
html={post.previous_post.excerpt_html}
fallback={post.previous_post.excerpt}
className="text-white/90"
/>
<Button asChild variant="secondary" className="bg-white/20 text-white hover:bg-white/30">
<Link href={buildArticleHref(post.previous_post.slug)}>
{t('read_story')}
</Link>
</Button>
</CardContent>
</Card>
</motion.div>
)}
{post.next_post && (
<Card className="border-none bg-gradient-to-br from-amber-400 to-pink-400 text-gray-900 shadow-lg">
<CardContent className="space-y-4 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-gray-800/80">
{t('next_post')}
</p>
<h3 className="text-2xl font-semibold">{post.next_post.title}</h3>
<MarkdownPreview
html={post.next_post.excerpt_html}
fallback={post.next_post.excerpt}
className="text-gray-900/80"
/>
<Button asChild className="bg-gray-900 text-white hover:bg-gray-800">
<Link href={buildArticleHref(post.next_post.slug)}>
{t('read_story')}
</Link>
</Button>
</CardContent>
</Card>
<motion.div variants={revealUp}>
<Card className="border-none bg-gradient-to-br from-amber-400 to-pink-400 text-gray-900 shadow-lg">
<CardContent className="space-y-4 p-6">
<p className="text-sm font-semibold uppercase tracking-wide text-gray-800/80">
{t('next_post')}
</p>
<h3 className="text-2xl font-semibold">{post.next_post.title}</h3>
<MarkdownPreview
html={post.next_post.excerpt_html}
fallback={post.next_post.excerpt}
className="text-gray-900/80"
/>
<Button asChild className="bg-gray-900 text-white hover:bg-gray-800">
<Link href={buildArticleHref(post.next_post.slug)}>
{t('read_story')}
</Link>
</Button>
</CardContent>
</Card>
</motion.div>
)}
</div>
</div>
</motion.div>
</section>
)}
</MarketingLayout>