Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
],
"packageManager": "[email protected]",
"scripts": {
"postinstall": "npm run setup",
"setup": "git config blame.ignoreRevsFile .git-blame-ignore-revs",
"build": "yarn workspaces run build && yarn lockfile-manage",
"dev": "concurrently --names 'api,ui' 'yarn workspace infra-core-api run dev' 'yarn workspace infra-core-ui run dev'",
"lockfile-manage": "synp --with-workspace --source-file yarn.lock && cp package-lock.json dist/lambda/ && cp package-lock.json dist/sqsConsumer/ && cp src/api/package.lambda.json dist/lambda/package.json && cp src/api/package.lambda.json dist/sqsConsumer/package.json && rm package-lock.json",
Expand Down Expand Up @@ -83,4 +85,4 @@
"resolutions": {
"pdfjs-dist": "^4.8.69"
}
}
}
2 changes: 1 addition & 1 deletion src/api/routes/roomRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
const body = {
...request.body,
eventStart: request.body.eventStart.toISOString(),
eventEnd: request.body.eventStart.toISOString(),
eventEnd: request.body.eventEnd.toISOString(),
...(request.body.recurrenceEndDate
? { recurrenceEndDate: request.body.recurrenceEndDate.toISOString() }
: {}),
Expand Down
9 changes: 9 additions & 0 deletions src/common/types/roomRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ export const roomRequestSchema = roomRequestDataSchema
path: ["eventEnd"],
},
)
.refine(
(data) => {
return (data.eventEnd.getTime() - data.eventStart.getTime()) >= (30 * 60 * 1000);
},
{
message: "Event must be at least 30 minutes long",
path: ["eventEnd"],
},
)
.refine(
(data) => {
// If recurrence is enabled, recurrence pattern must be provided
Expand Down
2 changes: 1 addition & 1 deletion src/ui/pages/events/ManageEvent.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const ManageEventPage: React.FC = () => {
const getEvent = async () => {
try {
const response = await api.get(
`/api/v1/events/${eventId}?ts=${Date.now()}`,
`/api/v1/events/${eventId}?ts=${Date.now()}&includeMetadata=true`,
);
const eventData = response.data;

Expand Down
207 changes: 124 additions & 83 deletions src/ui/pages/events/ViewEvents.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import {
Title,
Badge,
Anchor,
Divider,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
import { IconPlus, IconTrash } from "@tabler/icons-react";
import dayjs from "dayjs";
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { z } from "zod";

Expand Down Expand Up @@ -56,96 +57,77 @@ export const ViewEventsPage: React.FC = () => {
const [eventList, setEventList] = useState<EventsGetResponse>([]);
const api = useApi("core");
const [opened, { open, close }] = useDisclosure(false);
const [showPrevious, { toggle: togglePrevious }] = useDisclosure(false); // Changed default to false
const [showPrevious, { toggle: togglePrevious }] = useDisclosure(false);
const [deleteCandidate, setDeleteCandidate] =
useState<EventGetResponse | null>(null);
const navigate = useNavigate();

const renderTableRow = (event: EventGetResponse) => {
const shouldShow = event.upcoming || (!event.upcoming && showPrevious);
// Use useMemo to sort and filter events only when dependencies change
const sortedUpcomingEvents = useMemo(() => {
return eventList
.filter((event: EventGetResponse) => event.upcoming)
.sort(
(a: EventGetResponse, b: EventGetResponse) =>
new Date(a.start).getTime() - new Date(b.start).getTime(),
);
}, [eventList]);

return (
<Transition
mounted={shouldShow}
transition="fade"
duration={400}
timingFunction="ease"
key={`${event.id}-tr-transition`}
>
{(styles) => (
<tr
style={{ ...styles, display: shouldShow ? "table-row" : "none" }}
key={`${event.id}-tr`}
>
<Table.Td>
{event.title}{" "}
{event.featured ? <Badge color="green">Featured</Badge> : null}
</Table.Td>
<Table.Td>
{dayjs(event.start).format("MMM D YYYY hh:mm A")}
</Table.Td>
<Table.Td>
{event.end
? dayjs(event.end).format("MMM D YYYY hh:mm A")
: "N/A"}
</Table.Td>
<Table.Td>
{event.locationLink ? (
<Anchor target="_blank" size="sm" href={event.locationLink}>
{event.location}
</Anchor>
) : (
event.location
)}
</Table.Td>
<Table.Td>{event.host}</Table.Td>
<Table.Td>
{capitalizeFirstLetter(event.repeats || "Never")}
</Table.Td>
<Table.Td>
<ButtonGroup>
<Button component="a" href={`/events/edit/${event.id}`}>
Edit
</Button>
<Button
color="red"
onClick={() => {
setDeleteCandidate(event);
open();
}}
>
Delete
</Button>
</ButtonGroup>
</Table.Td>
</tr>
)}
</Transition>
);
};
// Use useMemo to sort and filter previous events only when dependencies change
const sortedPreviousEvents = useMemo(() => {
return eventList
.filter((event: EventGetResponse) => !event.upcoming)
.sort((a: EventGetResponse, b: EventGetResponse) => {
// For repeating events, compare by repeatEnds date first (if available)
if (a.repeatEnds && b.repeatEnds) {
return (
new Date(b.repeatEnds).getTime() - new Date(a.repeatEnds).getTime()
);
} else if (a.repeatEnds) {
return -1; // a has repeatEnds, b doesn't, so a comes first
} else if (b.repeatEnds) {
return 1; // b has repeatEnds, a doesn't, so b comes first
}
// Otherwise sort by start date in reverse order (newest first)
return new Date(b.start).getTime() - new Date(a.start).getTime();
});
}, [eventList]);

useEffect(() => {
const getEvents = async () => {
// setting ts lets us tell cloudfront I want fresh data
const response = await api.get(`/api/v1/events?ts=${Date.now()}`);
const upcomingEvents = await api.get(
`/api/v1/events?upcomingOnly=true&ts=${Date.now()}`,
);
const upcomingEventsSet = new Set(
upcomingEvents.data.map((x: EventGetResponse) => x.id),
);
const events = response.data;
events.sort((a: EventGetResponse, b: EventGetResponse) => {
return a.start.localeCompare(b.start);
});
const enrichedResponse = response.data.map((item: EventGetResponse) => {
if (upcomingEventsSet.has(item.id)) {
return { ...item, upcoming: true };
}
return { ...item, upcoming: false };
});
setEventList(enrichedResponse);
try {
// Setting ts lets us tell cloudfront I want fresh data
const response = await api.get(`/api/v1/events?ts=${Date.now()}`);
const upcomingEvents = await api.get(
`/api/v1/events?upcomingOnly=true&ts=${Date.now()}`,
);

const upcomingEventsSet = new Set(
upcomingEvents.data.map((x: EventGetResponse) => x.id),
);

const events = response.data;
events.sort((a: EventGetResponse, b: EventGetResponse) => {
return a.start.localeCompare(b.start);
});

const enrichedResponse = response.data.map((item: EventGetResponse) => {
if (upcomingEventsSet.has(item.id)) {
return { ...item, upcoming: true };
}
return { ...item, upcoming: false };
});

setEventList(enrichedResponse);
} catch (error) {
console.error("Error fetching events:", error);
notifications.show({
title: "Error fetching events",
message: `${error}`,
color: "red",
});
}
};

getEvents();
}, []);

Expand All @@ -170,6 +152,49 @@ export const ViewEventsPage: React.FC = () => {
}
};

const renderEvent = (event: EventGetResponse) => {
return (
<tr key={`${event.id}-tr`}>
<Table.Td>
{event.title}{" "}
{event.featured ? <Badge color="green">Featured</Badge> : null}
</Table.Td>
<Table.Td>{dayjs(event.start).format("MMM D YYYY hh:mm A")}</Table.Td>
<Table.Td>
{event.end ? dayjs(event.end).format("MMM D YYYY hh:mm A") : "N/A"}
</Table.Td>
<Table.Td>
{event.locationLink ? (
<Anchor target="_blank" size="sm" href={event.locationLink}>
{event.location}
</Anchor>
) : (
event.location
)}
</Table.Td>
<Table.Td>{event.host}</Table.Td>
<Table.Td>{capitalizeFirstLetter(event.repeats || "Never")}</Table.Td>
<Table.Td>
<ButtonGroup>
<Button component="a" href={`/events/edit/${event.id}`}>
Edit
</Button>
<Button
color="red"
onClick={(e) => {
e.stopPropagation();
setDeleteCandidate(event);
open();
}}
>
Delete
</Button>
</ButtonGroup>
</Table.Td>
</tr>
);
};

if (eventList.length === 0) {
return <FullScreenLoader />;
}
Expand All @@ -181,6 +206,7 @@ export const ViewEventsPage: React.FC = () => {
<Title order={1} mb="md">
Event Management
</Title>

{deleteCandidate && (
<Modal
opened={opened}
Expand All @@ -207,6 +233,7 @@ export const ViewEventsPage: React.FC = () => {
</Group>
</Modal>
)}

<div
style={{ display: "flex", columnGap: "1vw", verticalAlign: "middle" }}
>
Expand All @@ -222,6 +249,7 @@ export const ViewEventsPage: React.FC = () => {
{showPrevious ? "Hide Previous Events" : "Show Previous Events"}
</Button>
</div>

<Table
style={{ tableLayout: "fixed", width: "100%" }}
data-testid="events-table"
Expand All @@ -237,8 +265,21 @@ export const ViewEventsPage: React.FC = () => {
<Table.Th>Actions</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{eventList.map(renderTableRow)}</Table.Tbody>
<Table.Tbody>{sortedUpcomingEvents.map(renderEvent)}</Table.Tbody>
</Table>
{showPrevious && (
<>
<Divider labelPosition="center" label="Previous Events" />
<Table
style={{ tableLayout: "fixed", width: "100%" }}
data-testid="events-previous-table"
>
<Table.Tbody>
{showPrevious && sortedPreviousEvents.map(renderEvent)}
</Table.Tbody>
</Table>
</>
)}
</AuthGuard>
);
};
Loading
Loading