diff --git a/src/spa/pages/Home.tsx b/src/spa/pages/Home.tsx
index d2ce37a..e583cf7 100644
--- a/src/spa/pages/Home.tsx
+++ b/src/spa/pages/Home.tsx
@@ -15,6 +15,7 @@ import { useApplicationPhase, useCmsCollection, useCmsRoute } from '@/spa/cmsRou
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'
import { homeContent } from '@/spa/homeContent'
import { netzwerkContent } from '@/spa/netzwerkContent'
+import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder'
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo'
const FF = '"IBM Plex Sans", sans-serif';
@@ -220,7 +221,7 @@ function HomeWinnerCard({
item.name === winner?.name,
);
const title = fallbackText(winner?.title, fallbackText(winner?.name, staticWinner?.name || fallbackTitle));
- const imageSrc = mediaUrl(winner?.image, winner?.img || staticWinner?.img || fallbackImage);
+ const imageSrc = winner?.img || mediaUrl(winner?.image, fallbackImage);
const href = winner?.spaPath || (winner?.slug ? `/preistraeger/${winner.slug}` : staticWinner ? `/preistraeger/${staticWinner.slug}` : '/preistraeger');
return (
{
const [showVideo, setShowVideo] = useState(false);
const videoRef = useRef
(null);
const isMobile = useIsMobile();
+ const placeholderImage = usePreistraegerPlaceholderImage();
const home = (useCmsRoute()?.doc?.home || {}) as HomeCms;
const netzwerkPage = useCmsCollection('pages').find((page) => isNetzwerkPage(page as CmsRecord)) as
| ({ netzwerk?: { partners?: NetzwerkPartnersCms } } & CmsRecord)
@@ -302,7 +304,7 @@ const Home: React.FC = () => {
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
const applicationBenefits = fallbackArray(application.benefits, homeContent.application.benefits);
const finalSectionBenefits = showApplicationForm ? applicationBenefits : newsletterCopy.benefits;
- const winnerFallbackImage = mediaUrl(winnersSection.fallbackImage, `/images/${homeContent.winners.fallbackImageFilename}`);
+ const winnerFallbackImage = placeholderImage;
const videoTitle = fallbackText(videoModal.title, homeContent.videoModal.title);
const videoSrc = mediaUrl(videoModal.video, '');
diff --git a/src/spa/pages/Preistraeger.tsx b/src/spa/pages/Preistraeger.tsx
index f975f82..ff160db 100644
--- a/src/spa/pages/Preistraeger.tsx
+++ b/src/spa/pages/Preistraeger.tsx
@@ -7,6 +7,7 @@ import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
import { preistraegerIndexContent } from '@/spa/preistraegerIndexContent';
+import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder';
const FF = '"IBM Plex Sans", sans-serif';
const NAVY = '#111D55';
@@ -25,7 +26,7 @@ const fallbackText = (value: unknown, fallback: string) => typeof value === 'str
const normalizeWinner = (
doc: CmsRouteDoc,
- fallbackImage: string,
+ placeholderImage: string,
fallbackWinner: typeof preistraegerIndexContent.card.fallbackWinner,
): Winner => ({
id: String(doc.id),
@@ -34,7 +35,7 @@ const normalizeWinner = (
category: String(doc.category || fallbackWinner.category || preistraegerIndexContent.card.fallbackWinner.category),
year: Number(doc.year || new Date().getFullYear()),
type: String(doc.awardType || fallbackWinner.awardType || preistraegerIndexContent.card.fallbackWinner.awardType),
- img: docImageUrl(doc, fallbackImage),
+ img: docImageUrl(doc, placeholderImage),
shortDesc: String(doc.shortDesc || doc.description || doc.meta?.description || ''),
longDesc: String(doc.longDesc || doc.description || doc.meta?.description || ''),
quote: String(doc.quote || ''),
@@ -48,6 +49,7 @@ const normalizeWinner = (
export default function Preistraeger() {
const isMobile = useIsMobile();
+ const placeholderImage = usePreistraegerPlaceholderImage();
const cms = (useCmsRoute()?.doc?.preistraegerIndex || {}) as PreistraegerIndexCms;
const cmsPreistraeger = useCmsCollection('preistraeger');
const hero = { ...preistraegerIndexContent.hero, ...(cms.hero || {}) };
@@ -56,15 +58,11 @@ export default function Preistraeger() {
const empty = { ...preistraegerIndexContent.empty, ...(cms.empty || {}) };
const card = useMemo(() => ({ ...preistraegerIndexContent.card, ...(cms.card || {}) }), [cms.card]);
const cta = { ...preistraegerIndexContent.cta, ...(cms.cta || {}) };
- const cardFallbackImage = useMemo(
- () => mediaUrl(card.fallbackImage, `/images/${preistraegerIndexContent.card.fallbackImageFilename}`),
- [card],
- );
const winners = useMemo(() => {
const fallbackWinner = card.fallbackWinner || preistraegerIndexContent.card.fallbackWinner;
- if (cmsPreistraeger.length) return cmsPreistraeger.map((doc) => normalizeWinner(doc, cardFallbackImage, fallbackWinner)).filter((winner) => winner.slug);
+ if (cmsPreistraeger.length) return cmsPreistraeger.map((doc) => normalizeWinner(doc, placeholderImage, fallbackWinner)).filter((winner) => winner.slug);
return WINNERS;
- }, [card, cardFallbackImage, cmsPreistraeger]);
+ }, [card, cmsPreistraeger, placeholderImage]);
const years = useMemo(() => [...new Set(winners.map((winner) => winner.year))].sort((a, b) => b - a), [winners]);
const [activeYear, setActiveYear] = useState(undefined);
const selectedYear = activeYear ?? years[0] ?? new Date().getFullYear();
diff --git a/src/spa/pages/PreistraegerDetail.tsx b/src/spa/pages/PreistraegerDetail.tsx
index 26e94a2..5fd269d 100644
--- a/src/spa/pages/PreistraegerDetail.tsx
+++ b/src/spa/pages/PreistraegerDetail.tsx
@@ -1,11 +1,12 @@
import React from 'react';
import { Link, useParams, Navigate } from '@/spa/router';
-import { Trophy, ArrowLeft, MapPin, Building2, Globe, ChevronRight } from 'lucide-react';
+import { Trophy, ArrowLeft, MapPin, Building2, Globe, ChevronRight, Download, ChevronLeft, X } from 'lucide-react';
import { WINNERS } from '@/spa/data/winners';
import { useCmsRoute } from '@/spa/cmsRoute';
import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent';
+import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder';
import Image from '@/spa/components/ui/UnoptimizedImage'
const FF = '"IBM Plex Sans", sans-serif';
@@ -21,18 +22,38 @@ type PreistraegerDetailCms = Partial & {
sideImage?: Partial & { image?: unknown }
}
+type GalleryImageCms = {
+ id?: string | null
+ image?: unknown
+ imageAlt?: string | null
+ caption?: string | null
+}
+
+type PreistraegerGalleryCms = {
+ enabled?: boolean | null
+ heading?: string | null
+ description?: string | null
+ images?: GalleryImageCms[] | null
+}
+
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
+const mediaFilename = (media: unknown) => {
+ if (!media || typeof media !== 'object') return undefined;
+ const filename = (media as { filename?: unknown }).filename;
+ return typeof filename === 'string' && filename.length > 0 ? filename : undefined;
+};
+
export default function PreistraegerDetail() {
const { slug } = useParams<{ slug: string }>();
const route = useCmsRoute();
+ const placeholderImage = usePreistraegerPlaceholderImage();
const cmsWinner = route?.collection === 'preistraeger' ? route.doc : undefined;
const cmsDetail = {
...preistraegerDetailContent,
...((cmsWinner?.detailPage || {}) as PreistraegerDetailCms),
} as PreistraegerDetailCms;
const fallbackWinner = cmsDetail.fallbackWinner || preistraegerDetailContent.fallbackWinner;
- const winnerFallbackImage = mediaUrl(cmsDetail.fallbackImage, `/images/${preistraegerDetailContent.fallbackImageFilename}`);
const winner = cmsWinner ? {
id: String(cmsWinner.id),
slug: String(cmsWinner.slug || slug),
@@ -40,7 +61,7 @@ export default function PreistraegerDetail() {
category: String(cmsWinner.category || fallbackWinner.category || preistraegerDetailContent.fallbackWinner.category),
year: Number(cmsWinner.year || new Date().getFullYear()),
type: String(cmsWinner.awardType || fallbackWinner.awardType || preistraegerDetailContent.fallbackWinner.awardType),
- img: docImageUrl(cmsWinner, winnerFallbackImage),
+ img: docImageUrl(cmsWinner, placeholderImage),
shortDesc: String(cmsWinner.shortDesc || cmsWinner.description || cmsWinner.meta?.description || ''),
longDesc: String(cmsWinner.longDesc || cmsWinner.description || cmsWinner.meta?.description || fallbackWinner.longDesc || preistraegerDetailContent.fallbackWinner.longDesc),
quote: String(cmsWinner.quote || fallbackWinner.quote || preistraegerDetailContent.fallbackWinner.quote),
@@ -61,6 +82,23 @@ export default function PreistraegerDetail() {
const facts = { ...preistraegerDetailContent.facts, ...(detail.facts || {}) };
const websiteCta = { ...preistraegerDetailContent.websiteCta, ...(detail.websiteCta || {}) };
const backLink = { ...preistraegerDetailContent.backLink, ...(detail.backLink || {}) };
+ const downloadHref = mediaUrl(cmsWinner?.downloadZip, '');
+ const downloadFilename = mediaFilename(cmsWinner?.downloadZip);
+ const downloadLabel = fallbackText(cmsWinner?.downloadLabel, 'Pressepaket herunterladen');
+ const gallery = (cmsWinner?.gallery || {}) as PreistraegerGalleryCms;
+ const galleryImages = Array.isArray(gallery.images)
+ ? gallery.images
+ .map((item) => ({
+ id: item.id || mediaFilename(item.image) || String(mediaUrl(item.image, '')),
+ src: mediaUrl(item.image, ''),
+ alt: mediaAlt(item.image, fallbackText(item.imageAlt, fallbackText(item.caption, winner?.name || 'Galeriebild'))),
+ caption: fallbackText(item.caption, ''),
+ }))
+ .filter((item) => item.src)
+ : [];
+ const [activeGalleryIndex, setActiveGalleryIndex] = React.useState(undefined);
+ const activeGalleryImage = typeof activeGalleryIndex === 'number' ? galleryImages[activeGalleryIndex] : undefined;
+ const showGallery = Boolean(gallery.enabled) && galleryImages.length > 0;
if (!winner) return ;
@@ -187,9 +225,155 @@ export default function PreistraegerDetail() {
>
{fallbackText(websiteCta.label, preistraegerDetailContent.websiteCta.label)}
+
+ {downloadHref ? (
+ { const el = e.currentTarget as HTMLElement; el.style.background = GOLD; el.style.color = '#101828'; }}
+ onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.background = 'transparent'; el.style.color = NAVY; }}
+ >
+ {downloadLabel}
+
+ ) : null}
+ {showGallery ? (
+ setActiveGalleryIndex(undefined)}
+ style={{
+ position: 'fixed',
+ inset: 0,
+ zIndex: 1000,
+ background: 'rgba(3,9,58,0.92)',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: isMobile ? 18 : 40,
+ }}
+ >
+
+
+ {galleryImages.length > 1 ? (
+
+ ) : null}
+
+
event.stopPropagation()} style={{ maxWidth: 'min(1120px, 100%)', maxHeight: '86vh', display: 'flex', flexDirection: 'column', gap: 14 }}>
+
+ {activeGalleryImage.caption ? (
+
+ {activeGalleryImage.caption}
+
+ ) : null}
+
+
+ {galleryImages.length > 1 ? (
+
+ ) : null}
+
+ ) : null}
+
{/* Back link */}