feat: implement jury member selection and sorting functionality, update related database migrations

This commit is contained in:
syntaxbullet
2026-07-13 20:49:06 +02:00
parent 98af6423c5
commit 63387ce335
8 changed files with 273 additions and 55 deletions

View File

@@ -7,6 +7,7 @@ import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { homeContent } from '@/spa/homeContent';
import { selectJuryDocs, sortJuryMembersQuoteFirst } from '@/spa/juryMembers';
import { netzwerkContent } from '@/spa/netzwerkContent';
import Image from '@/spa/components/ui/UnoptimizedImage'
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo';
@@ -16,8 +17,9 @@ const FB = '"Inter", sans-serif';
const NAVY = '#111D55';
const GOLD = '#EFBF04';
type NetzwerkCms = Partial<typeof netzwerkContent> & {
type NetzwerkCms = Omit<Partial<typeof netzwerkContent>, 'hero' | 'jury' | 'sponsoring'> & {
hero?: Partial<typeof netzwerkContent.hero> & { image?: unknown }
jury?: Partial<Omit<typeof netzwerkContent.jury, 'members'>> & { members?: unknown[] | null }
sponsoring?: Partial<typeof netzwerkContent.sponsoring> & { image?: unknown }
}
@@ -76,14 +78,15 @@ const normalizeJuryMember = (member: CmsRouteDoc): JuryMember => {
const organization = fallbackText(member.organization, '')
const rawRole = fallbackText(member.role, '')
const role = rawRole && organization && rawRole !== organization ? `${rawRole} / ${organization}` : fallbackText(rawRole, organization)
const image = member.image ?? member.imageUpload
return {
name: fallbackText(member.name, ''),
role,
bio: fallbackText(member.bio, ''),
image: mediaUrl(member.image, juryImageFallback),
image: mediaUrl(image, juryImageFallback),
imageAlt: fallbackText(member.imageAlt, fallbackText(member.name, 'Jury-Mitglied')),
imageUpload: member.image,
imageUpload: image,
organization,
quote: fallbackText(member.quote, ''),
sortOrder: typeof member.sortOrder === 'number' ? member.sortOrder : undefined,
@@ -91,18 +94,6 @@ const normalizeJuryMember = (member: CmsRouteDoc): JuryMember => {
}
}
const hasJuryText = (value: unknown) => typeof value === 'string' && value.trim().length > 0
const hasCompleteJuryProfile = (member: JuryMember) => hasJuryText(member.bio) && hasJuryText(member.quote)
const sortedJuryMembers = (members: JuryMember[]) =>
members
.map((member, index) => ({ member, index }))
.sort((a, b) => {
const completenessOrder = Number(hasCompleteJuryProfile(b.member)) - Number(hasCompleteJuryProfile(a.member))
if (completenessOrder !== 0) return completenessOrder
const sortOrder = Number(a.member.sortOrder ?? a.index) - Number(b.member.sortOrder ?? b.index)
return sortOrder !== 0 ? sortOrder : a.index - b.index
})
.map(({ member }) => member)
function Lines({ text, highlight }: { text: string; highlight?: string }) {
return (
<>
@@ -494,10 +485,16 @@ const Netzwerk: React.FC = () => {
const partnerModal = { ...netzwerkContent.partners.modal, ...((cms.partners as typeof netzwerkContent.partners | undefined)?.modal || {}) };
const sponsoring = { ...netzwerkContent.sponsoring, ...(cms.sponsoring || {}) };
const patronPeople = fallbackArray<PatronPerson>(patronage.people, netzwerkContent.patronage.people);
const juryCollectionMembers = cmsJuryMembers.map(normalizeJuryMember).filter((member) => member.active !== false && member.name);
const juryMembers = sortedJuryMembers(juryCollectionMembers.length
? juryCollectionMembers
: fallbackArray<JuryMember>(jury.members, netzwerkContent.jury.members));
const cmsJurySelection = cms.jury?.members;
const hasCmsJurySelection = cmsJurySelection !== undefined;
const sourcedJuryDocs = selectJuryDocs(cmsJurySelection, cmsJuryMembers);
const normalizedJuryMembers = sourcedJuryDocs
.map(normalizeJuryMember)
.filter((member) => member.active !== false && member.name);
const jurySourceMembers: JuryMember[] = normalizedJuryMembers.length || hasCmsJurySelection
? normalizedJuryMembers
: netzwerkContent.jury.members;
const juryMembers = sortJuryMembersQuoteFirst(jurySourceMembers);
const expertiseRows = fallbackArray<ExpertiseRow>(expertise.rows, netzwerkContent.expertise.rows);
const partners = sortedPartners(cmsPartners)
.filter((partner) => partner.active !== false)
@@ -710,9 +707,11 @@ const Netzwerk: React.FC = () => {
</div>
{/* ── Prominenter Vorsitz-Bereich ─────────────────────────────────── */}
{/* TODO: echten Jury-Vorsitzenden bestätigen (Name & Foto sind Platzhalter) */}
<div style={{ padding: isMobile ? '28px 24px 8px' : '64px 80px 16px' }}>
<JuryChairCard member={juryMembers[0]} badge={fallbackText(jury.chairBadge, netzwerkContent.jury.chairBadge)} />
</div>
{juryMembers.length ? (
<div style={{ padding: isMobile ? '28px 24px 8px' : '64px 80px 16px' }}>
<JuryChairCard member={juryMembers[0]} badge={fallbackText(jury.chairBadge, netzwerkContent.jury.chairBadge)} />
</div>
) : null}
{/* ── Übrige Jury-Mitglieder ──────────────────────────────────────── */}
<div style={{ display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(auto-fill, minmax(260px, 1fr))', overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 12 : 0, padding: isMobile ? '24px 24px 36px' : '0 0 56px', WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', borderTop: isMobile ? 'none' : '1px solid rgba(255,255,255,0.07)' }}>