diff --git a/src/collections/Pages/netzwerkFields.ts b/src/collections/Pages/netzwerkFields.ts index d372a10..609562b 100644 --- a/src/collections/Pages/netzwerkFields.ts +++ b/src/collections/Pages/netzwerkFields.ts @@ -39,6 +39,7 @@ const sectionAdmin = (description: string) => ({ const statFields: Field[] = [text('value', 'Value'), text('label', 'Label')] const personFields: Field[] = [ + uploadField('imageUpload', 'Image'), text('image', 'Image URL'), text('imageAlt', 'Image alt text'), text('name', 'Name'), @@ -51,6 +52,7 @@ const juryMemberFields: Field[] = [ text('name', 'Name'), text('role', 'Role'), textarea('bio', 'Bio'), + uploadField('imageUpload', 'Image'), text('image', 'Image URL'), ] diff --git a/src/payload-types.ts b/src/payload-types.ts index 105a734..a2f5760 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -1716,6 +1716,7 @@ export interface Page { description?: string | null; people?: | { + imageUpload?: (number | null) | Media; image?: string | null; imageAlt?: string | null; name?: string | null; @@ -1769,6 +1770,7 @@ export interface Page { name?: string | null; role?: string | null; bio?: string | null; + imageUpload?: (number | null) | Media; image?: string | null; id?: string | null; }[] @@ -4596,6 +4598,7 @@ export interface PagesSelect { people?: | T | { + imageUpload?: T; image?: T; imageAlt?: T; name?: T; @@ -4647,6 +4650,7 @@ export interface PagesSelect { name?: T; role?: T; bio?: T; + imageUpload?: T; image?: T; id?: T; }; diff --git a/src/scripts/preload-netzwerk-page-cms.ts b/src/scripts/preload-netzwerk-page-cms.ts index 25d1e0d..c0ac4a8 100644 --- a/src/scripts/preload-netzwerk-page-cms.ts +++ b/src/scripts/preload-netzwerk-page-cms.ts @@ -1,6 +1,7 @@ import 'dotenv/config' import config from '@payload-config' +import crypto from 'crypto' import { getPayload, type Payload } from 'payload' import { netzwerkContent } from '@/spa/netzwerkContent' @@ -15,6 +16,29 @@ const mediaByFilename = async (payload: Payload, filename: string) => { return result.docs[0]?.id } +const safeRemoteName = (url: string) => { + const parsed = new URL(url) + const ext = parsed.pathname.split('/').pop()?.includes('.') ? `.${parsed.pathname.split('.').pop()}` : '.jpg' + const hash = crypto.createHash('sha1').update(url).digest('hex').slice(0, 10) + return `${parsed.hostname.replace(/[^a-z0-9]+/gi, '-')}-${hash}${ext}`.toLowerCase() +} + +const filenameForSource = (source: string) => { + if (source.startsWith('http')) return safeRemoteName(source) + return source.split('?')[0]?.split('/').filter(Boolean).pop() +} + +const itemsWithImageUploads = async (payload: Payload, items: T[]) => + Promise.all( + items.map(async (item) => { + const filename = item.image ? filenameForSource(item.image) : undefined + return { + ...item, + imageUpload: filename ? await mediaByFilename(payload, filename) : undefined, + } + }), + ) + async function main() { const payload = await getPayload({ config }) @@ -36,6 +60,10 @@ async function main() { const sponsoringImage = await mediaByFilename(payload, netzwerkContent.sponsoring.imageFilename) const { imageFilename: _heroFilename, ...heroContent } = netzwerkContent.hero const { imageFilename: _sponsoringFilename, ...sponsoringContent } = netzwerkContent.sponsoring + const [patronPeople, juryMembers] = await Promise.all([ + itemsWithImageUploads(payload, netzwerkContent.patronage.people), + itemsWithImageUploads(payload, netzwerkContent.jury.members), + ]) await payload.update({ collection: 'pages', @@ -49,6 +77,14 @@ async function main() { ...heroContent, image: heroImage, }, + patronage: { + ...netzwerkContent.patronage, + people: patronPeople, + }, + jury: { + ...netzwerkContent.jury, + members: juryMembers, + }, sponsoring: { ...sponsoringContent, image: sponsoringImage, diff --git a/src/spa/pages/Netzwerk.tsx b/src/spa/pages/Netzwerk.tsx index 2aead64..8ee511c 100644 --- a/src/spa/pages/Netzwerk.tsx +++ b/src/spa/pages/Netzwerk.tsx @@ -28,8 +28,8 @@ type NetzwerkCms = Partial & { sponsoring?: Partial & { image?: unknown } } -type PatronPerson = (typeof netzwerkContent.patronage.people)[number] -type JuryMember = (typeof netzwerkContent.jury.members)[number] +type PatronPerson = (typeof netzwerkContent.patronage.people)[number] & { imageUpload?: unknown } +type JuryMember = (typeof netzwerkContent.jury.members)[number] & { imageUpload?: unknown } type Partner = (typeof netzwerkContent.partners.items)[number] & { tier: 1 | 2 | 3; heroImg?: string; linkedin?: string; logoSubtext?: string } type PartnerTier = (typeof netzwerkContent.partners.tiers)[number] type ExpertiseRow = (typeof netzwerkContent.expertise.rows)[number] @@ -39,6 +39,7 @@ type PartnerModalCopy = typeof netzwerkContent.partners.modal const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; const fallbackArray = (value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback; +const managedImage = (upload: unknown, fallback: unknown) => mediaUrl(upload, fallbackText(fallback, '')) function Lines({ text, highlight }: { text: string; highlight?: string }) { return ( @@ -112,7 +113,7 @@ const JuryCard: React.FC<{ member: JuryMember; idx: number }> = ({ member, idx } style={{ borderLeft: !isMobileCard && idx > 0 ? '1px solid rgba(255,255,255,0.07)' : 'none', display: 'flex', flexDirection: 'column', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.03)' : 'transparent', transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '74vw', maxWidth: '74vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : {}) }} >
- {member.name} + {member.name}
@@ -145,7 +146,7 @@ const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member }} >
- {member.name} + {member.name}
@@ -502,7 +503,7 @@ const Netzwerk: React.FC = () => { {patronPeople.map((person, index) => (