Skip to content

Commit d678eb6

Browse files
committed
Adding Siglead Management Option in the Side Menu & Adding a draft Page
1 parent 2462f45 commit d678eb6

File tree

4 files changed

+221
-7
lines changed

4 files changed

+221
-7
lines changed

src/common/roles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const runEnvironments = ["dev", "prod"] as const;
33
export type RunEnvironment = (typeof runEnvironments)[number];
44
export enum AppRoles {
55
EVENTS_MANAGER = "manage:events",
6+
SIGLEAD_MANAGER = "manage:siglead",
67
TICKETS_SCANNER = "scan:tickets",
78
TICKETS_MANAGER = "manage:tickets",
89
IAM_ADMIN = "admin:iam",

src/ui/Router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ViewTicketsPage } from './pages/tickets/ViewTickets.page';
1919
import { ManageIamPage } from './pages/iam/ManageIam.page';
2020
import { ManageProfilePage } from './pages/profile/ManageProfile.page';
2121
import { ManageStripeLinksPage } from './pages/stripe/ViewLinks.page';
22+
import { ManageSigLeadsPage } from './pages/siglead/ManageSigLeads.page';
2223

2324
const ProfileRediect: React.FC = () => {
2425
const location = useLocation();
@@ -162,6 +163,10 @@ const authenticatedRouter = createBrowserRouter([
162163
path: '/stripe',
163164
element: <ManageStripeLinksPage />,
164165
},
166+
{
167+
path: '/siglead-management',
168+
element: <ManageSigLeadsPage />,
169+
},
165170
// Catch-all route for authenticated users shows 404 page
166171
{
167172
path: '*',

src/ui/components/AppShell/index.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
IconPizza,
1818
IconTicket,
1919
IconLock,
20+
IconUsers,
2021
} from '@tabler/icons-react';
2122
import { ReactNode } from 'react';
2223
import { useNavigate } from 'react-router-dom';
@@ -37,13 +38,6 @@ export interface AcmAppShellProps {
3738
}
3839

3940
export const navItems = [
40-
{
41-
link: '/events/manage',
42-
name: 'Events',
43-
icon: IconCalendar,
44-
description: null,
45-
validRoles: [AppRoles.EVENTS_MANAGER],
46-
},
4741
{
4842
link: '/tickets',
4943
name: 'Ticketing/Merch',
@@ -65,6 +59,13 @@ export const navItems = [
6559
description: null,
6660
validRoles: [AppRoles.STRIPE_LINK_CREATOR],
6761
},
62+
{
63+
link: '/siglead-management',
64+
name: 'SigLead',
65+
icon: IconUsers,
66+
description: null,
67+
validRoles: [AppRoles.SIGLEAD_MANAGER],
68+
},
6869
];
6970

7071
export const extLinks = [
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { Title, Box, TextInput, Textarea, Switch, Select, Button, Loader } from '@mantine/core';
2+
import { DateTimePicker } from '@mantine/dates';
3+
import { useForm, zodResolver } from '@mantine/form';
4+
import { notifications } from '@mantine/notifications';
5+
import dayjs from 'dayjs';
6+
import React, { useEffect, useState } from 'react';
7+
import { useNavigate, useParams } from 'react-router-dom';
8+
import { z } from 'zod';
9+
import { AuthGuard } from '@ui/components/AuthGuard';
10+
import { getRunEnvironmentConfig } from '@ui/config';
11+
import { useApi } from '@ui/util/api';
12+
import { OrganizationList as orgList } from '@common/orgs';
13+
import { AppRoles } from '@common/roles';
14+
15+
export function capitalizeFirstLetter(string: string) {
16+
return string.charAt(0).toUpperCase() + string.slice(1);
17+
}
18+
19+
const repeatOptions = ['weekly', 'biweekly'] as const;
20+
21+
const baseBodySchema = z.object({
22+
title: z.string().min(1, 'Title is required'),
23+
description: z.string().min(1, 'Description is required'),
24+
start: z.date(),
25+
end: z.optional(z.date()),
26+
location: z.string().min(1, 'Location is required'),
27+
locationLink: z.optional(z.string().url('Invalid URL')),
28+
host: z.string().min(1, 'Host is required'),
29+
featured: z.boolean().default(false),
30+
paidEventId: z.string().min(1, 'Paid Event ID must be at least 1 character').optional(),
31+
});
32+
33+
const requestBodySchema = baseBodySchema
34+
.extend({
35+
repeats: z.optional(z.enum(repeatOptions)).nullable(),
36+
repeatEnds: z.date().optional(),
37+
})
38+
.refine((data) => (data.repeatEnds ? data.repeats !== undefined : true), {
39+
message: 'Repeat frequency is required when Repeat End is specified.',
40+
})
41+
.refine((data) => !data.end || data.end >= data.start, {
42+
message: 'Event end date cannot be earlier than the start date.',
43+
path: ['end'],
44+
})
45+
.refine((data) => !data.repeatEnds || data.repeatEnds >= data.start, {
46+
message: 'Repeat end date cannot be earlier than the start date.',
47+
path: ['repeatEnds'],
48+
});
49+
50+
type EventPostRequest = z.infer<typeof requestBodySchema>;
51+
52+
export const ManageSigLeadsPage: React.FC = () => {
53+
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
54+
const navigate = useNavigate();
55+
const api = useApi('core');
56+
57+
const { eventId } = useParams();
58+
59+
const isEditing = eventId !== undefined;
60+
61+
useEffect(() => {
62+
if (!isEditing) {
63+
return;
64+
}
65+
// Fetch event data and populate form
66+
const getEvent = async () => {
67+
try {
68+
const response = await api.get(`/api/v1/events/${eventId}`);
69+
const eventData = response.data;
70+
const formValues = {
71+
title: eventData.title,
72+
description: eventData.description,
73+
start: new Date(eventData.start),
74+
end: eventData.end ? new Date(eventData.end) : undefined,
75+
location: eventData.location,
76+
locationLink: eventData.locationLink,
77+
host: eventData.host,
78+
featured: eventData.featured,
79+
repeats: eventData.repeats,
80+
repeatEnds: eventData.repeatEnds ? new Date(eventData.repeatEnds) : undefined,
81+
paidEventId: eventData.paidEventId,
82+
};
83+
form.setValues(formValues);
84+
} catch (error) {
85+
console.error('Error fetching event data:', error);
86+
notifications.show({
87+
message: 'Failed to fetch event data, please try again.',
88+
});
89+
}
90+
};
91+
getEvent();
92+
}, [eventId, isEditing]);
93+
94+
const form = useForm<EventPostRequest>({
95+
validate: zodResolver(requestBodySchema),
96+
initialValues: {
97+
title: '',
98+
description: '',
99+
start: new Date(),
100+
end: new Date(new Date().valueOf() + 3.6e6), // 1 hr later
101+
location: 'ACM Room (Siebel CS 1104)',
102+
locationLink: 'https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8',
103+
host: 'ACM',
104+
featured: false,
105+
repeats: undefined,
106+
repeatEnds: undefined,
107+
paidEventId: undefined,
108+
},
109+
});
110+
111+
const checkPaidEventId = async (paidEventId: string) => {
112+
try {
113+
const merchEndpoint = getRunEnvironmentConfig().ServiceConfiguration.merch.baseEndpoint;
114+
const ticketEndpoint = getRunEnvironmentConfig().ServiceConfiguration.tickets.baseEndpoint;
115+
const paidEventHref = paidEventId.startsWith('merch:')
116+
? `${merchEndpoint}/api/v1/merch/details?itemid=${paidEventId.slice(6)}`
117+
: `${ticketEndpoint}/api/v1/event/details?eventid=${paidEventId}`;
118+
const response = await api.get(paidEventHref);
119+
return Boolean(response.status < 299 && response.status >= 200);
120+
} catch (error) {
121+
console.error('Error validating paid event ID:', error);
122+
return false;
123+
}
124+
};
125+
126+
const handleSubmit = async (values: EventPostRequest) => {
127+
try {
128+
setIsSubmitting(true);
129+
const realValues = {
130+
...values,
131+
start: dayjs(values.start).format('YYYY-MM-DD[T]HH:mm:00'),
132+
end: values.end ? dayjs(values.end).format('YYYY-MM-DD[T]HH:mm:00') : undefined,
133+
repeatEnds:
134+
values.repeatEnds && values.repeats
135+
? dayjs(values.repeatEnds).format('YYYY-MM-DD[T]HH:mm:00')
136+
: undefined,
137+
repeats: values.repeats ? values.repeats : undefined,
138+
};
139+
140+
const eventURL = isEditing ? `/api/v1/events/${eventId}` : '/api/v1/events';
141+
const response = await api.post(eventURL, realValues);
142+
notifications.show({
143+
title: isEditing ? 'Event updated!' : 'Event created!',
144+
message: isEditing ? undefined : `The event ID is "${response.data.id}".`,
145+
});
146+
navigate('/events/manage');
147+
} catch (error) {
148+
setIsSubmitting(false);
149+
console.error('Error creating/editing event:', error);
150+
notifications.show({
151+
message: 'Failed to create/edit event, please try again.',
152+
});
153+
}
154+
};
155+
156+
return (
157+
<div
158+
style={{
159+
display: 'flex',
160+
flexDirection: 'column',
161+
alignItems: 'center',
162+
justifyContent: 'center',
163+
height: '100vh',
164+
textAlign: 'center',
165+
}}
166+
>
167+
<svg
168+
xmlns="http://www.w3.org/2000/svg"
169+
width="100"
170+
height="100"
171+
viewBox="0 0 24 24"
172+
fill="none"
173+
stroke="currentColor"
174+
strokeWidth="2"
175+
strokeLinecap="round"
176+
strokeLinejoin="round"
177+
style={{
178+
animation: 'rotate 2s linear infinite',
179+
}}
180+
>
181+
<path d="M12 2v2"></path>
182+
<path d="M12 20v2"></path>
183+
<path d="m4.93 4.93 1.41 1.41"></path>
184+
<path d="m17.66 17.66 1.41 1.41"></path>
185+
<path d="M2 12h2"></path>
186+
<path d="M20 12h2"></path>
187+
<path d="m6.34 17.66-1.41 1.41"></path>
188+
<path d="m19.07 4.93-1.41 1.41"></path>
189+
<circle cx="12" cy="12" r="3"></circle>
190+
</svg>
191+
<h1 style={{ marginTop: '20px', fontSize: '24px' }}>Page Under Construction</h1>
192+
193+
<style>
194+
{`
195+
@keyframes rotate {
196+
from {
197+
transform: rotate(0deg);
198+
}
199+
to {
200+
transform: rotate(360deg);
201+
}
202+
}
203+
`}
204+
</style>
205+
</div>
206+
);
207+
};

0 commit comments

Comments
 (0)