feat: add JuryMitglieder collection and migration for jury members
- Created a new collection `JuryMitglieder` with fields for name, image, quote, role, organization, bio, website, LinkedIn, sort order, active status, and published date. - Implemented access control for creating, reading, updating, and deleting jury members. - Added migration script to create necessary database tables and columns for jury members, including versioning support. - Updated existing `preistraeger` table to include new columns for award group, winner rank, sort order, and visibility settings. - Introduced a temporary global switch to control the visibility of testimonials in the SPA.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from '@/spa/router';
|
||||
import { Calendar, MapPin, Download, ArrowRight, FileText } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { Calendar, MapPin } from 'lucide-react';
|
||||
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||
@@ -75,20 +74,17 @@ function Lines({ text }: { text: string }) {
|
||||
|
||||
const fallbackCardImage = (item: PressEventItem | PressNewsItem) => mediaUrl(item.image, fallbackText(item.img, ''))
|
||||
|
||||
function NewsCard({ item, idx, isMobile, readMoreLabel, total }: { item: PressNewsItem; idx: number; isMobile: boolean; readMoreLabel: string; total: number }) {
|
||||
function NewsCard({ item, idx, isMobile, total }: { item: PressNewsItem; idx: number; isMobile: boolean; total: number }) {
|
||||
const [hovered, setHovered] = React.useState(false);
|
||||
return (
|
||||
<Link
|
||||
to={item.slug}
|
||||
<article
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
cursor: 'pointer',
|
||||
borderLeft: isMobile ? 'none' : (idx > 0 ? '1px solid rgba(255,255,255,0.07)' : 'none'),
|
||||
borderBottom: isMobile && idx < total - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
{/* image */}
|
||||
@@ -166,54 +162,8 @@ function NewsCard({ item, idx, isMobile, readMoreLabel, total }: { item: PressNe
|
||||
>
|
||||
{item.excerpt}
|
||||
</p>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: FF,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.12em',
|
||||
color: GOLD,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
{readMoreLabel} <ArrowRight size={12} />
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function DarkInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
|
||||
const [f, setF] = React.useState(false);
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
onFocus={(e) => {
|
||||
setF(true);
|
||||
props.onFocus?.(e);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
setF(false);
|
||||
props.onBlur?.(e);
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 44,
|
||||
padding: '0 14px',
|
||||
fontFamily: FF,
|
||||
fontSize: 18,
|
||||
color: '#fff',
|
||||
border: `1px solid ${f ? 'rgba(239,191,4,0.8)' : 'rgba(255,255,255,0.12)'}`,
|
||||
background: f ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.05)',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
transition: 'border-color 0.15s, background 0.15s',
|
||||
...props.style,
|
||||
}}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,7 +174,6 @@ const Press: React.FC = () => {
|
||||
const eventsSection = React.useMemo(() => ({ ...pressIndexContent.events, ...(cms.events || {}) }), [cms.events]);
|
||||
const newsSection = React.useMemo(() => ({ ...pressIndexContent.news, ...(cms.news || {}) }), [cms.news]);
|
||||
const downloads = React.useMemo(() => ({ ...pressIndexContent.downloads, ...(cms.downloads || {}) }), [cms.downloads]);
|
||||
const accreditation = React.useMemo(() => ({ ...pressIndexContent.accreditation, ...(cms.accreditation || {}) }), [cms.accreditation]);
|
||||
const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n');
|
||||
const cmsEvents = useCmsCollection('events');
|
||||
const cmsPosts = useCmsCollection('posts');
|
||||
@@ -248,22 +197,6 @@ const Press: React.FC = () => {
|
||||
if (cmsPosts.length) return cmsPosts.map((post) => postFromCms(post, newsCollectionFallbackImage, newsSection));
|
||||
return fallbackArray<PressNewsItem>(newsSection.fallbackItems, pressIndexContent.news.fallbackItems);
|
||||
}, [cmsPosts, newsCollectionFallbackImage, newsSection]);
|
||||
const [hoveredEvent, setHoveredEvent] = useState<number | null>(null);
|
||||
const [dlHovered, setDlHovered] = useState(false);
|
||||
const [submitHovered, setSubmitHovered] = useState(false);
|
||||
|
||||
// Form state
|
||||
const [medium, setMedium] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (medium.trim() && name.trim() && email.trim()) {
|
||||
setSubmitted(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in">
|
||||
|
||||
@@ -411,15 +344,11 @@ const Press: React.FC = () => {
|
||||
{combinedEvents.map((event, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
onMouseEnter={() => setHoveredEvent(idx)}
|
||||
onMouseLeave={() => setHoveredEvent(null)}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : '320px 1fr',
|
||||
borderBottom: '1px solid rgba(3,9,58,0.08)',
|
||||
background: hoveredEvent === idx ? '#EAE7E6' : 'transparent',
|
||||
transition: 'background 0.15s',
|
||||
cursor: 'pointer',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
{/* Left – image cell */}
|
||||
@@ -444,9 +373,6 @@ const Press: React.FC = () => {
|
||||
</div>
|
||||
<div style={{ width: 24, height: 1, background: 'rgba(239,191,4,0.4)', marginBottom: 14 }} />
|
||||
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.5)', lineHeight: 1.7, flex: 1 }}>{event.desc}</p>
|
||||
<Link to={event.slug} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginTop: 16, fontFamily: FF, fontSize: 16, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em', color: '#101828', textDecoration: 'none' }}>
|
||||
{fallbackText(eventsSection.detailLabel, pressIndexContent.events.detailLabel)} <ArrowRight size={12} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -511,7 +437,7 @@ const Press: React.FC = () => {
|
||||
{/* 3-column news grid */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)' }}>
|
||||
{combinedNews.map((item, idx) => (
|
||||
<NewsCard key={idx} item={item} idx={idx} isMobile={isMobile} readMoreLabel={fallbackText(newsSection.readMoreLabel, pressIndexContent.news.readMoreLabel)} total={combinedNews.length} />
|
||||
<NewsCard key={idx} item={item} idx={idx} isMobile={isMobile} total={combinedNews.length} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
@@ -519,7 +445,7 @@ const Press: React.FC = () => {
|
||||
{/* ── 4. PRESSE-MATERIAL & AKKREDITIERUNG ─────────────────────────── */}
|
||||
<section id="downloads" style={{ background: CREAM, overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||||
<MunichSkylineBg />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', minHeight: isMobile ? 'auto' : 600 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', minHeight: isMobile ? 'auto' : 600 }}>
|
||||
|
||||
{/* Left col */}
|
||||
<div
|
||||
@@ -529,8 +455,6 @@ const Press: React.FC = () => {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
borderRight: isMobile ? 'none' : '1px solid rgba(3,9,58,0.1)',
|
||||
borderBottom: isMobile ? '1px solid rgba(3,9,58,0.1)' : 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -571,62 +495,12 @@ const Press: React.FC = () => {
|
||||
color: 'rgba(16,24,40,0.5)',
|
||||
lineHeight: 1.8,
|
||||
maxWidth: 380,
|
||||
marginBottom: 48,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
{fallbackText(downloads.description, pressIndexContent.downloads.description)}
|
||||
</p>
|
||||
|
||||
{/* Download button */}
|
||||
<a
|
||||
href={mediaUrl(downloads.kitFile, fallbackText(downloads.kitUrl, pressIndexContent.downloads.kitUrl))}
|
||||
download={fallbackText(downloads.kitDownloadName, pressIndexContent.downloads.kitDownloadName)}
|
||||
onMouseEnter={() => setDlHovered(true)}
|
||||
onMouseLeave={() => setDlHovered(false)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 20,
|
||||
padding: '24px 28px',
|
||||
border: `1px solid ${dlHovered ? GOLD : 'rgba(3,9,58,0.12)'}`,
|
||||
background: '#fff',
|
||||
cursor: 'pointer',
|
||||
marginBottom: 20,
|
||||
transition: 'border-color 0.15s',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: NAVY,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Download size={20} color={GOLD} />
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FF,
|
||||
fontSize: 18,
|
||||
fontWeight: 700,
|
||||
color: '#101828',
|
||||
marginBottom: 3,
|
||||
}}
|
||||
>
|
||||
{fallbackText(downloads.kitLabel, pressIndexContent.downloads.kitLabel)}
|
||||
</div>
|
||||
<div style={{ fontFamily: FB, fontSize: 17, color: 'rgba(16,24,40,0.4)' }}>
|
||||
{fallbackText(downloads.kitMeta, pressIndexContent.downloads.kitMeta)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Press contact block */}
|
||||
<div style={{ borderLeft: `3px solid ${GOLD}`, paddingLeft: 24, marginTop: 32 }}>
|
||||
<div
|
||||
@@ -671,204 +545,6 @@ const Press: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right col */}
|
||||
<div
|
||||
style={{
|
||||
background: NAVY,
|
||||
padding: isMobile ? '48px 24px' : '72px 64px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{submitted ? (
|
||||
/* Success state */
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
background: GOLD,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 28,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<FileText size={24} color={NAVY} />
|
||||
</div>
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: FF,
|
||||
fontSize: 'clamp(1.4rem, 2.2vw, 1.8rem)',
|
||||
fontWeight: 900,
|
||||
color: '#fff',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '-0.02em',
|
||||
margin: '0 0 16px',
|
||||
}}
|
||||
>
|
||||
{fallbackText(accreditation.successHeading, pressIndexContent.accreditation.successHeading)}
|
||||
</h3>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: FB,
|
||||
fontSize: 18,
|
||||
color: 'rgba(255,255,255,0.45)',
|
||||
lineHeight: 1.8,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{fallbackText(accreditation.successMessage, pressIndexContent.accreditation.successMessage)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: FF,
|
||||
fontSize: 10,
|
||||
color: GOLD,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.32em',
|
||||
fontWeight: 700,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
{fallbackText(accreditation.eyebrow, pressIndexContent.accreditation.eyebrow)}
|
||||
</div>
|
||||
|
||||
<h3
|
||||
style={{
|
||||
fontFamily: FF,
|
||||
fontSize: 'clamp(1.4rem, 2.2vw, 1.8rem)',
|
||||
fontWeight: 900,
|
||||
color: '#fff',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '-0.02em',
|
||||
margin: '0 0 16px',
|
||||
}}
|
||||
>
|
||||
<Lines text={fallbackText(accreditation.heading, pressIndexContent.accreditation.heading)} />
|
||||
</h3>
|
||||
|
||||
{/* Gold divider */}
|
||||
<div style={{ width: 36, height: 2, background: GOLD, marginBottom: 28 }} />
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontFamily: FB,
|
||||
fontSize: 18,
|
||||
color: 'rgba(255,255,255,0.45)',
|
||||
lineHeight: 1.8,
|
||||
marginBottom: 36,
|
||||
}}
|
||||
>
|
||||
{fallbackText(accreditation.description, pressIndexContent.accreditation.description)}
|
||||
</p>
|
||||
|
||||
{/* Form fields */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label
|
||||
style={{
|
||||
fontFamily: FF,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.2em',
|
||||
color: 'rgba(255,255,255,0.35)',
|
||||
}}
|
||||
>
|
||||
{fallbackText(accreditation.mediumLabel, pressIndexContent.accreditation.mediumLabel)}
|
||||
</label>
|
||||
<DarkInput
|
||||
type="text"
|
||||
value={medium}
|
||||
onChange={(e) => setMedium(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label
|
||||
style={{
|
||||
fontFamily: FF,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.2em',
|
||||
color: 'rgba(255,255,255,0.35)',
|
||||
}}
|
||||
>
|
||||
{fallbackText(accreditation.nameLabel, pressIndexContent.accreditation.nameLabel)}
|
||||
</label>
|
||||
<DarkInput
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label
|
||||
style={{
|
||||
fontFamily: FF,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.2em',
|
||||
color: 'rgba(255,255,255,0.35)',
|
||||
}}
|
||||
>
|
||||
{fallbackText(accreditation.emailLabel, pressIndexContent.accreditation.emailLabel)}
|
||||
</label>
|
||||
<DarkInput
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit button */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
onMouseEnter={() => setSubmitHovered(true)}
|
||||
onMouseLeave={() => setSubmitHovered(false)}
|
||||
style={{
|
||||
background: submitHovered ? '#FFD130' : GOLD,
|
||||
boxShadow: submitHovered ? '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)' : 'none',
|
||||
color: '#101828',
|
||||
border: 'none',
|
||||
fontFamily: FF,
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.12em',
|
||||
cursor: 'pointer',
|
||||
padding: '14px 28px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
width: '100%',
|
||||
transition: 'background 0.15s, box-shadow 0.2s',
|
||||
}}
|
||||
>
|
||||
{fallbackText(accreditation.submitLabel, pressIndexContent.accreditation.submitLabel)} <ArrowRight size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user