feat: implement jury member selection and sorting functionality, update related database migrations
This commit is contained in:
@@ -48,14 +48,6 @@ const personFields: Field[] = [
|
|||||||
text('institution', 'Institution'),
|
text('institution', 'Institution'),
|
||||||
]
|
]
|
||||||
|
|
||||||
const juryMemberFields: Field[] = [
|
|
||||||
text('name', 'Name'),
|
|
||||||
text('role', 'Role'),
|
|
||||||
textarea('bio', 'Bio'),
|
|
||||||
uploadField('imageUpload', 'Image'),
|
|
||||||
text('image', 'Image URL'),
|
|
||||||
]
|
|
||||||
|
|
||||||
const rowFields: Field[] = [text('num', 'Number'), text('label', 'Label'), textarea('body', 'Body')]
|
const rowFields: Field[] = [text('num', 'Number'), text('label', 'Label'), textarea('body', 'Body')]
|
||||||
|
|
||||||
const benefitFields: Field[] = [text('num', 'Number'), text('label', 'Label'), text('sub', 'Subline')]
|
const benefitFields: Field[] = [text('num', 'Number'), text('label', 'Label'), text('sub', 'Subline')]
|
||||||
@@ -152,10 +144,12 @@ export const netzwerkFields: Field[] = [
|
|||||||
{
|
{
|
||||||
name: 'members',
|
name: 'members',
|
||||||
label: 'Members',
|
label: 'Members',
|
||||||
type: 'array',
|
type: 'relationship',
|
||||||
dbName: 'network_jury',
|
relationTo: 'jury-mitglieder',
|
||||||
defaultValue: netzwerkContent.jury.members,
|
hasMany: true,
|
||||||
fields: juryMemberFields,
|
admin: {
|
||||||
|
description: 'Choose the jury members rendered on this page. Members with a quote are displayed before members without one.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
133
src/migrations/20260713_213000_netzwerk_jury_selection.ts
Normal file
133
src/migrations/20260713_213000_netzwerk_jury_selection.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
|
||||||
|
|
||||||
|
type MigrationDB = MigrateUpArgs['db']
|
||||||
|
|
||||||
|
const ident = (value: string) => `\`${value.replace(/`/g, '``')}\``
|
||||||
|
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
|
||||||
|
|
||||||
|
async function tableExists(db: MigrationDB, tableName: string) {
|
||||||
|
const rows = (await db.all(
|
||||||
|
sql.raw(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`),
|
||||||
|
)) as Array<{ name: string }>
|
||||||
|
|
||||||
|
return rows.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async function columnExists(db: MigrationDB, tableName: string, columnName: string) {
|
||||||
|
if (!(await tableExists(db, tableName))) return false
|
||||||
|
|
||||||
|
const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }>
|
||||||
|
return columns.some((column) => column.name === columnName)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addJuryRelationshipColumn(db: MigrationDB, tableName: string) {
|
||||||
|
if (!(await tableExists(db, tableName))) return
|
||||||
|
|
||||||
|
if (!(await columnExists(db, tableName, 'jury_mitglieder_id'))) {
|
||||||
|
await db.run(
|
||||||
|
sql.raw(
|
||||||
|
`ALTER TABLE ${ident(tableName)} ADD ${ident('jury_mitglieder_id')} integer REFERENCES ${ident('jury_mitglieder')}(${ident('id')});`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.run(
|
||||||
|
sql.raw(
|
||||||
|
`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_jury_mitglieder_id_idx`)} ON ${ident(tableName)} (${ident('jury_mitglieder_id')});`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function backfillLivePageSelections(db: MigrationDB) {
|
||||||
|
if (!(await tableExists(db, 'pages_rels')) || !(await tableExists(db, 'jury_mitglieder'))) return
|
||||||
|
|
||||||
|
await db.run(sql.raw(`DELETE FROM ${ident('pages_rels')} WHERE ${ident('path')} = ${literal('netzwerk.jury.members')};`))
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
INSERT INTO ${ident('pages_rels')} (
|
||||||
|
${ident('order')},
|
||||||
|
${ident('parent_id')},
|
||||||
|
${ident('path')},
|
||||||
|
${ident('jury_mitglieder_id')}
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY page.${ident('id')}
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN LENGTH(TRIM(COALESCE(member.${ident('quote')}, ''))) > 0 THEN 0 ELSE 1 END,
|
||||||
|
COALESCE(member.${ident('sort_order')}, 100),
|
||||||
|
member.${ident('id')}
|
||||||
|
),
|
||||||
|
page.${ident('id')},
|
||||||
|
${literal('netzwerk.jury.members')},
|
||||||
|
member.${ident('id')}
|
||||||
|
FROM ${ident('pages')} AS page
|
||||||
|
CROSS JOIN ${ident('jury_mitglieder')} AS member
|
||||||
|
WHERE
|
||||||
|
(page.${ident('spa_path')} = ${literal('/netzwerk')}
|
||||||
|
OR page.${ident('slug')} IN (${literal('netzwerk')}, ${literal('network')}))
|
||||||
|
AND COALESCE(member.${ident('active')}, 1) = 1;
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function backfillVersionSelections(db: MigrationDB) {
|
||||||
|
if (!(await tableExists(db, '_pages_v_rels')) || !(await tableExists(db, 'jury_mitglieder'))) return
|
||||||
|
|
||||||
|
await db.run(
|
||||||
|
sql.raw(`DELETE FROM ${ident('_pages_v_rels')} WHERE ${ident('path')} = ${literal('version.netzwerk.jury.members')};`),
|
||||||
|
)
|
||||||
|
await db.run(sql.raw(`
|
||||||
|
INSERT INTO ${ident('_pages_v_rels')} (
|
||||||
|
${ident('order')},
|
||||||
|
${ident('parent_id')},
|
||||||
|
${ident('path')},
|
||||||
|
${ident('jury_mitglieder_id')}
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY version.${ident('id')}
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN LENGTH(TRIM(COALESCE(member.${ident('quote')}, ''))) > 0 THEN 0 ELSE 1 END,
|
||||||
|
COALESCE(member.${ident('sort_order')}, 100),
|
||||||
|
member.${ident('id')}
|
||||||
|
),
|
||||||
|
version.${ident('id')},
|
||||||
|
${literal('version.netzwerk.jury.members')},
|
||||||
|
member.${ident('id')}
|
||||||
|
FROM ${ident('_pages_v')} AS version
|
||||||
|
CROSS JOIN ${ident('jury_mitglieder')} AS member
|
||||||
|
WHERE
|
||||||
|
(version.${ident('version_spa_path')} = ${literal('/netzwerk')}
|
||||||
|
OR version.${ident('version_slug')} IN (${literal('netzwerk')}, ${literal('network')}))
|
||||||
|
AND COALESCE(member.${ident('active')}, 1) = 1;
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeJuryRelationshipColumn(db: MigrationDB, tableName: string) {
|
||||||
|
if (!(await tableExists(db, tableName))) return
|
||||||
|
|
||||||
|
await db.run(sql.raw(`DROP INDEX IF EXISTS ${ident(`${tableName}_jury_mitglieder_id_idx`)};`))
|
||||||
|
if (await columnExists(db, tableName, 'jury_mitglieder_id')) {
|
||||||
|
await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} DROP COLUMN ${ident('jury_mitglieder_id')};`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
|
||||||
|
await addJuryRelationshipColumn(db, 'pages_rels')
|
||||||
|
await addJuryRelationshipColumn(db, '_pages_v_rels')
|
||||||
|
await backfillLivePageSelections(db)
|
||||||
|
await backfillVersionSelections(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
|
||||||
|
if (await tableExists(db, 'pages_rels')) {
|
||||||
|
await db.run(sql.raw(`DELETE FROM ${ident('pages_rels')} WHERE ${ident('path')} = ${literal('netzwerk.jury.members')};`))
|
||||||
|
}
|
||||||
|
if (await tableExists(db, '_pages_v_rels')) {
|
||||||
|
await db.run(
|
||||||
|
sql.raw(`DELETE FROM ${ident('_pages_v_rels')} WHERE ${ident('path')} = ${literal('version.netzwerk.jury.members')};`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await removeJuryRelationshipColumn(db, '_pages_v_rels')
|
||||||
|
await removeJuryRelationshipColumn(db, 'pages_rels')
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import * as migration_20260713_133000_newsletter_form_and_subscriptions from './
|
|||||||
import * as migration_20260713_155500_shared_application_form from './20260713_155500_shared_application_form';
|
import * as migration_20260713_155500_shared_application_form from './20260713_155500_shared_application_form';
|
||||||
import * as migration_20260713_163000_participation_supporting_awards_copy from './20260713_163000_participation_supporting_awards_copy';
|
import * as migration_20260713_163000_participation_supporting_awards_copy from './20260713_163000_participation_supporting_awards_copy';
|
||||||
import * as migration_20260713_194500_contact_submissions from './20260713_194500_contact_submissions';
|
import * as migration_20260713_194500_contact_submissions from './20260713_194500_contact_submissions';
|
||||||
|
import * as migration_20260713_213000_netzwerk_jury_selection from './20260713_213000_netzwerk_jury_selection';
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
@@ -90,4 +91,9 @@ export const migrations = [
|
|||||||
down: migration_20260713_194500_contact_submissions.down,
|
down: migration_20260713_194500_contact_submissions.down,
|
||||||
name: '20260713_194500_contact_submissions',
|
name: '20260713_194500_contact_submissions',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260713_213000_netzwerk_jury_selection.up,
|
||||||
|
down: migration_20260713_213000_netzwerk_jury_selection.down,
|
||||||
|
name: '20260713_213000_netzwerk_jury_selection',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1765,16 +1765,10 @@ export interface Page {
|
|||||||
description?: string | null;
|
description?: string | null;
|
||||||
chairBadge?: string | null;
|
chairBadge?: string | null;
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
members?:
|
/**
|
||||||
| {
|
* Choose the jury members rendered on this page. Members with a quote are displayed before members without one.
|
||||||
name?: string | null;
|
*/
|
||||||
role?: string | null;
|
members?: (number | JuryMitglieder)[] | null;
|
||||||
bio?: string | null;
|
|
||||||
imageUpload?: (number | null) | Media;
|
|
||||||
image?: string | null;
|
|
||||||
id?: string | null;
|
|
||||||
}[]
|
|
||||||
| null;
|
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Evaluation process rows and quote.
|
* Evaluation process rows and quote.
|
||||||
@@ -4398,16 +4392,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
description?: T;
|
description?: T;
|
||||||
chairBadge?: T;
|
chairBadge?: T;
|
||||||
note?: T;
|
note?: T;
|
||||||
members?:
|
members?: T;
|
||||||
| T
|
|
||||||
| {
|
|
||||||
name?: T;
|
|
||||||
role?: T;
|
|
||||||
bio?: T;
|
|
||||||
imageUpload?: T;
|
|
||||||
image?: T;
|
|
||||||
id?: T;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
expertise?:
|
expertise?:
|
||||||
| T
|
| T
|
||||||
|
|||||||
@@ -76,6 +76,44 @@ async function upsertPartners(payload: Payload) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function juryMembersForPage(payload: Payload) {
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: 'jury-mitglieder',
|
||||||
|
limit: 300,
|
||||||
|
pagination: false,
|
||||||
|
draft: true,
|
||||||
|
overrideAccess: true,
|
||||||
|
sort: 'sortOrder',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existing.docs.length) {
|
||||||
|
return existing.docs.filter((member) => member.active !== false).map((member) => member.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const membersWithUploads = await itemsWithImageUploads(payload, netzwerkContent.jury.members)
|
||||||
|
const created = await Promise.all(
|
||||||
|
membersWithUploads.map((member, index) =>
|
||||||
|
payload.create({
|
||||||
|
collection: 'jury-mitglieder',
|
||||||
|
overrideAccess: true,
|
||||||
|
context: { disableRevalidate: true },
|
||||||
|
data: {
|
||||||
|
name: member.name,
|
||||||
|
image: member.imageUpload,
|
||||||
|
imageAlt: member.name,
|
||||||
|
role: member.role,
|
||||||
|
bio: member.bio,
|
||||||
|
sortOrder: index,
|
||||||
|
active: true,
|
||||||
|
_status: 'published',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return created.map((member) => member.id)
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
@@ -100,7 +138,7 @@ async function main() {
|
|||||||
const { items: _partnerItems, ...partnerSectionContent } = netzwerkContent.partners
|
const { items: _partnerItems, ...partnerSectionContent } = netzwerkContent.partners
|
||||||
const [patronPeople, juryMembers] = await Promise.all([
|
const [patronPeople, juryMembers] = await Promise.all([
|
||||||
itemsWithImageUploads(payload, netzwerkContent.patronage.people),
|
itemsWithImageUploads(payload, netzwerkContent.patronage.people),
|
||||||
itemsWithImageUploads(payload, netzwerkContent.jury.members),
|
juryMembersForPage(payload),
|
||||||
])
|
])
|
||||||
|
|
||||||
await payload.update({
|
await payload.update({
|
||||||
|
|||||||
30
src/spa/juryMembers.ts
Normal file
30
src/spa/juryMembers.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { resolveCmsRelationship, type CmsRouteDoc } from '@/spa/cmsRoute'
|
||||||
|
|
||||||
|
const hasText = (value: unknown) => typeof value === 'string' && value.trim().length > 0
|
||||||
|
|
||||||
|
export function selectJuryDocs(selection: unknown, docs: CmsRouteDoc[]) {
|
||||||
|
if (selection === undefined) return docs
|
||||||
|
if (!Array.isArray(selection)) return []
|
||||||
|
|
||||||
|
return selection
|
||||||
|
.map((entry) => resolveCmsRelationship(entry, docs))
|
||||||
|
.filter(
|
||||||
|
(member): member is CmsRouteDoc =>
|
||||||
|
Boolean(member && docs.some((doc) => String(doc.id) === String(member.id))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortJuryMembersQuoteFirst<T extends object>(members: T[]) {
|
||||||
|
return members
|
||||||
|
.map((member, index) => ({ member, index }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aSortable = a.member as { quote?: unknown; sortOrder?: number | null }
|
||||||
|
const bSortable = b.member as { quote?: unknown; sortOrder?: number | null }
|
||||||
|
const quoteOrder = Number(hasText(bSortable.quote)) - Number(hasText(aSortable.quote))
|
||||||
|
if (quoteOrder !== 0) return quoteOrder
|
||||||
|
|
||||||
|
const sortOrder = Number(aSortable.sortOrder ?? a.index) - Number(bSortable.sortOrder ?? b.index)
|
||||||
|
return sortOrder !== 0 ? sortOrder : a.index - b.index
|
||||||
|
})
|
||||||
|
.map(({ member }) => member)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
|||||||
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||||
import { homeContent } from '@/spa/homeContent';
|
import { homeContent } from '@/spa/homeContent';
|
||||||
|
import { selectJuryDocs, sortJuryMembersQuoteFirst } from '@/spa/juryMembers';
|
||||||
import { netzwerkContent } from '@/spa/netzwerkContent';
|
import { netzwerkContent } from '@/spa/netzwerkContent';
|
||||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo';
|
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo';
|
||||||
@@ -16,8 +17,9 @@ const FB = '"Inter", sans-serif';
|
|||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
const GOLD = '#EFBF04';
|
const GOLD = '#EFBF04';
|
||||||
|
|
||||||
type NetzwerkCms = Partial<typeof netzwerkContent> & {
|
type NetzwerkCms = Omit<Partial<typeof netzwerkContent>, 'hero' | 'jury' | 'sponsoring'> & {
|
||||||
hero?: Partial<typeof netzwerkContent.hero> & { image?: unknown }
|
hero?: Partial<typeof netzwerkContent.hero> & { image?: unknown }
|
||||||
|
jury?: Partial<Omit<typeof netzwerkContent.jury, 'members'>> & { members?: unknown[] | null }
|
||||||
sponsoring?: Partial<typeof netzwerkContent.sponsoring> & { image?: unknown }
|
sponsoring?: Partial<typeof netzwerkContent.sponsoring> & { image?: unknown }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +78,15 @@ const normalizeJuryMember = (member: CmsRouteDoc): JuryMember => {
|
|||||||
const organization = fallbackText(member.organization, '')
|
const organization = fallbackText(member.organization, '')
|
||||||
const rawRole = fallbackText(member.role, '')
|
const rawRole = fallbackText(member.role, '')
|
||||||
const role = rawRole && organization && rawRole !== organization ? `${rawRole} / ${organization}` : fallbackText(rawRole, organization)
|
const role = rawRole && organization && rawRole !== organization ? `${rawRole} / ${organization}` : fallbackText(rawRole, organization)
|
||||||
|
const image = member.image ?? member.imageUpload
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: fallbackText(member.name, ''),
|
name: fallbackText(member.name, ''),
|
||||||
role,
|
role,
|
||||||
bio: fallbackText(member.bio, ''),
|
bio: fallbackText(member.bio, ''),
|
||||||
image: mediaUrl(member.image, juryImageFallback),
|
image: mediaUrl(image, juryImageFallback),
|
||||||
imageAlt: fallbackText(member.imageAlt, fallbackText(member.name, 'Jury-Mitglied')),
|
imageAlt: fallbackText(member.imageAlt, fallbackText(member.name, 'Jury-Mitglied')),
|
||||||
imageUpload: member.image,
|
imageUpload: image,
|
||||||
organization,
|
organization,
|
||||||
quote: fallbackText(member.quote, ''),
|
quote: fallbackText(member.quote, ''),
|
||||||
sortOrder: typeof member.sortOrder === 'number' ? member.sortOrder : undefined,
|
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 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 }) {
|
function Lines({ text, highlight }: { text: string; highlight?: string }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -494,10 +485,16 @@ const Netzwerk: React.FC = () => {
|
|||||||
const partnerModal = { ...netzwerkContent.partners.modal, ...((cms.partners as typeof netzwerkContent.partners | undefined)?.modal || {}) };
|
const partnerModal = { ...netzwerkContent.partners.modal, ...((cms.partners as typeof netzwerkContent.partners | undefined)?.modal || {}) };
|
||||||
const sponsoring = { ...netzwerkContent.sponsoring, ...(cms.sponsoring || {}) };
|
const sponsoring = { ...netzwerkContent.sponsoring, ...(cms.sponsoring || {}) };
|
||||||
const patronPeople = fallbackArray<PatronPerson>(patronage.people, netzwerkContent.patronage.people);
|
const patronPeople = fallbackArray<PatronPerson>(patronage.people, netzwerkContent.patronage.people);
|
||||||
const juryCollectionMembers = cmsJuryMembers.map(normalizeJuryMember).filter((member) => member.active !== false && member.name);
|
const cmsJurySelection = cms.jury?.members;
|
||||||
const juryMembers = sortedJuryMembers(juryCollectionMembers.length
|
const hasCmsJurySelection = cmsJurySelection !== undefined;
|
||||||
? juryCollectionMembers
|
const sourcedJuryDocs = selectJuryDocs(cmsJurySelection, cmsJuryMembers);
|
||||||
: fallbackArray<JuryMember>(jury.members, netzwerkContent.jury.members));
|
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 expertiseRows = fallbackArray<ExpertiseRow>(expertise.rows, netzwerkContent.expertise.rows);
|
||||||
const partners = sortedPartners(cmsPartners)
|
const partners = sortedPartners(cmsPartners)
|
||||||
.filter((partner) => partner.active !== false)
|
.filter((partner) => partner.active !== false)
|
||||||
@@ -710,9 +707,11 @@ const Netzwerk: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* ── Prominenter Vorsitz-Bereich ─────────────────────────────────── */}
|
{/* ── Prominenter Vorsitz-Bereich ─────────────────────────────────── */}
|
||||||
{/* TODO: echten Jury-Vorsitzenden bestätigen (Name & Foto sind Platzhalter) */}
|
{/* TODO: echten Jury-Vorsitzenden bestätigen (Name & Foto sind Platzhalter) */}
|
||||||
<div style={{ padding: isMobile ? '28px 24px 8px' : '64px 80px 16px' }}>
|
{juryMembers.length ? (
|
||||||
<JuryChairCard member={juryMembers[0]} badge={fallbackText(jury.chairBadge, netzwerkContent.jury.chairBadge)} />
|
<div style={{ padding: isMobile ? '28px 24px 8px' : '64px 80px 16px' }}>
|
||||||
</div>
|
<JuryChairCard member={juryMembers[0]} badge={fallbackText(jury.chairBadge, netzwerkContent.jury.chairBadge)} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* ── Übrige Jury-Mitglieder ──────────────────────────────────────── */}
|
{/* ── Ü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)' }}>
|
<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)' }}>
|
||||||
|
|||||||
33
tests/int/jury-members.int.spec.ts
Normal file
33
tests/int/jury-members.int.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { selectJuryDocs, sortJuryMembersQuoteFirst } from '@/spa/juryMembers'
|
||||||
|
|
||||||
|
describe('Netzwerk jury members', () => {
|
||||||
|
const docs = [
|
||||||
|
{ id: 1, name: 'First' },
|
||||||
|
{ id: 2, name: 'Second' },
|
||||||
|
{ id: 3, name: 'Third' },
|
||||||
|
]
|
||||||
|
|
||||||
|
it('uses the CMS relationship selection when it is present', () => {
|
||||||
|
expect(selectJuryDocs([3, { id: 1 }], docs).map((member) => member.id)).toEqual([3, 1])
|
||||||
|
expect(selectJuryDocs([], docs)).toEqual([])
|
||||||
|
expect(selectJuryDocs(undefined, docs)).toEqual(docs)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('places non-empty quotes first and preserves sort order within each group', () => {
|
||||||
|
const members = [
|
||||||
|
{ name: 'Unquoted early', quote: '', sortOrder: 1 },
|
||||||
|
{ name: 'Quoted later', quote: 'Quote', sortOrder: 20 },
|
||||||
|
{ name: 'Quoted earlier', quote: ' Another quote ', sortOrder: 10 },
|
||||||
|
{ name: 'Unquoted later', quote: ' ', sortOrder: 30 },
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(sortJuryMembersQuoteFirst(members).map((member) => member.name)).toEqual([
|
||||||
|
'Quoted earlier',
|
||||||
|
'Quoted later',
|
||||||
|
'Unquoted early',
|
||||||
|
'Unquoted later',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user