feat: manage network people media
This commit is contained in:
@@ -39,6 +39,7 @@ const sectionAdmin = (description: string) => ({
|
|||||||
const statFields: Field[] = [text('value', 'Value'), text('label', 'Label')]
|
const statFields: Field[] = [text('value', 'Value'), text('label', 'Label')]
|
||||||
|
|
||||||
const personFields: Field[] = [
|
const personFields: Field[] = [
|
||||||
|
uploadField('imageUpload', 'Image'),
|
||||||
text('image', 'Image URL'),
|
text('image', 'Image URL'),
|
||||||
text('imageAlt', 'Image alt text'),
|
text('imageAlt', 'Image alt text'),
|
||||||
text('name', 'Name'),
|
text('name', 'Name'),
|
||||||
@@ -51,6 +52,7 @@ const juryMemberFields: Field[] = [
|
|||||||
text('name', 'Name'),
|
text('name', 'Name'),
|
||||||
text('role', 'Role'),
|
text('role', 'Role'),
|
||||||
textarea('bio', 'Bio'),
|
textarea('bio', 'Bio'),
|
||||||
|
uploadField('imageUpload', 'Image'),
|
||||||
text('image', 'Image URL'),
|
text('image', 'Image URL'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1716,6 +1716,7 @@ export interface Page {
|
|||||||
description?: string | null;
|
description?: string | null;
|
||||||
people?:
|
people?:
|
||||||
| {
|
| {
|
||||||
|
imageUpload?: (number | null) | Media;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
imageAlt?: string | null;
|
imageAlt?: string | null;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
@@ -1769,6 +1770,7 @@ export interface Page {
|
|||||||
name?: string | null;
|
name?: string | null;
|
||||||
role?: string | null;
|
role?: string | null;
|
||||||
bio?: string | null;
|
bio?: string | null;
|
||||||
|
imageUpload?: (number | null) | Media;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[]
|
}[]
|
||||||
@@ -4596,6 +4598,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
people?:
|
people?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
|
imageUpload?: T;
|
||||||
image?: T;
|
image?: T;
|
||||||
imageAlt?: T;
|
imageAlt?: T;
|
||||||
name?: T;
|
name?: T;
|
||||||
@@ -4647,6 +4650,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
name?: T;
|
name?: T;
|
||||||
role?: T;
|
role?: T;
|
||||||
bio?: T;
|
bio?: T;
|
||||||
|
imageUpload?: T;
|
||||||
image?: T;
|
image?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dotenv/config'
|
import 'dotenv/config'
|
||||||
|
|
||||||
import config from '@payload-config'
|
import config from '@payload-config'
|
||||||
|
import crypto from 'crypto'
|
||||||
import { getPayload, type Payload } from 'payload'
|
import { getPayload, type Payload } from 'payload'
|
||||||
|
|
||||||
import { netzwerkContent } from '@/spa/netzwerkContent'
|
import { netzwerkContent } from '@/spa/netzwerkContent'
|
||||||
@@ -15,6 +16,29 @@ const mediaByFilename = async (payload: Payload, filename: string) => {
|
|||||||
return result.docs[0]?.id
|
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 <T extends { image?: string }>(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() {
|
async function main() {
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
@@ -36,6 +60,10 @@ async function main() {
|
|||||||
const sponsoringImage = await mediaByFilename(payload, netzwerkContent.sponsoring.imageFilename)
|
const sponsoringImage = await mediaByFilename(payload, netzwerkContent.sponsoring.imageFilename)
|
||||||
const { imageFilename: _heroFilename, ...heroContent } = netzwerkContent.hero
|
const { imageFilename: _heroFilename, ...heroContent } = netzwerkContent.hero
|
||||||
const { imageFilename: _sponsoringFilename, ...sponsoringContent } = netzwerkContent.sponsoring
|
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({
|
await payload.update({
|
||||||
collection: 'pages',
|
collection: 'pages',
|
||||||
@@ -49,6 +77,14 @@ async function main() {
|
|||||||
...heroContent,
|
...heroContent,
|
||||||
image: heroImage,
|
image: heroImage,
|
||||||
},
|
},
|
||||||
|
patronage: {
|
||||||
|
...netzwerkContent.patronage,
|
||||||
|
people: patronPeople,
|
||||||
|
},
|
||||||
|
jury: {
|
||||||
|
...netzwerkContent.jury,
|
||||||
|
members: juryMembers,
|
||||||
|
},
|
||||||
sponsoring: {
|
sponsoring: {
|
||||||
...sponsoringContent,
|
...sponsoringContent,
|
||||||
image: sponsoringImage,
|
image: sponsoringImage,
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ type NetzwerkCms = Partial<typeof netzwerkContent> & {
|
|||||||
sponsoring?: Partial<typeof netzwerkContent.sponsoring> & { image?: unknown }
|
sponsoring?: Partial<typeof netzwerkContent.sponsoring> & { image?: unknown }
|
||||||
}
|
}
|
||||||
|
|
||||||
type PatronPerson = (typeof netzwerkContent.patronage.people)[number]
|
type PatronPerson = (typeof netzwerkContent.patronage.people)[number] & { imageUpload?: unknown }
|
||||||
type JuryMember = (typeof netzwerkContent.jury.members)[number]
|
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 Partner = (typeof netzwerkContent.partners.items)[number] & { tier: 1 | 2 | 3; heroImg?: string; linkedin?: string; logoSubtext?: string }
|
||||||
type PartnerTier = (typeof netzwerkContent.partners.tiers)[number]
|
type PartnerTier = (typeof netzwerkContent.partners.tiers)[number]
|
||||||
type ExpertiseRow = (typeof netzwerkContent.expertise.rows)[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 fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||||
const fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
|
const fallbackArray = <T,>(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 }) {
|
function Lines({ text, highlight }: { text: string; highlight?: string }) {
|
||||||
return (
|
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' } : {}) }}
|
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' } : {}) }}
|
||||||
>
|
>
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: '3/4' }}>
|
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: '3/4' }}>
|
||||||
<Image unoptimized src={member.image} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
|
<Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '40%', background: 'linear-gradient(to top, rgba(3,9,58,0.65), transparent)' }} />
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '40%', background: 'linear-gradient(to top, rgba(3,9,58,0.65), transparent)' }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: isMobileCard ? '28px 28px 32px' : '32px 32px 36px' }}>
|
<div style={{ padding: isMobileCard ? '28px 28px 32px' : '32px 32px 36px' }}>
|
||||||
@@ -145,7 +146,7 @@ const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: isMobileCard ? '16/10' : '3/4', minHeight: isMobileCard ? 240 : 'auto' }}>
|
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: isMobileCard ? '16/10' : '3/4', minHeight: isMobileCard ? 240 : 'auto' }}>
|
||||||
<Image unoptimized src={member.image} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
|
<Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={member.name} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
|
||||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '45%', background: 'linear-gradient(to top, rgba(3,9,58,0.7), transparent)' }} />
|
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '45%', background: 'linear-gradient(to top, rgba(3,9,58,0.7), transparent)' }} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
<div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||||
@@ -502,7 +503,7 @@ const Netzwerk: React.FC = () => {
|
|||||||
{patronPeople.map((person, index) => (
|
{patronPeople.map((person, index) => (
|
||||||
<PersonCard
|
<PersonCard
|
||||||
key={`${person.name}-${index}`}
|
key={`${person.name}-${index}`}
|
||||||
imgSrc={fallbackText(person.image, netzwerkContent.patronage.people[index]?.image || '')}
|
imgSrc={managedImage(person.imageUpload, fallbackText(person.image, netzwerkContent.patronage.people[index]?.image || ''))}
|
||||||
imgAlt={fallbackText(person.imageAlt, person.name)}
|
imgAlt={fallbackText(person.imageAlt, person.name)}
|
||||||
name={fallbackText(person.name, netzwerkContent.patronage.people[index]?.name || '')}
|
name={fallbackText(person.name, netzwerkContent.patronage.people[index]?.name || '')}
|
||||||
titleLine1={fallbackText(person.titleLine1, netzwerkContent.patronage.people[index]?.titleLine1 || '')}
|
titleLine1={fallbackText(person.titleLine1, netzwerkContent.patronage.people[index]?.titleLine1 || '')}
|
||||||
|
|||||||
Reference in New Issue
Block a user