admin widget zu dokploy geswitched, viele übersetzungen im Frontend vervollständigt und Anlässe-Seiten mit ChatGPT ausgebaut
This commit is contained in:
@@ -14,6 +14,7 @@ interface PostSummary {
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt?: string;
|
||||
excerpt_html?: string;
|
||||
featured_image?: string;
|
||||
published_at?: string;
|
||||
author?: { name?: string } | string;
|
||||
@@ -34,6 +35,18 @@ interface Props {
|
||||
};
|
||||
}
|
||||
|
||||
const MarkdownPreview: React.FC<{ html?: string; fallback?: string; className?: string }> = ({ html, fallback, className }) => {
|
||||
if (html && html.trim().length > 0) {
|
||||
return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
|
||||
if (!fallback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <p className={className}>{fallback}</p>;
|
||||
};
|
||||
|
||||
const Blog: React.FC<Props> = ({ posts }) => {
|
||||
const { localizedPath } = useLocalizedRoutes();
|
||||
const { props } = usePage<{ supportedLocales?: string[] }>();
|
||||
@@ -118,6 +131,7 @@ const Blog: React.FC<Props> = ({ posts }) => {
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{posts.links.map((link, index) => {
|
||||
const href = resolvePaginationHref(link.url);
|
||||
const labelText = link.label?.trim() || '…';
|
||||
|
||||
if (!href) {
|
||||
return (
|
||||
@@ -126,8 +140,9 @@ const Blog: React.FC<Props> = ({ posts }) => {
|
||||
variant={link.active ? 'default' : 'outline'}
|
||||
disabled
|
||||
className={link.active ? 'bg-[#FFB6C1] hover:bg-[#FF69B4]' : ''}
|
||||
dangerouslySetInnerHTML={{ __html: link.label }}
|
||||
/>
|
||||
>
|
||||
{labelText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,7 +153,9 @@ const Blog: React.FC<Props> = ({ posts }) => {
|
||||
variant={link.active ? 'default' : 'outline'}
|
||||
className={link.active ? 'bg-[#FFB6C1] hover:bg-[#FF69B4]' : ''}
|
||||
>
|
||||
<Link href={href} dangerouslySetInnerHTML={{ __html: link.label }} />
|
||||
<Link href={href} aria-label={labelText}>
|
||||
{labelText}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
@@ -182,9 +199,11 @@ const Blog: React.FC<Props> = ({ posts }) => {
|
||||
<h2 className="text-3xl font-bold text-gray-900 dark:text-gray-50">
|
||||
{featuredPost.title || 'Untitled'}
|
||||
</h2>
|
||||
<p className="text-lg leading-relaxed text-gray-600 dark:text-gray-300">
|
||||
{featuredPost.excerpt || ''}
|
||||
</p>
|
||||
<MarkdownPreview
|
||||
html={featuredPost.excerpt_html}
|
||||
fallback={featuredPost.excerpt}
|
||||
className="text-lg leading-relaxed text-gray-600 dark:text-gray-300"
|
||||
/>
|
||||
{renderPostMeta(featuredPost)}
|
||||
<Button asChild size="lg" className="bg-[#FFB6C1] hover:bg-[#FF69B4] text-white">
|
||||
<Link href={buildArticleHref(featuredPost.slug)}>
|
||||
@@ -224,9 +243,11 @@ const Blog: React.FC<Props> = ({ posts }) => {
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-gray-50">
|
||||
{post.title || 'Untitled'}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-600 dark:text-gray-300">
|
||||
{post.excerpt || ''}
|
||||
</p>
|
||||
<MarkdownPreview
|
||||
html={post.excerpt_html}
|
||||
fallback={post.excerpt}
|
||||
className="text-sm leading-relaxed text-gray-600 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
{renderPostMeta(post)}
|
||||
@@ -261,7 +282,11 @@ const Blog: React.FC<Props> = ({ posts }) => {
|
||||
<h3 className="text-2xl font-semibold text-gray-900 dark:text-gray-50">
|
||||
{post.title || 'Untitled'}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">{post.excerpt || ''}</p>
|
||||
<MarkdownPreview
|
||||
html={post.excerpt_html}
|
||||
fallback={post.excerpt}
|
||||
className="text-gray-600 dark:text-gray-300"
|
||||
/>
|
||||
{renderPostMeta(post)}
|
||||
<Button asChild variant="outline" className="w-fit border-pink-200 text-pink-600 hover:bg-pink-50 dark:border-pink-500/40 dark:text-pink-200">
|
||||
<Link href={buildArticleHref(post.slug)}>
|
||||
@@ -283,18 +308,18 @@ const Blog: React.FC<Props> = ({ posts }) => {
|
||||
<MarketingLayout title={t('blog.title')}>
|
||||
<Head title={t('blog.title')} />
|
||||
|
||||
<section className="bg-gradient-to-br from-pink-50 via-white to-amber-50 px-4 py-14 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950">
|
||||
<div className="container mx-auto max-w-4xl space-y-6 text-center">
|
||||
<section className="bg-gradient-to-br from-pink-50 via-white to-amber-50 px-4 py-10 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 md:py-12">
|
||||
<div className="container mx-auto max-w-3xl space-y-5 text-center">
|
||||
<Badge variant="outline" className="border-pink-200 text-pink-600 dark:border-pink-500/50 dark:text-pink-200">
|
||||
Fotospiel Blog
|
||||
</Badge>
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-gray-50 md:text-5xl">{t('blog.hero_title')}</h1>
|
||||
<p className="text-lg leading-relaxed text-gray-600 dark:text-gray-300">{t('blog.hero_description')}</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<Button asChild size="lg" className="bg-[#FFB6C1] hover:bg-[#FF69B4] text-white">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-50 md:text-4xl">{t('blog.hero_title')}</h1>
|
||||
<p className="text-base leading-relaxed text-gray-600 dark:text-gray-300 md:text-lg">{t('blog.hero_description')}</p>
|
||||
<div className="flex flex-wrap justify-center gap-2.5">
|
||||
<Button asChild className="bg-[#FFB6C1] hover:bg-[#FF69B4] text-white px-6 py-2.5 text-base">
|
||||
<Link href={localizedPath('/packages')}>{t('home.cta_explore')}</Link>
|
||||
</Button>
|
||||
<Button asChild size="lg" variant="outline" className="border-gray-300 text-gray-800 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-50 dark:hover:bg-gray-800">
|
||||
<Button asChild variant="outline" className="border-gray-300 text-gray-800 hover:bg-gray-100 px-6 py-2.5 text-base dark:border-gray-700 dark:text-gray-50 dark:hover:bg-gray-800">
|
||||
<Link href="#articles">{t('blog.hero_cta')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,112 +1,336 @@
|
||||
import React from 'react';
|
||||
import React from 'react';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MarketingLayout from '@/layouts/mainWebsite';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
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';
|
||||
|
||||
interface AdjacentPost {
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt?: string;
|
||||
excerpt_html?: string;
|
||||
}
|
||||
|
||||
interface HeadingItem {
|
||||
text: string;
|
||||
slug: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
post: {
|
||||
id: number;
|
||||
title: string;
|
||||
excerpt?: string;
|
||||
excerpt_html?: string;
|
||||
content: string;
|
||||
content_html: string;
|
||||
headings?: HeadingItem[];
|
||||
featured_image?: string;
|
||||
published_at: string;
|
||||
author?: { name: string };
|
||||
slug: string;
|
||||
url?: string;
|
||||
previous_post?: AdjacentPost | null;
|
||||
next_post?: AdjacentPost | null;
|
||||
};
|
||||
}
|
||||
|
||||
const MarkdownPreview: React.FC<{ html?: string; fallback?: string; className?: string }> = ({ html, fallback, className }) => {
|
||||
if (html && html.trim().length > 0) {
|
||||
return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
|
||||
if (!fallback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <p className={className}>{fallback}</p>;
|
||||
};
|
||||
|
||||
const BlogShow: React.FC<Props> = ({ post }) => {
|
||||
const { localizedPath } = useLocalizedRoutes();
|
||||
const { t } = useTranslation('blog_show');
|
||||
const { t, i18n } = useTranslation('blog_show');
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const locale = i18n.language || 'de';
|
||||
const dateLocale = locale === 'en' ? 'en-US' : 'de-DE';
|
||||
|
||||
const formattedDate = React.useMemo(() => {
|
||||
try {
|
||||
return new Date(post.published_at).toLocaleDateString(dateLocale, {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[Marketing BlogShow] Failed to format date', error);
|
||||
return post.published_at;
|
||||
}
|
||||
}, [post.published_at, dateLocale]);
|
||||
|
||||
const handleCopyLink = React.useCallback(() => {
|
||||
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(post.url || window.location.href).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}).catch((error) => {
|
||||
console.warn('[Marketing BlogShow] Failed to copy link', error);
|
||||
});
|
||||
}, [post.url]);
|
||||
|
||||
const shareUrl = post.url || (typeof window !== 'undefined' ? window.location.href : '');
|
||||
const encodedShareUrl = encodeURIComponent(shareUrl);
|
||||
const encodedTitle = encodeURIComponent(post.title);
|
||||
|
||||
const shareLinks = [
|
||||
{
|
||||
key: 'whatsapp',
|
||||
label: t('share_whatsapp'),
|
||||
href: `https://wa.me/?text=${encodedTitle}%20${encodedShareUrl}`
|
||||
},
|
||||
{
|
||||
key: 'linkedin',
|
||||
label: t('share_linkedin'),
|
||||
href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedShareUrl}`
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: t('share_email'),
|
||||
href: `mailto:?subject=${encodedTitle}&body=${encodedShareUrl}`
|
||||
}
|
||||
];
|
||||
|
||||
const canUseNativeShare = typeof navigator !== 'undefined' && typeof navigator.share === 'function';
|
||||
|
||||
const buildArticleHref = React.useCallback((slug?: string | null) => {
|
||||
if (!slug) {
|
||||
return '#';
|
||||
}
|
||||
|
||||
return localizedPath(`/blog/${encodeURIComponent(slug)}`);
|
||||
}, [localizedPath]);
|
||||
|
||||
return (
|
||||
<MarketingLayout title={`${post.title} ${t('title_suffix')}`}>
|
||||
<Head title={`${post.title} ${t('title_suffix')}`} />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="bg-gradient-to-r from-[#FFB6C1] via-[#FFD700] to-[#87CEEB] py-20 px-4">
|
||||
<div className="container mx-auto max-w-4xl">
|
||||
<Card className="bg-white/10 backdrop-blur-sm border-white/20 text-white shadow-xl">
|
||||
<CardContent className="p-8 text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-6 leading-tight">{post.title}</h1>
|
||||
<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">
|
||||
<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')}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<Link href={localizedPath('/blog')} className="hover:text-gray-900 dark:hover:text-gray-100">
|
||||
{t('breadcrumb_blog')}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-gray-700 dark:text-gray-200">{post.title}</span>
|
||||
</nav>
|
||||
<Link href={localizedPath('/blog')} className="text-pink-600 hover:text-pink-700">
|
||||
{t('back_to_blog')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8 text-lg">
|
||||
<Badge variant="secondary" className="bg-white/20 text-white border-white/30">
|
||||
{t('by_author')} {post.author?.name || t('team')}
|
||||
</Badge>
|
||||
<Separator orientation="vertical" className="hidden sm:block h-6 bg-white/30" />
|
||||
<Badge variant="secondary" className="bg-white/20 text-white border-white/30">
|
||||
{t('published_on')} {new Date(post.published_at).toLocaleDateString('de-DE', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{post.featured_image && (
|
||||
<div className="mt-8">
|
||||
<img
|
||||
src={post.featured_image}
|
||||
alt={post.title}
|
||||
className="mx-auto rounded-lg shadow-lg max-w-2xl w-full object-cover"
|
||||
<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-200"
|
||||
/>
|
||||
</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">
|
||||
Fotospiel Stories
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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">
|
||||
<div
|
||||
className="prose prose-lg prose-slate max-w-none
|
||||
prose-headings:text-slate-900 prose-headings:font-semibold
|
||||
prose-p:text-slate-700 prose-p:leading-relaxed
|
||||
prose-a:text-pink-600 prose-a:no-underline hover:prose-a:underline
|
||||
prose-strong:text-slate-900 prose-strong:font-semibold
|
||||
prose-code:text-slate-900 prose-code:bg-slate-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded
|
||||
prose-pre:bg-slate-900 prose-pre:text-slate-100
|
||||
prose-blockquote:border-l-4 prose-blockquote:border-pink-500 prose-blockquote:pl-6 prose-blockquote:italic
|
||||
prose-ul:text-slate-700 prose-ol:text-slate-700
|
||||
prose-li:text-slate-700 dark:prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: post.content_html }}
|
||||
/>
|
||||
</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-200"
|
||||
/>
|
||||
</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-200">
|
||||
{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>
|
||||
<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>
|
||||
{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-50">
|
||||
<a href={link.href} target="_blank" rel="noreferrer">
|
||||
{link.label}
|
||||
</a>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
</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">
|
||||
<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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Post Content */}
|
||||
<section className="py-20 px-4 bg-white">
|
||||
<div className="container mx-auto max-w-4xl">
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="p-8 md:p-12">
|
||||
<div
|
||||
className="prose prose-lg prose-slate max-w-none
|
||||
prose-headings:text-slate-900 prose-headings:font-semibold
|
||||
prose-p:text-slate-700 prose-p:leading-relaxed
|
||||
prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline
|
||||
prose-strong:text-slate-900 prose-strong:font-semibold
|
||||
prose-code:text-slate-900 prose-code:bg-slate-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded
|
||||
prose-pre:bg-slate-900 prose-pre:text-slate-100
|
||||
prose-blockquote:border-l-4 prose-blockquote:border-blue-500 prose-blockquote:pl-6 prose-blockquote:italic
|
||||
prose-ul:text-slate-700 prose-ol:text-slate-700
|
||||
prose-li:text-slate-700"
|
||||
dangerouslySetInnerHTML={{ __html: post.content_html }}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Back to Blog */}
|
||||
<section className="py-10 px-4 bg-gray-50">
|
||||
<div className="container mx-auto max-w-4xl">
|
||||
<Card className="shadow-sm">
|
||||
<CardContent className="p-8 text-center">
|
||||
<Separator className="mb-6" />
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="bg-[#FFB6C1] hover:bg-[#FF69B4] text-white px-8 py-3 rounded-full font-semibold transition-colors"
|
||||
>
|
||||
<Link href={localizedPath('/blog')}>
|
||||
{t('back_to_blog')}
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</MarketingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -103,7 +103,7 @@ const HowItWorks: React.FC = () => {
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button asChild size="lg" className="bg-pink-500 hover:bg-pink-600">
|
||||
<Link href={localizedPath('/register')}>
|
||||
<Link href={localizedPath('/packages')}>
|
||||
{hero.primaryCta}
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -192,7 +192,7 @@ const HowItWorks: React.FC = () => {
|
||||
{t('how_it_works_page.timeline_title', 'Der Ablauf im Detail')}
|
||||
</Badge>
|
||||
<h2 className="mt-3 text-3xl font-bold text-gray-900 dark:text-gray-50">
|
||||
Ein klarer Fahrplan für dein Event
|
||||
{t('how_it_works_page.labels.timeline_heading', 'Ein klarer Fahrplan für dein Event')}
|
||||
</h2>
|
||||
</div>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
@@ -211,7 +211,7 @@ const HowItWorks: React.FC = () => {
|
||||
{item.tips?.length ? (
|
||||
<div className="mt-4 rounded-lg border border-pink-100 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-pink-900/40 dark:bg-gray-900 dark:text-gray-300">
|
||||
<p className="mb-2 font-semibold text-pink-600 dark:text-pink-300">
|
||||
{t('marketing.actions.tips', 'Tipps')}
|
||||
{t('how_it_works_page.labels.tips', 'Tipps')}
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{item.tips.map((tip) => (
|
||||
@@ -271,7 +271,7 @@ const HowItWorks: React.FC = () => {
|
||||
<CardContent className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="mb-3 text-sm font-semibold uppercase tracking-wide text-pink-600 dark:text-pink-300">
|
||||
{t('marketing.labels.recommendations', 'Empfehlungen')}
|
||||
{t('how_it_works_page.labels.recommendations', 'Empfehlungen')}
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
{tab.recommendations.map((item) => (
|
||||
@@ -284,7 +284,7 @@ const HowItWorks: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-3 text-sm font-semibold uppercase tracking-wide text-pink-600 dark:text-pink-300">
|
||||
{t('marketing.labels.challengeIdeas', 'Ideen für Challenges')}
|
||||
{t('how_it_works_page.labels.challenge_ideas', 'Ideen für Challenges')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tab.ideas.map((idea) => (
|
||||
@@ -308,7 +308,7 @@ const HowItWorks: React.FC = () => {
|
||||
<CardHeader>
|
||||
<CardTitle>{checklist.title}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('marketing.labels.prepHint', 'Alles, was du vor dem Event abhaken solltest.')}
|
||||
{t('how_it_works_page.labels.prep_hint', 'Alles, was du vor dem Event abhaken solltest.')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user