From a8d6b0cd08a92680fd2f25d68146e4e3a6011f2a Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 22 Jun 2025 20:52:59 -0400 Subject: [PATCH 1/7] add repeating event exclusion support to api endpoints --- src/api/routes/events.ts | 16 +++++++++++----- src/api/routes/ics.ts | 20 +++++++++++++++++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/api/routes/events.ts b/src/api/routes/events.ts index 04af650..5f3efb7 100644 --- a/src/api/routes/events.ts +++ b/src/api/routes/events.ts @@ -57,6 +57,7 @@ const createProjectionParams = (includeMetadata: boolean = false) => { id: "#id", repeats: "#repeats", repeatEnds: "#repeatEnds", + excludeDates: "#excludeDates", ...(includeMetadata ? { metadata: "#metadata" } : {}), }; @@ -123,14 +124,19 @@ const baseSchema = z.object({ const requestSchema = baseSchema.extend({ repeats: z.optional(z.enum(repeatOptions)), repeatEnds: z.string().optional(), + repeatExcludes: z.array(z.string().date()).optional().openapi({ + description: + "Dates to exclude from recurrence rules (in the America/Chicago timezone).", + }), }); -const postRequestSchema = requestSchema.refine( - (data) => (data.repeatEnds ? data.repeats !== undefined : true), - { +const postRequestSchema = requestSchema + .refine((data) => (data.repeatEnds ? data.repeats !== undefined : true), { message: "repeats is required when repeatEnds is defined", - }, -); + }) + .refine((data) => (data.repeatExcludes ? data.repeats !== undefined : true), { + message: "repeats is required when repeatExcludes is defined", + }); export type EventPostRequest = z.infer; const getEventSchema = requestSchema.extend({ diff --git a/src/api/routes/ics.ts b/src/api/routes/ics.ts index 3f7b36d..5ecc95f 100644 --- a/src/api/routes/ics.ts +++ b/src/api/routes/ics.ts @@ -155,15 +155,29 @@ const icalPlugin: FastifyPluginAsync = async (fastify, _options) => { }); if (rawEvent.repeats) { + const startTime = moment.tz(rawEvent.start, "America/Chicago"); + const hours = startTime.hours(); + const minutes = startTime.minutes(); + const seconds = startTime.seconds(); + const milliseconds = startTime.milliseconds(); + const exclusions = ((rawEvent.repeatExcludes as string[]) || []).map( + (x) => + moment + .tz(x, "America/Chicago") + .set({ hours, minutes, seconds, milliseconds }), + ); + if (rawEvent.repeatEnds) { event = event.repeating({ ...repeatingIcalMap[rawEvent.repeats as EventRepeatOptions], until: moment.tz(rawEvent.repeatEnds, "America/Chicago"), + exclude: exclusions, }); } else { - event.repeating( - repeatingIcalMap[rawEvent.repeats as EventRepeatOptions], - ); + event.repeating({ + ...repeatingIcalMap[rawEvent.repeats as EventRepeatOptions], + exclude: exclusions, + }); } } if (rawEvent.location) { From 48d2fe3ca539aa0790f5c79d0926d2ea93443122 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 22 Jun 2025 21:01:56 -0400 Subject: [PATCH 2/7] fix projection --- src/api/routes/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/routes/events.ts b/src/api/routes/events.ts index 5f3efb7..ee9e056 100644 --- a/src/api/routes/events.ts +++ b/src/api/routes/events.ts @@ -57,7 +57,7 @@ const createProjectionParams = (includeMetadata: boolean = false) => { id: "#id", repeats: "#repeats", repeatEnds: "#repeatEnds", - excludeDates: "#excludeDates", + repeatExcludes: "#repeatExcludes", ...(includeMetadata ? { metadata: "#metadata" } : {}), }; From ae92a6a2620639727e7b15817dcd332fef5ea409 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 22 Jun 2025 21:12:20 -0400 Subject: [PATCH 3/7] add unit tests for excluded dates --- .editorconfig | 3 + package.json | 3 +- src/api/routes/ics.ts | 4 +- .../data/acmWideCalendarRepeatExclude.ics | 103 ++++++++++++++++++ tests/unit/ical.test.ts | 26 ++++- tests/unit/mockEventData.testdata.ts | 53 +++++++++ 6 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 tests/unit/data/acmWideCalendarRepeatExclude.ics diff --git a/.editorconfig b/.editorconfig index 6f2f4eb..bb49bd2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,9 @@ indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true +[*.ics] +insert_final_newline = false + [*.md] max_line_length = off trim_trailing_whitespace = false diff --git a/package.json b/package.json index c03e6d6..1ae813d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "lint": "yarn workspaces run lint", "prepare": "node .husky/install.mjs || true", "typecheck": "yarn workspaces run typecheck", - "test:unit": "cross-env RunEnvironment='dev' concurrently --names 'api,ui' 'vitest run --coverage --config tests/unit/vitest.config.ts tests/unit' 'yarn workspace infra-core-ui run test:unit'", + "test:unit": "cross-env RunEnvironment='dev' concurrently --names 'api,ui' 'yarn run test:unit-api' 'yarn workspace infra-core-ui run test:unit'", + "test:unit-api": "cross-env RunEnvironment='dev' vitest run --coverage --config tests/unit/vitest.config.ts tests/unit", "test:unit-ui": "yarn test:unit --ui", "test:unit-watch": "vitest tests/unit", "test:live": "vitest tests/live", diff --git a/src/api/routes/ics.ts b/src/api/routes/ics.ts index 5ecc95f..09e3cb1 100644 --- a/src/api/routes/ics.ts +++ b/src/api/routes/ics.ts @@ -171,12 +171,12 @@ const icalPlugin: FastifyPluginAsync = async (fastify, _options) => { event = event.repeating({ ...repeatingIcalMap[rawEvent.repeats as EventRepeatOptions], until: moment.tz(rawEvent.repeatEnds, "America/Chicago"), - exclude: exclusions, + ...(exclusions.length > 0 && { exclude: exclusions }), }); } else { event.repeating({ ...repeatingIcalMap[rawEvent.repeats as EventRepeatOptions], - exclude: exclusions, + ...(exclusions.length > 0 && { exclude: exclusions }), }); } } diff --git a/tests/unit/data/acmWideCalendarRepeatExclude.ics b/tests/unit/data/acmWideCalendarRepeatExclude.ics new file mode 100644 index 0000000..130436f --- /dev/null +++ b/tests/unit/data/acmWideCalendarRepeatExclude.ics @@ -0,0 +1,103 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//sebbo.net//ical-generator//EN +METHOD:PUBLISH +NAME:ACM@UIUC - All Events +X-WR-CALNAME:ACM@UIUC - All Events +BEGIN:VTIMEZONE +TZID:America/Chicago +TZURL:http://tzurl.org/zoneinfo-outlook/America/Chicago +X-LIC-LOCATION:America/Chicago +BEGIN:DAYLIGHT +TZOFFSETFROM:-0600 +TZOFFSETTO:-0500 +TZNAME:CDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0500 +TZOFFSETTO:-0600 +TZNAME:CST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +END:STANDARD +END:VTIMEZONE +TIMEZONE-ID:America/Chicago +X-WR-TIMEZONE:America/Chicago +BEGIN:VEVENT +UID:3138bead-b2c5-4bfe-bce4-4b478658cb78 +SEQUENCE:0 +DTSTAMP:20240822T155148 +DTSTART;TZID=America/Chicago:20240825T120000 +DTEND;TZID=America/Chicago:20240825T160000 +SUMMARY:Quad Day +LOCATION:Main Quad +DESCRIPTION:Host: ACM\nGoogle Maps Link: https://maps.app.goo.gl/2ZRYibtE7 + Yem5TrP6\n\nJoin us on Quad Day to learn more about ACM and CS at Illinois + ! +ORGANIZER;CN="ACM@UIUC":mailto:ACM@UIUC +END:VEVENT +BEGIN:VEVENT +UID:5bc69f3b-e958-4c80-b041-ddeae0385db8 +SEQUENCE:0 +DTSTAMP:20240822T155148 +DTSTART;TZID=America/Chicago:20240725T180000 +DTEND;TZID=America/Chicago:20240725T190000 +RRULE:FREQ=WEEKLY;UNTIL=20240905T190000 +SUMMARY:Infra Meeting +LOCATION:ACM Middle Room +DESCRIPTION:Host: Infrastructure Committee\n\nTest event. +ORGANIZER;CN="ACM@UIUC":mailto:ACM@UIUC +END:VEVENT +BEGIN:VEVENT +UID:4d38608d-90bf-4a58-8701-3f1b659a53db +SEQUENCE:0 +DTSTAMP:20240822T155148 +DTSTART;TZID=America/Chicago:20240925T180000 +DTEND;TZID=America/Chicago:20240925T190000 +SUMMARY:Testing Paid and Featured Event +LOCATION:ACM Middle Room +DESCRIPTION:Host: Social Committee\n\nTest paid featured event. +ORGANIZER;CN="ACM@UIUC":mailto:ACM@UIUC +END:VEVENT +BEGIN:VEVENT +UID:accd7fe0-50ac-427b-8041-a2b3ddcd328e +SEQUENCE:0 +DTSTAMP:20240822T155148 +DTSTART;TZID=America/Chicago:20240725T180000 +DTEND;TZID=America/Chicago:20240725T190000 +SUMMARY:Event in the past. +LOCATION:ACM Middle Room +DESCRIPTION:Host: Infrastructure Committee\n\nTest event in the past. +ORGANIZER;CN="ACM@UIUC":mailto:ACM@UIUC +END:VEVENT +BEGIN:VEVENT +UID:78be8f2b-3d1d-4481-90b6-85bfd84d38b4 +SEQUENCE:0 +DTSTAMP:20240822T155148 +DTSTART;TZID=America/Chicago:20240830T170000 +DTEND;TZID=America/Chicago:20240830T170000 +RRULE:FREQ=WEEKLY +SUMMARY:Weekly Happy Hour +LOCATION:Legends +DESCRIPTION:Host: ACM\nGoogle Maps Link: https://goo.gl/maps/CXESXd3otbGZN + qFP7\n\nMeet and chat with your peers and fellow ACM members\, with food o + n us! +ORGANIZER;CN="ACM@UIUC":mailto:ACM@UIUC +END:VEVENT +BEGIN:VEVENT +UID:6ac35cf3-3a4d-4d45-957a-6fee8dea50a7 +SEQUENCE:0 +DTSTAMP:20240822T155148 +DTSTART;TZID=America/Chicago:20241220T012100 +DTEND;TZID=America/Chicago:20241220T022100 +RRULE:FREQ=WEEKLY;UNTIL=20251221T000000 +EXDATE;TZID=America/Chicago:20250627T012100 +SUMMARY:Test Title +LOCATION:ACM Room (Siebel CS 1104) +DESCRIPTION:Host: ACM\nGoogle Maps Link: https://maps.app.goo.gl/dwbBBBkfj + kgj8gvA8\n\nTest Description with now etag 1? +ORGANIZER;CN="ACM@UIUC":mailto:ACM@UIUC +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/tests/unit/ical.test.ts b/tests/unit/ical.test.ts index f26c75e..0622e41 100644 --- a/tests/unit/ical.test.ts +++ b/tests/unit/ical.test.ts @@ -2,7 +2,10 @@ import { afterAll, expect, test, beforeEach, vi } from "vitest"; import { ScanCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { mockClient } from "aws-sdk-client-mock"; import init from "../../src/api/index.js"; -import { dynamoTableData } from "./mockEventData.testdata.js"; +import { + dynamoEventWithRepeatExclusion, + dynamoTableData, +} from "./mockEventData.testdata.js"; import { secretObject } from "./secret.testdata.js"; import { readFile } from "fs/promises"; @@ -30,6 +33,27 @@ test("Test getting ACM-wide iCal calendar", async () => { ); }); +test("Test getting ACM-wide iCal calendar with specific repeat exclusion", async () => { + const date = new Date(2024, 7, 22, 15, 51, 48); // August 22, 2024, at 15:51:48 (3:51:48 PM) + vi.setSystemTime(date); + ddbMock.on(ScanCommand).resolves({ + Items: [...dynamoTableData, dynamoEventWithRepeatExclusion] as any, + }); + const response = await app.inject({ + method: "GET", + url: "/api/v1/ical", + }); + expect(response.statusCode).toBe(200); + expect(response.headers["content-disposition"]).toEqual( + 'attachment; filename="calendar.ics"', + ); + expect(response.body).toEqual( + ( + await readFile("./tests/unit/data/acmWideCalendarRepeatExclude.ics") + ).toString(), + ); +}); + test("Test getting non-existent iCal calendar fails", async () => { const date = new Date(2024, 7, 22, 15, 51, 48); // August 22, 2024, at 15:51:48 (3:51:48 PM) vi.setSystemTime(date); diff --git a/tests/unit/mockEventData.testdata.ts b/tests/unit/mockEventData.testdata.ts index 380e6d4..4eba82e 100644 --- a/tests/unit/mockEventData.testdata.ts +++ b/tests/unit/mockEventData.testdata.ts @@ -187,10 +187,63 @@ const dynamoTableDataUnmarshalledUpcomingOnly = dynamoTableData }) .filter((x: any) => x.title !== "Event in the past."); +const dynamoEventWithRepeatExclusion = { + id: { + S: "6ac35cf3-3a4d-4d45-957a-6fee8dea50a7", + }, + createdAt: { + S: "2024-12-20T08:08:40.329Z", + }, + description: { + S: "Test Description with now etag 1?", + }, + end: { + S: "2024-12-20T02:21:00", + }, + featured: { + BOOL: false, + }, + host: { + S: "ACM", + }, + location: { + S: "ACM Room (Siebel CS 1104)", + }, + locationLink: { + S: "https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8", + }, + paidEventId: { + S: "asasasasasas", + }, + repeatEnds: { + S: "2025-12-21T00:00:00", + }, + repeatExcludes: { + L: [ + { + S: "2025-06-27", + }, + ], + }, + repeats: { + S: "weekly", + }, + start: { + S: "2024-12-20T01:21:00", + }, + title: { + S: "Test Title", + }, + updatedAt: { + S: "2025-03-21T03:34:50.228Z", + }, +}; + export { dynamoTableData, dynamoTableDataUnmarshalled, dynamoTableDataUnmarshalledUpcomingOnly, infraEventsOnly, infraEventsOnlyUnmarshalled, + dynamoEventWithRepeatExclusion, }; From 598e35bdd3a67abbcf1d11ed4b35a36caad7e328 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 22 Jun 2025 21:23:15 -0400 Subject: [PATCH 4/7] schema bounds, exclude templates from test coverage --- src/api/routes/events.ts | 13 +++++++++---- tests/unit/vitest.config.ts | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/api/routes/events.ts b/src/api/routes/events.ts index ee9e056..aa936fc 100644 --- a/src/api/routes/events.ts +++ b/src/api/routes/events.ts @@ -124,10 +124,15 @@ const baseSchema = z.object({ const requestSchema = baseSchema.extend({ repeats: z.optional(z.enum(repeatOptions)), repeatEnds: z.string().optional(), - repeatExcludes: z.array(z.string().date()).optional().openapi({ - description: - "Dates to exclude from recurrence rules (in the America/Chicago timezone).", - }), + repeatExcludes: z + .array(z.string().date()) + .min(1) + .max(100) + .optional() + .openapi({ + description: + "Dates to exclude from recurrence rules (in the America/Chicago timezone).", + }), }); const postRequestSchema = requestSchema diff --git a/tests/unit/vitest.config.ts b/tests/unit/vitest.config.ts index 96f29e5..e67573c 100644 --- a/tests/unit/vitest.config.ts +++ b/tests/unit/vitest.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ coverage: { provider: "istanbul", include: ["src/api/**/*.ts", "src/common/**/*.ts"], - exclude: ["src/api/lambda.ts"], + exclude: ["src/api/lambda.ts", "src/api/sqs/handlers/templates/*.ts"], thresholds: { statements: 54, functions: 65, From c944b7b1262c3cdc4de53132892ef4a1acd1bdcf Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 22 Jun 2025 22:16:55 -0400 Subject: [PATCH 5/7] Audit log to console when running in dev and disabled via env var --- src/api/functions/auditLog.ts | 9 ++++++++- src/api/package.json | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/api/functions/auditLog.ts b/src/api/functions/auditLog.ts index dfd3763..c8a4f9b 100644 --- a/src/api/functions/auditLog.ts +++ b/src/api/functions/auditLog.ts @@ -31,6 +31,10 @@ export async function createAuditLogEntry({ dynamoClient, entry, }: AuditLogParams) { + if (process.env.DISABLE_AUDIT_LOG && process.env.RunEnvironment === "dev") { + console.log(`Audit log entry: ${JSON.stringify(entry)}`); + return; + } const safeDynamoClient = dynamoClient || new DynamoDBClient({ @@ -52,8 +56,11 @@ export function buildAuditLogTransactPut({ }: { entry: AuditLogEntry; }): TransactWriteItem { + if (process.env.DISABLE_AUDIT_LOG && process.env.RunEnvironment === "dev") { + console.log(`Audit log entry: ${JSON.stringify(entry)}`); + return {}; + } const item = buildMarshalledAuditLogItem(entry); - return { Put: { TableName: genericConfig.AuditLogTable, diff --git a/src/api/package.json b/src/api/package.json index 2b77c99..7f19dbe 100644 --- a/src/api/package.json +++ b/src/api/package.json @@ -8,7 +8,7 @@ "type": "module", "scripts": { "build": "tsc && node build.js", - "dev": "cross-env LOG_LEVEL=debug concurrently --names 'esbuild,server' 'node esbuild.config.js --watch' 'cd ../../dist_devel && nodemon index.js'", + "dev": "cross-env DISABLE_AUDIT_LOG=true cross-env LOG_LEVEL=debug concurrently --names 'esbuild,server' 'node esbuild.config.js --watch' 'cd ../../dist_devel && nodemon index.js'", "typecheck": "tsc --noEmit", "lint": "eslint . --ext .ts --cache", "prettier": "prettier --check *.ts **/*.ts", From 0cb2f0194c470dae3439fc3c98db82288f19e8cb Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 22 Jun 2025 22:17:07 -0400 Subject: [PATCH 6/7] add repeat skip UI --- src/ui/pages/events/ManageEvent.page.tsx | 102 +++++++++++++++++------ 1 file changed, 75 insertions(+), 27 deletions(-) diff --git a/src/ui/pages/events/ManageEvent.page.tsx b/src/ui/pages/events/ManageEvent.page.tsx index c44f25b..0ec70c4 100644 --- a/src/ui/pages/events/ManageEvent.page.tsx +++ b/src/ui/pages/events/ManageEvent.page.tsx @@ -10,8 +10,10 @@ import { Group, ActionIcon, Text, + Alert, } from "@mantine/core"; -import { DateTimePicker } from "@mantine/dates"; +import moment from "moment-timezone"; +import { DateFormatter, DatePickerInput, DateTimePicker } from "@mantine/dates"; import { useForm, zodResolver } from "@mantine/form"; import { notifications } from "@mantine/notifications"; import dayjs from "dayjs"; @@ -23,7 +25,7 @@ import { useApi } from "@ui/util/api"; import { AllOrganizationList as orgList } from "@acm-uiuc/js-shared"; import { AppRoles } from "@common/roles"; import { EVENT_CACHED_DURATION } from "@common/config"; -import { IconPlus, IconTrash } from "@tabler/icons-react"; +import { IconInfoCircle, IconPlus, IconTrash } from "@tabler/icons-react"; import { MAX_METADATA_KEYS, MAX_KEY_LENGTH, @@ -34,6 +36,23 @@ import { export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); } +const valueFormatter: DateFormatter = ({ type, date, locale, format }) => { + if (type === "multiple" && Array.isArray(date)) { + if (date.length === 1) { + return dayjs(date[0]).locale(locale).format(format); + } + + if (date.length > 1) { + return date + .map((d) => dayjs(d).locale(locale).format(format)) + .join(" | "); + } + + return ""; + } + + return ""; +}; const repeatOptions = ["weekly", "biweekly"] as const; @@ -50,7 +69,6 @@ const baseBodySchema = z.object({ .string() .min(1, "Paid Event ID must be at least 1 character") .optional(), - // Add metadata field metadata: metadataSchema, }); @@ -58,6 +76,7 @@ const requestBodySchema = baseBodySchema .extend({ repeats: z.optional(z.enum(repeatOptions)).nullable(), repeatEnds: z.date().optional(), + repeatExcludes: z.array(z.date()).max(100).optional(), }) .refine((data) => (data.repeatEnds ? data.repeats !== undefined : true), { message: "Repeat frequency is required when Repeat End is specified.", @@ -86,7 +105,6 @@ export const ManageEventPage: React.FC = () => { if (!isEditing) { return; } - // Fetch event data and populate form const getEvent = async () => { try { const response = await api.get( @@ -108,6 +126,12 @@ export const ManageEventPage: React.FC = () => { ? new Date(eventData.repeatEnds) : undefined, paidEventId: eventData.paidEventId, + repeatExcludes: + eventData.repeatExcludes && eventData.repeatExcludes.length > 0 + ? eventData.repeatExcludes.map((dateString: string) => + moment.tz(dateString, "America/Chicago").toDate(), + ) + : [], metadata: eventData.metadata || {}, }; form.setValues(formValues); @@ -128,7 +152,7 @@ export const ManageEventPage: React.FC = () => { title: "", description: "", start: new Date(startDate), - end: new Date(startDate + 3.6e6), // 1 hr later + end: new Date(startDate + 3.6e6), location: "ACM Room (Siebel CS 1104)", locationLink: "https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8", host: "ACM", @@ -136,13 +160,14 @@ export const ManageEventPage: React.FC = () => { repeats: undefined, repeatEnds: undefined, paidEventId: undefined, - metadata: {}, // Initialize empty metadata object + metadata: {}, + repeatExcludes: [], }, }); useEffect(() => { if (form.values.end && form.values.end <= form.values.start) { - form.setFieldValue("end", new Date(form.values.start.getTime() + 3.6e6)); // 1 hour after the start date + form.setFieldValue("end", new Date(form.values.start.getTime() + 3.6e6)); } }, [form.values.start]); @@ -150,7 +175,10 @@ export const ManageEventPage: React.FC = () => { if (form.values.locationLink === "") { form.setFieldValue("locationLink", undefined); } - }, [form.values.locationLink]); + if (form.values.repeatExcludes?.length === 0) { + form.setFieldValue("repeatExcludes", undefined); + } + }, [form.values.locationLink, form.values.repeatExcludes]); const handleSubmit = async (values: EventPostRequest) => { try { @@ -166,6 +194,9 @@ export const ManageEventPage: React.FC = () => { values.repeatEnds && values.repeats ? dayjs(values.repeatEnds).format("YYYY-MM-DD[T]HH:mm:00") : undefined, + repeatExcludes: values.repeatExcludes + ? values.repeatExcludes.map((x) => dayjs(x).format("YYYY-MM-DD")) + : undefined, repeats: values.repeats ? values.repeats : undefined, metadata: Object.keys(values.metadata || {}).length > 0 @@ -191,7 +222,6 @@ export const ManageEventPage: React.FC = () => { } }; - // Function to add a new metadata field const addMetadataField = () => { const currentMetadata = { ...form.values.metadata }; if (Object.keys(currentMetadata).length >= MAX_METADATA_KEYS) { @@ -201,14 +231,11 @@ export const ManageEventPage: React.FC = () => { return; } - // Generate a temporary key name that doesn't exist yet let tempKey = `key${Object.keys(currentMetadata).length + 1}`; - // Make sure it's unique while (currentMetadata[tempKey] !== undefined) { tempKey = `key${parseInt(tempKey.replace("key", ""), 10) + 1}`; } - // Update the form form.setValues({ ...form.values, metadata: { @@ -218,7 +245,6 @@ export const ManageEventPage: React.FC = () => { }); }; - // Function to update a metadata value const updateMetadataValue = (key: string, value: string) => { form.setValues({ ...form.values, @@ -245,7 +271,6 @@ export const ManageEventPage: React.FC = () => { }); }; - // Function to remove a metadata field const removeMetadataField = (key: string) => { const currentMetadata = { ...form.values.metadata }; delete currentMetadata[key]; @@ -258,11 +283,9 @@ export const ManageEventPage: React.FC = () => { const [metadataKeys, setMetadataKeys] = useState>({}); - // Initialize metadata keys with unique IDs when form loads or changes useEffect(() => { const newMetadataKeys: Record = {}; - // For existing metadata, create stable IDs Object.keys(form.values.metadata || {}).forEach((key) => { if (!metadataKeys[key]) { newMetadataKeys[key] = @@ -283,6 +306,19 @@ export const ManageEventPage: React.FC = () => { {isEditing ? `Edit` : `Create`} Event + {Intl.DateTimeFormat().resolvedOptions().timeZone !== + "America/Chicago" && ( + } + > + All dates and times are shown in the America/Chicago timezone. + Please ensure you enter them in the America/Chicago timezone. + + )} +
{ @@ -344,16 +380,29 @@ export const ManageEventPage: React.FC = () => { {...form.getInputProps("repeats")} /> {form.values.repeats && ( - + <> + + + )} @@ -406,14 +455,13 @@ export const ManageEventPage: React.FC = () => { } error={valueError} /> - {/* Empty space to maintain consistent height */} {valueError &&
} removeMetadataField(key)} - mt={30} // align with inputs when label is present + mt={30} > From 677b05cb4776df517e643e4e34518e72a369d872 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Mon, 23 Jun 2025 00:28:50 -0400 Subject: [PATCH 7/7] deactivate the product as well as the link in stripe --- src/api/functions/stripe.ts | 13 +++++++++++++ src/api/routes/stripe.ts | 17 ++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/api/functions/stripe.ts b/src/api/functions/stripe.ts index 31f7f27..84ee74e 100644 --- a/src/api/functions/stripe.ts +++ b/src/api/functions/stripe.ts @@ -111,3 +111,16 @@ export const deactivateStripeLink = async ({ active: false, }); }; + +export const deactivateStripeProduct = async ({ + productId, + stripeApiKey, +}: { + productId: string; + stripeApiKey: string; +}): Promise => { + const stripe = new Stripe(stripeApiKey); + await stripe.products.update(productId, { + active: false, + }); +}; diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index 49d8f9e..0a71b88 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -13,6 +13,7 @@ import { import { createStripeLink, deactivateStripeLink, + deactivateStripeProduct, StripeLinkCreateParams, } from "api/functions/stripe.js"; import { getSecretValue } from "api/plugins/auth.js"; @@ -260,9 +261,9 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { const unmarshalledEntry = unmarshall(response.Items[0]) as { userId: string; invoiceId: string; - amount: number; - priceId: string; - productId: string; + amount?: number; + priceId?: string; + productId?: string; }; if (!unmarshalledEntry.userId || !unmarshalledEntry.invoiceId) { return reply.status(200).send({ @@ -345,6 +346,16 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { }, ], }); + if (unmarshalledEntry.productId) { + request.log.debug( + `Deactivating Stripe product ${unmarshalledEntry.productId}`, + ); + await deactivateStripeProduct({ + stripeApiKey: secretApiConfig.stripe_secret_key as string, + productId: unmarshalledEntry.productId, + }); + } + request.log.debug(`Deactivating Stripe link ${paymentLinkId}`); await deactivateStripeLink({ stripeApiKey: secretApiConfig.stripe_secret_key as string, linkId: paymentLinkId,