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
10 changes: 5 additions & 5 deletions apps/api/src/modules/events/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ describe("Event route", () => {
});
});

describe("POST /v1/events/:id", () => {
describe("PUT /v1/events/:id", () => {
beforeEach(async () => {
await db.insert(eventsTable).values({
id: "existing-event",
Expand All @@ -273,7 +273,7 @@ describe("Event route", () => {
setMockAuth({ userId: null, sessionClaims: null });

const response = await app.inject({
method: "POST",
method: "PUT",
url: "/v1/events/existing-event",
payload: { title: "Updated Title" },
});
Expand All @@ -289,7 +289,7 @@ describe("Event route", () => {
});

const response = await app.inject({
method: "POST",
method: "PUT",
url: "/v1/events/existing-event",
payload: { title: "Updated Title" },
});
Expand All @@ -304,7 +304,7 @@ describe("Event route", () => {
});

const response = await app.inject({
method: "POST",
method: "PUT",
url: "/v1/events/existing-event",
payload: {
title: "Updated Event Title",
Expand All @@ -327,7 +327,7 @@ describe("Event route", () => {
});

const response = await app.inject({
method: "POST",
method: "PUT",
url: "/v1/events/non-existing-event",
payload: { title: "Updated Title" },
});
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/modules/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const eventRoutes = async (server: FastifyInstance) => {
return reply.status(201).send(newEvent);
});

server.post("/:id", async (request, reply) => {
server.put("/:id", async (request, reply) => {
const { userId, sessionClaims } = getAuth(request);
const role = sessionClaims?.metadata?.role;

Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.6",
"tw-animate-css": "^1.3.6",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"vite-tsconfig-paths": "^6.0.2",
Expand All @@ -75,6 +74,7 @@
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^27.0.0",
"prettier": "^3.5.3",
"tw-animate-css": "^1.4.0",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vitest": "^3.0.5",
Expand Down
49 changes: 49 additions & 0 deletions apps/web/src/components/controlls/delete-event-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useNavigate } from '@tanstack/react-router'
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The directory name "controlls" appears to be misspelled. The correct spelling should be "controls" (with one 'l'). This affects the import paths in other files and should be renamed for consistency with standard English spelling.

Copilot uses AI. Check for mistakes.
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog.tsx'
import { Button } from '@/components/ui/button.tsx'
import { useDeleteEvent } from '@/lib/hooks/use-delete-event.tsx'

function DeleteEventButton({ eventId }: { eventId: string }) {
const navigate = useNavigate({ from: '/events/$eventId' })
const { deleteEvent, isDeleting } = useDeleteEvent(eventId)

return (
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">Delete</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Event</DialogTitle>
<DialogDescription>
Are you sure you want to delete this event? This action is permanent
and will remove all associated registrations and data.
</DialogDescription>
</DialogHeader>
<div className="flex justify-start gap-3 mt-4">
<Button
disabled={isDeleting}
onClick={() => {
deleteEvent(undefined, {
onSuccess: () => {
void navigate({ to: '/', replace: true })
},
})
}}
>
{isDeleting ? 'Deleting...' : 'Confirm Delete'}
</Button>
</div>
</DialogContent>
</Dialog>
Comment on lines +18 to +45
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dialog doesn't automatically close after the delete action completes. Although the user is navigated away on success, if an error occurs or if there's a delay, the dialog remains open with no way to dismiss it except by clicking outside. Consider using controlled Dialog state with open/onOpenChange props and closing the dialog in both the onSuccess callback (before navigation) and potentially in the onError callback.

Copilot uses AI. Check for mistakes.
)
}

export default DeleteEventButton
39 changes: 39 additions & 0 deletions apps/web/src/components/controlls/publish-event-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Button } from '@/components/ui/button.tsx'
import { usePublishEvent } from '@/lib/hooks/use-publish-event.tsx'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog.tsx'

function PublishEventButton({ eventId }: { eventId: string }) {
const { publishEvent, isPublishing } = usePublishEvent(eventId)

return (
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After publishing an event (which changes its state from 'draft' to 'published'), only the single event query is invalidated with queryKey ['events', eventId]. However, the events list queries use queryKeys like ['events', { state: 'published' }] and ['events', { state: 'draft' }]. These list queries will not be invalidated, so the published event will still appear in the draft list and won't appear in the published list until the page is refreshed. Consider invalidating all events queries using { queryKey: ['events'] } to ensure list views are updated.

Copilot uses AI. Check for mistakes.
<Dialog>
<DialogTrigger asChild>
<Button>Publish</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Publish Event</DialogTitle>
<DialogDescription>
Are you sure you want to publish this event? Once published, it will
be visible in the search results and students will be able to
register.
</DialogDescription>
</DialogHeader>
<div className="flex justify-start gap-3 mt-4">
<Button onClick={() => publishEvent()} disabled={isPublishing}>
{isPublishing ? 'Publishing' : 'Publish'}
</Button>
</div>
</DialogContent>
</Dialog>
Comment on lines +16 to +35
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dialog doesn't automatically close after the publish action is successful. The button triggers publishEvent() but there's no mechanism to close the dialog after a successful publish. Consider using controlled Dialog state with open/onOpenChange props and closing the dialog in the onSuccess callback of the mutation, or add a DialogClose component wrapper around the confirmation button.

Copilot uses AI. Check for mistakes.
)
}

export default PublishEventButton
33 changes: 33 additions & 0 deletions apps/web/src/components/controlls/register-event-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState } from 'react'
import type { CustomField } from '@events.comp-soc.com/shared'
import { Button } from '@/components/ui/button.tsx'
import { useCommitteeAuth } from '@/lib/auth.ts'
import EventRegistrationFormDialog from '@/components/forms/event-registration-form-dialog.tsx'

function RegisterEventButton({
form,
title,
}: {
form: Array<CustomField>
title: string
}) {
const [open, setOpen] = useState(false)
const { isAuthenticated } = useCommitteeAuth()

return (
<>
<Button onClick={() => setOpen(!open)} disabled={!isAuthenticated}>
Register Now
</Button>
<EventRegistrationFormDialog
onFormSubmit={() => {}}
formStructure={form}
isOpen={open}
onOpenChange={() => setOpen(!open)}
eventTitle={title}
/>
</>
)
}

export default RegisterEventButton
57 changes: 27 additions & 30 deletions apps/web/src/components/forms/modify-event-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
} from '@/components/ui/tooltip.tsx'
import { Switch } from '@/components/ui/switch.tsx'
import { ButtonGroup } from '@/components/ui/button-group.tsx'
import { DEFAULT_FIELDS } from '@/config/event-form.ts'

export const EventFormSchema = z
.object({
Expand Down Expand Up @@ -102,41 +103,26 @@ export const FormToRequest = EventFormSchema.transform((form) => {
capacity: form.capacity ? parseInt(form.capacity, 10) : null,
date: date.toISOString(),
aboutMarkdown: form.aboutMarkdown || null,
locationURL: form.locationURL ? `https://${form.locationURL}` : null,
locationURL: form.locationURL ? form.locationURL : null,
form: form.registrationFormEnabled ? form.customFields : null,
}
}).pipe(EventContractSchema)

function ModifyEventForm({
onFormSubmit,
onCancel,
defaultValues,
isModify = false,
isLoading = false,
}: {
onFormSubmit: (value: z.infer<typeof EventFormSchema>) => void
defaultValues: z.infer<typeof EventFormSchema>
onCancel?: () => void
isModify?: boolean
isLoading?: boolean
}) {
const [open, setOpen] = useState(false)

const getDefaultCustomFields = (): Array<CustomField> => [
{
id: `field-${Date.now()}-1`,
type: 'select',
label: 'University Year',
required: true,
options: ['1', '2', '3', '4', 'Masters', 'PhD'],
},
{
id: `field-${Date.now()}-2`,
type: 'select',
label: 'Dietary Requirements',
options: ['None', 'Vegetarian', 'Vegan', 'Gluten-Free', 'Halal'],
required: true,
},
]

const form = useForm({
defaultValues,
validators: {
Expand Down Expand Up @@ -167,9 +153,8 @@ function ModifyEventForm({
form.setFieldValue('customFields', [...customFields, newField])
}

const addDefaultFields = () => {
const defaultFields = getDefaultCustomFields()
form.setFieldValue('customFields', [...customFields, ...defaultFields])
const setDefaultFields = () => {
form.setFieldValue('customFields', [...DEFAULT_FIELDS])
}

const removeCustomField = (id: string) => {
Expand Down Expand Up @@ -613,7 +598,7 @@ function ModifyEventForm({
size="sm"
disabled={isLoading}
variant="outline"
onClick={addDefaultFields}
onClick={setDefaultFields}
>
Default
</Button>
Expand Down Expand Up @@ -771,14 +756,26 @@ function ModifyEventForm({
className="flex-col sm:flex-row gap-2 sm:gap-3"
>
{isModify ? (
<Button
type="submit"
form="event-form"
disabled={isLoading}
className="w-full sm:w-auto"
>
{isLoading ? 'Updating' : 'Update'}
</Button>
<>
<Button
type="submit"
form="event-form"
disabled={isLoading}
className="w-full sm:w-auto"
>
{isLoading ? 'Updating' : 'Update'}
</Button>
{onCancel && (
<Button
variant="secondary"
disabled={isLoading}
onClick={onCancel}
className="w-full sm:w-auto"
>
Back
</Button>
)}
</>
) : (
<>
<Button
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/config/event-form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { CustomField } from '@events.comp-soc.com/shared'

export const DEFAULT_FIELDS: Array<CustomField> = [
{
id: `field-${Date.now()}-1`,
type: 'input',
label: 'University Email',
required: true,
},
{
id: `field-${Date.now()}-1`,
type: 'select',
label: 'University Year',
required: true,
options: ['1', '2', '3', '4', 'Masters', 'PhD'],
},
{
id: `field-${Date.now()}-2`,
Comment on lines +11 to +18
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DEFAULT_FIELDS array contains duplicate IDs. The first field has id field-${Date.now()}-1 (line 5), and the second field also has id field-${Date.now()}-1 (line 11). Since Date.now() is evaluated once at module load time, both fields will have identical IDs, which can cause issues with form field management and React key props. Each field should have a unique ID suffix.

Suggested change
id: `field-${Date.now()}-1`,
type: 'select',
label: 'University Year',
required: true,
options: ['1', '2', '3', '4', 'Masters', 'PhD'],
},
{
id: `field-${Date.now()}-2`,
id: `field-${Date.now()}-2`,
type: 'select',
label: 'University Year',
required: true,
options: ['1', '2', '3', '4', 'Masters', 'PhD'],
},
{
id: `field-${Date.now()}-3`,

Copilot uses AI. Check for mistakes.
type: 'select',
label: 'Dietary Requirements',
options: ['None', 'Vegetarian', 'Vegan', 'Gluten-Free', 'Halal'],
required: true,
},
]
36 changes: 0 additions & 36 deletions apps/web/src/config/mocks.ts

This file was deleted.

Loading