@@ -10,8 +10,10 @@ import {
1010 Group ,
1111 ActionIcon ,
1212 Text ,
13+ Alert ,
1314} from "@mantine/core" ;
14- import { DateTimePicker } from "@mantine/dates" ;
15+ import moment from "moment-timezone" ;
16+ import { DateFormatter , DatePickerInput , DateTimePicker } from "@mantine/dates" ;
1517import { useForm , zodResolver } from "@mantine/form" ;
1618import { notifications } from "@mantine/notifications" ;
1719import dayjs from "dayjs" ;
@@ -23,7 +25,7 @@ import { useApi } from "@ui/util/api";
2325import { AllOrganizationList as orgList } from "@acm-uiuc/js-shared" ;
2426import { AppRoles } from "@common/roles" ;
2527import { EVENT_CACHED_DURATION } from "@common/config" ;
26- import { IconPlus , IconTrash } from "@tabler/icons-react" ;
28+ import { IconInfoCircle , IconPlus , IconTrash } from "@tabler/icons-react" ;
2729import {
2830 MAX_METADATA_KEYS ,
2931 MAX_KEY_LENGTH ,
@@ -34,6 +36,23 @@ import {
3436export function capitalizeFirstLetter ( string : string ) {
3537 return string . charAt ( 0 ) . toUpperCase ( ) + string . slice ( 1 ) ;
3638}
39+ const valueFormatter : DateFormatter = ( { type, date, locale, format } ) => {
40+ if ( type === "multiple" && Array . isArray ( date ) ) {
41+ if ( date . length === 1 ) {
42+ return dayjs ( date [ 0 ] ) . locale ( locale ) . format ( format ) ;
43+ }
44+
45+ if ( date . length > 1 ) {
46+ return date
47+ . map ( ( d ) => dayjs ( d ) . locale ( locale ) . format ( format ) )
48+ . join ( " | " ) ;
49+ }
50+
51+ return "" ;
52+ }
53+
54+ return "" ;
55+ } ;
3756
3857const repeatOptions = [ "weekly" , "biweekly" ] as const ;
3958
@@ -50,14 +69,14 @@ const baseBodySchema = z.object({
5069 . string ( )
5170 . min ( 1 , "Paid Event ID must be at least 1 character" )
5271 . optional ( ) ,
53- // Add metadata field
5472 metadata : metadataSchema ,
5573} ) ;
5674
5775const requestBodySchema = baseBodySchema
5876 . extend ( {
5977 repeats : z . optional ( z . enum ( repeatOptions ) ) . nullable ( ) ,
6078 repeatEnds : z . date ( ) . optional ( ) ,
79+ repeatExcludes : z . array ( z . date ( ) ) . max ( 100 ) . optional ( ) ,
6180 } )
6281 . refine ( ( data ) => ( data . repeatEnds ? data . repeats !== undefined : true ) , {
6382 message : "Repeat frequency is required when Repeat End is specified." ,
@@ -86,7 +105,6 @@ export const ManageEventPage: React.FC = () => {
86105 if ( ! isEditing ) {
87106 return ;
88107 }
89- // Fetch event data and populate form
90108 const getEvent = async ( ) => {
91109 try {
92110 const response = await api . get (
@@ -108,6 +126,12 @@ export const ManageEventPage: React.FC = () => {
108126 ? new Date ( eventData . repeatEnds )
109127 : undefined ,
110128 paidEventId : eventData . paidEventId ,
129+ repeatExcludes :
130+ eventData . repeatExcludes && eventData . repeatExcludes . length > 0
131+ ? eventData . repeatExcludes . map ( ( dateString : string ) =>
132+ moment . tz ( dateString , "America/Chicago" ) . toDate ( ) ,
133+ )
134+ : [ ] ,
111135 metadata : eventData . metadata || { } ,
112136 } ;
113137 form . setValues ( formValues ) ;
@@ -128,29 +152,33 @@ export const ManageEventPage: React.FC = () => {
128152 title : "" ,
129153 description : "" ,
130154 start : new Date ( startDate ) ,
131- end : new Date ( startDate + 3.6e6 ) , // 1 hr later
155+ end : new Date ( startDate + 3.6e6 ) ,
132156 location : "ACM Room (Siebel CS 1104)" ,
133157 locationLink : "https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8" ,
134158 host : "ACM" ,
135159 featured : false ,
136160 repeats : undefined ,
137161 repeatEnds : undefined ,
138162 paidEventId : undefined ,
139- metadata : { } , // Initialize empty metadata object
163+ metadata : { } ,
164+ repeatExcludes : [ ] ,
140165 } ,
141166 } ) ;
142167
143168 useEffect ( ( ) => {
144169 if ( form . values . end && form . values . end <= form . values . start ) {
145- form . setFieldValue ( "end" , new Date ( form . values . start . getTime ( ) + 3.6e6 ) ) ; // 1 hour after the start date
170+ form . setFieldValue ( "end" , new Date ( form . values . start . getTime ( ) + 3.6e6 ) ) ;
146171 }
147172 } , [ form . values . start ] ) ;
148173
149174 useEffect ( ( ) => {
150175 if ( form . values . locationLink === "" ) {
151176 form . setFieldValue ( "locationLink" , undefined ) ;
152177 }
153- } , [ form . values . locationLink ] ) ;
178+ if ( form . values . repeatExcludes ?. length === 0 ) {
179+ form . setFieldValue ( "repeatExcludes" , undefined ) ;
180+ }
181+ } , [ form . values . locationLink , form . values . repeatExcludes ] ) ;
154182
155183 const handleSubmit = async ( values : EventPostRequest ) => {
156184 try {
@@ -166,6 +194,9 @@ export const ManageEventPage: React.FC = () => {
166194 values . repeatEnds && values . repeats
167195 ? dayjs ( values . repeatEnds ) . format ( "YYYY-MM-DD[T]HH:mm:00" )
168196 : undefined ,
197+ repeatExcludes : values . repeatExcludes
198+ ? values . repeatExcludes . map ( ( x ) => dayjs ( x ) . format ( "YYYY-MM-DD" ) )
199+ : undefined ,
169200 repeats : values . repeats ? values . repeats : undefined ,
170201 metadata :
171202 Object . keys ( values . metadata || { } ) . length > 0
@@ -191,7 +222,6 @@ export const ManageEventPage: React.FC = () => {
191222 }
192223 } ;
193224
194- // Function to add a new metadata field
195225 const addMetadataField = ( ) => {
196226 const currentMetadata = { ...form . values . metadata } ;
197227 if ( Object . keys ( currentMetadata ) . length >= MAX_METADATA_KEYS ) {
@@ -201,14 +231,11 @@ export const ManageEventPage: React.FC = () => {
201231 return ;
202232 }
203233
204- // Generate a temporary key name that doesn't exist yet
205234 let tempKey = `key${ Object . keys ( currentMetadata ) . length + 1 } ` ;
206- // Make sure it's unique
207235 while ( currentMetadata [ tempKey ] !== undefined ) {
208236 tempKey = `key${ parseInt ( tempKey . replace ( "key" , "" ) , 10 ) + 1 } ` ;
209237 }
210238
211- // Update the form
212239 form . setValues ( {
213240 ...form . values ,
214241 metadata : {
@@ -218,7 +245,6 @@ export const ManageEventPage: React.FC = () => {
218245 } ) ;
219246 } ;
220247
221- // Function to update a metadata value
222248 const updateMetadataValue = ( key : string , value : string ) => {
223249 form . setValues ( {
224250 ...form . values ,
@@ -245,7 +271,6 @@ export const ManageEventPage: React.FC = () => {
245271 } ) ;
246272 } ;
247273
248- // Function to remove a metadata field
249274 const removeMetadataField = ( key : string ) => {
250275 const currentMetadata = { ...form . values . metadata } ;
251276 delete currentMetadata [ key ] ;
@@ -258,11 +283,9 @@ export const ManageEventPage: React.FC = () => {
258283
259284 const [ metadataKeys , setMetadataKeys ] = useState < Record < string , string > > ( { } ) ;
260285
261- // Initialize metadata keys with unique IDs when form loads or changes
262286 useEffect ( ( ) => {
263287 const newMetadataKeys : Record < string , string > = { } ;
264288
265- // For existing metadata, create stable IDs
266289 Object . keys ( form . values . metadata || { } ) . forEach ( ( key ) => {
267290 if ( ! metadataKeys [ key ] ) {
268291 newMetadataKeys [ key ] =
@@ -283,6 +306,19 @@ export const ManageEventPage: React.FC = () => {
283306 < Title mb = "sm" order = { 2 } >
284307 { isEditing ? `Edit` : `Create` } Event
285308 </ Title >
309+ { Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone !==
310+ "America/Chicago" && (
311+ < Alert
312+ variant = "light"
313+ color = "red"
314+ title = "Timezone Alert"
315+ icon = { < IconInfoCircle /> }
316+ >
317+ All dates and times are shown in the America/Chicago timezone.
318+ Please ensure you enter them in the America/Chicago timezone.
319+ </ Alert >
320+ ) }
321+
286322 < form onSubmit = { form . onSubmit ( handleSubmit ) } >
287323 < TextInput
288324 label = "Event Title"
@@ -299,14 +335,14 @@ export const ManageEventPage: React.FC = () => {
299335 < DateTimePicker
300336 label = "Start Date"
301337 withAsterisk
302- valueFormat = "MM-DD-YYYY h:mm A [Urbana Time] "
338+ valueFormat = "MM-DD-YYYY h:mm A"
303339 placeholder = "Pick start date"
304340 { ...form . getInputProps ( "start" ) }
305341 />
306342 < DateTimePicker
307343 label = "End Date"
308344 withAsterisk
309- valueFormat = "MM-DD-YYYY h:mm A [Urbana Time] "
345+ valueFormat = "MM-DD-YYYY h:mm A"
310346 placeholder = "Pick end date (optional)"
311347 { ...form . getInputProps ( "end" ) }
312348 />
@@ -344,16 +380,29 @@ export const ManageEventPage: React.FC = () => {
344380 { ...form . getInputProps ( "repeats" ) }
345381 />
346382 { form . values . repeats && (
347- < DateTimePicker
348- valueFormat = "MM-DD-YYYY h:mm A [Urbana Time]"
349- label = "Repeat Ends"
350- placeholder = "Pick repeat end date"
351- { ...form . getInputProps ( "repeatEnds" ) }
352- />
383+ < >
384+ < DateTimePicker
385+ valueFormat = "MM-DD-YYYY h:mm A"
386+ label = "Repeat Ends"
387+ placeholder = "Pick repeat end date"
388+ { ...form . getInputProps ( "repeatEnds" ) }
389+ />
390+ < DatePickerInput
391+ label = "Repeat Excludes"
392+ description = "Dates selected here will be skipped in the recurring schedule."
393+ valueFormat = "MMM D, YYYY"
394+ type = "multiple"
395+ placeholder = "Click to select dates to exclude"
396+ clearable
397+ valueFormatter = { valueFormatter }
398+ { ...form . getInputProps ( "repeatExcludes" ) }
399+ />
400+ </ >
353401 ) }
354402 < TextInput
355403 label = "Paid Event ID"
356- placeholder = "Enter Ticketing ID or Merch ID prefixed with merch:"
404+ description = "For integration with ACM ticketing only."
405+ placeholder = "Enter Ticketing or Merch ID"
357406 { ...form . getInputProps ( "paidEventId" ) }
358407 />
359408
@@ -406,14 +455,13 @@ export const ManageEventPage: React.FC = () => {
406455 }
407456 error = { valueError }
408457 />
409- { /* Empty space to maintain consistent height */ }
410458 { valueError && < div style = { { height : "0.75rem" } } /> }
411459 </ Box >
412460 < ActionIcon
413461 color = "red"
414462 variant = "light"
415463 onClick = { ( ) => removeMetadataField ( key ) }
416- mt = { 30 } // align with inputs when label is present
464+ mt = { 30 }
417465 >
418466 < IconTrash size = { 16 } />
419467 </ ActionIcon >
0 commit comments