Skip to content

Commit a982bd1

Browse files
HugoGresseclaudeCopilot
authored
Claude/sponsor custom fields (#224)
* Add sponsor custom fields (boolean/text) configurable per event - Add SponsorCustomField type and sponsorCustomFields to Event interface - Add customFields map to Sponsor interface - Add SponsorCustomFieldsFields settings component for defining fields globally - Add collapsible "Sponsor custom fields" section in EventSettings page - Render custom fields (text inputs / switches) in SponsorForm based on event config - Custom fields are included in published JSON via existing sponsor spread https://claude.ai/code/session_01RSsX3a4wrxGfQoVQbGCGxS * Fix sponsor custom fields: type safety, defaults, and duplicate handling - Remove `as any` cast by typing useForm<Sponsor> explicitly - Merge existing sponsor customFields with defaults so new fields appear when editing sponsors created before the field was added - Change debug label `id: autogenerated-...` to proper "Field name" label, showing slug ID only as helper text for saved fields - Deduplicate custom fields by slugified ID to prevent silent data loss - Remove unused MenuItem import https://claude.ai/code/session_01RSsX3a4wrxGfQoVQbGCGxS * Add sponsorCustomFields to new event creation Fixes TS2322 error: the required sponsorCustomFields property was missing from the NewEvent object in NewEventDialog. https://claude.ai/code/session_01RSsX3a4wrxGfQoVQbGCGxS * Add sponsor custom fields with GCP JSON export support (#225) * Initial plan * Include sponsorCustomFields field definitions in GCP storage JSON output Co-authored-by: HugoGresse <662377+HugoGresse@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: HugoGresse <662377+HugoGresse@users.noreply.github.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: HugoGresse <662377+HugoGresse@users.noreply.github.com>
1 parent 926eabe commit a982bd1

File tree

8 files changed

+196
-12
lines changed

8 files changed

+196
-12
lines changed

functions/src/api/routes/deploy/updateWebsiteActions/generateStaticJson.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export const generateStaticJson = async (firebaseApp: firebase.app.App, event: E
138138
logoUrl: event.logoUrl,
139139
logoUrl2: event.logoUrl2,
140140
backgroundUrl: event.backgroundUrl,
141+
sponsorCustomFields: event.sponsorCustomFields || [],
141142
}
142143

143144
const outputPublic: JsonPublicOutput = {

functions/src/api/routes/deploy/updateWebsiteActions/jsonTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Category, Track, Format, SponsorCategory, TeamMember, FaqCategory, Social } from '../../../../../../src/types'
1+
import { Category, Track, Format, SponsorCategory, TeamMember, FaqCategory, Social, SponsorCustomField } from '../../../../../../src/types'
22

33
export interface JsonSession {
44
id: string
@@ -59,6 +59,7 @@ export interface JsonEvent {
5959
logoUrl: string | null | undefined
6060
logoUrl2: string | null | undefined
6161
backgroundUrl: string | null | undefined
62+
sponsorCustomFields: SponsorCustomField[]
6263
}
6364

6465
export interface JsonPublicOutput {

src/events/new/NewEventDialog.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export const NewEventDialog = ({ isOpen, onClose }: NewEventDialogProps) => {
8585
colorSecondary: null,
8686
colorBackground: null,
8787
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
88+
sponsorCustomFields: [],
8889
publicEnabled: false,
8990
}
9091
mutation

src/events/page/settings/EventSettings.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { deleteSessionsAndSpeakers } from '../../actions/deleteSessionsAndSpeake
2121
import { CategoriesFields } from './components/CategoriesFields'
2222
import { FormatsFields } from './components/FormatsFields'
2323
import { TrackFields } from './components/TrackFields'
24+
import { SponsorCustomFieldsFields } from './components/SponsorCustomFieldsFields'
2425
import { mapEventSettingsFormToMutateObject } from './mapEventSettingsFormToMutateObject'
2526
import { SaveShortcut } from '../../../components/form/SaveShortcut'
2627
import { EventSettingsFormatCategoriesGrid } from './EventSettingsFormatCategoriesGrid'
@@ -38,6 +39,7 @@ const schema = yup
3839
const convertInputEvent = (event: Event): EventForForm => {
3940
return {
4041
...event,
42+
sponsorCustomFields: event.sponsorCustomFields || [],
4143
timezone: event.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
4244
dates: {
4345
start: event.dates.start ? DateTime.fromJSDate(event.dates.start).toFormat("kkkk-LL-dd'T'T") : null,
@@ -55,6 +57,7 @@ export const EventSettings = ({ event }: EventSettingsProps) => {
5557
const [expandedGeneralInfo, setExpandedGeneralInfo] = useState(true)
5658
const [expandedMoreDetails, setExpandedMoreDetails] = useState(true)
5759
const [expandedTracks, setExpandedTracks] = useState(true)
60+
const [expandedSponsorFields, setExpandedSponsorFields] = useState(true)
5861
const { createNotification } = useNotification()
5962
const documentDeletion = useFirestoreDocumentDeletion(doc(collections.events, event.id))
6063
const mutation = useFirestoreDocumentMutation(doc(collections.events, event.id))
@@ -380,6 +383,39 @@ export const EventSettings = ({ event }: EventSettingsProps) => {
380383
</Grid>
381384
</Collapse>
382385
</Card>
386+
<Card sx={{ paddingX: 2, mt: 4 }}>
387+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', pb: 2 }}>
388+
<Typography fontSize="large" sx={{ mt: 2 }}>
389+
{' '}
390+
Sponsor custom fields{' '}
391+
</Typography>
392+
<IconButton
393+
size="small"
394+
onClick={() => setExpandedSponsorFields(!expandedSponsorFields)}
395+
sx={{
396+
transform: expandedSponsorFields ? 'rotate(0deg)' : 'rotate(-90deg)',
397+
transition: '0.3s',
398+
}}>
399+
<ExpandMoreIcon />
400+
</IconButton>
401+
</Box>
402+
<Collapse in={expandedSponsorFields}>
403+
<SponsorCustomFieldsFields control={control} isSubmitting={formState.isSubmitting} />
404+
405+
<Grid item xs={12}>
406+
<LoadingButton
407+
type="submit"
408+
disabled={formState.isSubmitting}
409+
loading={formState.isSubmitting}
410+
fullWidth
411+
variant="contained"
412+
sx={{ mt: 2, mb: 2 }}>
413+
Save
414+
</LoadingButton>
415+
{mutation.error && <Typography color="error">{mutation.error.message}</Typography>}
416+
</Grid>
417+
</Collapse>
418+
</Card>
383419
{!deleteOpen && <SaveShortcut />}
384420
</FormContainer>
385421

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as React from 'react'
2+
import { Box, IconButton, Typography } from '@mui/material'
3+
import { Control, TextFieldElement, SelectElement, useFieldArray } from 'react-hook-form-mui'
4+
import { Add, Delete } from '@mui/icons-material'
5+
import { EventForForm } from '../../../../types'
6+
7+
export type SponsorCustomFieldsFieldsProps = {
8+
control: Control<EventForForm, any>
9+
isSubmitting: boolean
10+
}
11+
12+
export const SponsorCustomFieldsFields = ({ control, isSubmitting }: SponsorCustomFieldsFieldsProps) => {
13+
const { fields, append, remove } = useFieldArray({
14+
control,
15+
name: 'sponsorCustomFields',
16+
keyName: 'key',
17+
})
18+
19+
return (
20+
<>
21+
<Typography fontWeight="600" mt={2}>
22+
Sponsor custom fields
23+
</Typography>
24+
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
25+
Define custom fields that will be available on every sponsor. These fields will also appear in the
26+
published JSON.
27+
</Typography>
28+
29+
<Box paddingLeft={2}>
30+
{fields.map((field, index) => (
31+
<Box display="flex" key={field.key} alignItems="center" gap={1}>
32+
<TextFieldElement
33+
label="Field name"
34+
name={`sponsorCustomFields.${index}.name`}
35+
control={control}
36+
variant="filled"
37+
size="small"
38+
margin="dense"
39+
disabled={isSubmitting}
40+
helperText={
41+
!field.id.startsWith('autogenerated-') ? `id: ${field.id}` : undefined
42+
}
43+
/>
44+
<SelectElement
45+
label="Type"
46+
name={`sponsorCustomFields.${index}.type`}
47+
control={control}
48+
variant="filled"
49+
size="small"
50+
margin="dense"
51+
disabled={isSubmitting}
52+
options={[
53+
{ id: 'text', label: 'Text' },
54+
{ id: 'boolean', label: 'Boolean' },
55+
]}
56+
sx={{ minWidth: 120 }}
57+
/>
58+
<IconButton
59+
aria-label="Remove custom field"
60+
onClick={() => {
61+
remove(index)
62+
}}
63+
edge="end">
64+
<Delete />
65+
</IconButton>
66+
</Box>
67+
))}
68+
<IconButton
69+
aria-label="Add sponsor custom field"
70+
onClick={() => {
71+
append({
72+
id: `autogenerated-${Date.now()}`,
73+
name: '',
74+
type: 'text',
75+
})
76+
}}>
77+
<Add />
78+
</IconButton>
79+
</Box>
80+
</>
81+
)
82+
}

src/events/page/settings/mapEventSettingsFormToMutateObject.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DateTime } from 'luxon'
2-
import { Category, Event, EventForForm, EventSettingForForm, Format, Track } from '../../../types'
2+
import { Category, Event, EventForForm, EventSettingForForm, Format, SponsorCustomField, Track } from '../../../types'
33
import { slugify } from '../../../utils/slugify'
44
import { DEFAULT_SESSION_CARD_BACKGROUND_COLOR } from '../schedule/scheduleConstants'
55

@@ -36,6 +36,16 @@ export const mapEventSettingsFormToMutateObject = (event: Event, data: EventForF
3636
const transcriptionPassword = data.transcriptionPassword || ''
3737
const timezone = data.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone
3838

39+
const sponsorCustomFieldsRaw: SponsorCustomField[] = (data.sponsorCustomFields || [])
40+
.filter((field) => field.name && field.name.trim().length > 0)
41+
.map((field) => ({
42+
name: field.name.trim(),
43+
type: field.type,
44+
id: field.id.startsWith('autogenerated-') ? slugify(field.name.trim()) : field.id,
45+
}))
46+
// Deduplicate by id (last wins) to prevent silent data loss from duplicate names
47+
const sponsorCustomFields = [...new Map(sponsorCustomFieldsRaw.map((f) => [f.id, f])).values()]
48+
3949
return {
4050
...event,
4151
name: eventName,
@@ -57,6 +67,7 @@ export const mapEventSettingsFormToMutateObject = (event: Event, data: EventForF
5767
gladiaAPIKey,
5868
transcriptionPassword,
5969
timezone,
70+
sponsorCustomFields,
6071
}
6172
}
6273

src/events/page/sponsors/components/SponsorForm.tsx

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react'
22
import { Event, Sponsor } from '../../../../types'
3-
import { FormContainer, TextFieldElement, useForm } from 'react-hook-form-mui'
4-
import { Grid } from '@mui/material'
3+
import { FormContainer, TextFieldElement, SwitchElement, useForm } from 'react-hook-form-mui'
4+
import { Grid, Typography } from '@mui/material'
55
import LoadingButton from '@mui/lab/LoadingButton'
66
import { ImageTextFieldElement } from '../../../../components/form/ImageTextFieldElement'
77
import { SaveShortcut } from '../../../../components/form/SaveShortcut'
@@ -12,14 +12,24 @@ export type SponsorFormProps = {
1212
onSubmit: (sponsor: Sponsor) => void
1313
}
1414
export const SponsorForm = ({ event, sponsor, onSubmit }: SponsorFormProps) => {
15-
const formContext = useForm({
16-
defaultValues: sponsor
17-
? sponsor
18-
: {
19-
name: '',
20-
logoUrl: '',
21-
website: undefined,
22-
},
15+
const customFields = event.sponsorCustomFields || []
16+
17+
const defaultCustomFields = customFields.reduce(
18+
(acc, field) => {
19+
acc[field.id] = field.type === 'boolean' ? false : ''
20+
return acc
21+
},
22+
{} as { [key: string]: string | boolean }
23+
)
24+
25+
const formContext = useForm<Sponsor>({
26+
defaultValues: {
27+
id: sponsor?.id || '',
28+
name: sponsor?.name || '',
29+
logoUrl: sponsor?.logoUrl || '',
30+
website: sponsor?.website || undefined,
31+
customFields: { ...defaultCustomFields, ...sponsor?.customFields },
32+
},
2333
})
2434
const { formState } = formContext
2535

@@ -29,12 +39,19 @@ export const SponsorForm = ({ event, sponsor, onSubmit }: SponsorFormProps) => {
2939
<FormContainer
3040
formContext={formContext}
3141
onSuccess={async (data) => {
42+
const customFieldValues: { [key: string]: string | boolean } = {}
43+
for (const field of customFields) {
44+
const value = data.customFields?.[field.id]
45+
customFieldValues[field.id] = field.type === 'boolean' ? !!value : value || ''
46+
}
47+
3248
return onSubmit({
3349
...sponsor,
3450
id: sponsor?.id || '',
3551
name: data.name,
3652
logoUrl: data.logoUrl,
3753
website: data.website || null,
54+
customFields: customFieldValues,
3855
} as Sponsor)
3956
}}>
4057
<Grid container spacing={4}>
@@ -80,6 +97,33 @@ export const SponsorForm = ({ event, sponsor, onSubmit }: SponsorFormProps) => {
8097
disabled={isSubmitting}
8198
type="url"
8299
/>
100+
101+
{customFields.length > 0 && (
102+
<>
103+
<Typography fontWeight="600" mt={2} mb={1}>
104+
Custom fields
105+
</Typography>
106+
{customFields.map((field) =>
107+
field.type === 'boolean' ? (
108+
<SwitchElement
109+
key={field.id}
110+
label={field.name}
111+
name={`customFields.${field.id}`}
112+
/>
113+
) : (
114+
<TextFieldElement
115+
key={field.id}
116+
margin="dense"
117+
fullWidth
118+
label={field.name}
119+
name={`customFields.${field.id}`}
120+
variant="filled"
121+
disabled={isSubmitting}
122+
/>
123+
)
124+
)}
125+
</>
126+
)}
83127
</Grid>
84128
</Grid>
85129

src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ export interface EventFiles {
117117
pdf: string | null
118118
}
119119

120+
export interface SponsorCustomField {
121+
id: string
122+
name: string
123+
type: 'boolean' | 'text'
124+
}
125+
120126
export interface EventShortVidSettings {
121127
template: string | null
122128
server: string | null
@@ -165,6 +171,7 @@ export interface Event {
165171
color: string | null
166172
colorSecondary: string | null
167173
colorBackground: string | null
174+
sponsorCustomFields: SponsorCustomField[]
168175
bupherSession?: string | null
169176
bupherOrganizationId?: string | null
170177
timezone: string | null
@@ -198,6 +205,7 @@ export interface Sponsor {
198205
logoUrl: string
199206
website: string | null
200207
jobPostToken?: string | null // Unique token for sponsor's job management page
208+
customFields?: { [key: string]: string | boolean }
201209
}
202210

203211
export interface SponsorCategory {

0 commit comments

Comments
 (0)