Skip to content

Commit 0cb2f01

Browse files
committed
add repeat skip UI
1 parent c944b7b commit 0cb2f01

File tree

1 file changed

+75
-27
lines changed

1 file changed

+75
-27
lines changed

src/ui/pages/events/ManageEvent.page.tsx

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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";
1517
import { useForm, zodResolver } from "@mantine/form";
1618
import { notifications } from "@mantine/notifications";
1719
import dayjs from "dayjs";
@@ -23,7 +25,7 @@ import { useApi } from "@ui/util/api";
2325
import { AllOrganizationList as orgList } from "@acm-uiuc/js-shared";
2426
import { AppRoles } from "@common/roles";
2527
import { EVENT_CACHED_DURATION } from "@common/config";
26-
import { IconPlus, IconTrash } from "@tabler/icons-react";
28+
import { IconInfoCircle, IconPlus, IconTrash } from "@tabler/icons-react";
2729
import {
2830
MAX_METADATA_KEYS,
2931
MAX_KEY_LENGTH,
@@ -34,6 +36,23 @@ import {
3436
export 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

3857
const 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

5775
const 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

Comments
 (0)