From 0b2452ccffe73b0fc5db2590669430e0fbb7de47 Mon Sep 17 00:00:00 2001 From: DGoel1602 Date: Fri, 13 Feb 2026 01:20:55 -0500 Subject: [PATCH 01/18] add auto form making on event check in + extract form responder into another abstract layer --- .../member-dashboard/forms/form-responses.tsx | 2 +- .../_components/form-responder-client.tsx | 2 +- apps/blade/src/app/forms/[formName]/page.tsx | 13 +++- packages/api/src/routers/forms.ts | 62 ++------------- packages/api/src/routers/member.ts | 76 ++++++++++++++++++- packages/api/src/utils.ts | 61 +++++++++++++++ 6 files changed, 152 insertions(+), 64 deletions(-) diff --git a/apps/blade/src/app/dashboard/_components/member-dashboard/forms/form-responses.tsx b/apps/blade/src/app/dashboard/_components/member-dashboard/forms/form-responses.tsx index 95b75163f..d49788521 100644 --- a/apps/blade/src/app/dashboard/_components/member-dashboard/forms/form-responses.tsx +++ b/apps/blade/src/app/dashboard/_components/member-dashboard/forms/form-responses.tsx @@ -37,7 +37,7 @@ export async function FormResponses() { diff --git a/apps/blade/src/app/forms/[formName]/page.tsx b/apps/blade/src/app/forms/[formName]/page.tsx index b91b8d817..64f942a39 100644 --- a/apps/blade/src/app/forms/[formName]/page.tsx +++ b/apps/blade/src/app/forms/[formName]/page.tsx @@ -68,13 +68,18 @@ export default async function FormResponderPage({ redirect(`/?callbackURL=${encodeURIComponent(callbackURL)}`); } - if (!params.formName) { + return ( + + ); +} + +export async function FormResponderHydrater({ formName } : { formName: string }) { + const session = await auth(); + + if (!session) { return ; } - // handle url encode form names to allow spacing and special characters - const formName = params.formName; - // use blade member name instead of discord name let userName = session.user.name || "Member"; try { diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 1278d8f1d..b17e0de97 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -3,7 +3,7 @@ import type { JSONSchema7 } from "json-schema"; import { TRPCError } from "@trpc/server"; import { and, count, desc, eq, inArray, lt, sql } from "drizzle-orm"; import jsonSchemaToZod from "json-schema-to-zod"; -import * as z from "zod"; +import { z } from "zod"; import { FORMS, MINIO } from "@forge/consts"; import { db } from "@forge/db/client"; @@ -28,66 +28,16 @@ import { generateJsonSchema, log, regenerateMediaUrls, + createForm, + CreateFormSchema, } from "../utils"; export const formsRouter = { createForm: permProcedure - .input( - FormSchemaSchema.omit({ - id: true, - name: true, - slugName: true, - createdAt: true, - formData: true, - formValidatorJson: true, - }) - .extend({ formData: FORMS.FormSchemaValidator }) - .extend({ section: z.string().optional() }), - ) + .input(CreateFormSchema) .mutation(async ({ input, ctx }) => { controlPerms.or(["EDIT_FORMS"], ctx); - - const jsonSchema = generateJsonSchema(input.formData); - - const slug_name = input.formData.name.toLowerCase().replaceAll(" ", "-"); - - if (!jsonSchema.success) { - throw new TRPCError({ - message: jsonSchema.msg, - code: "BAD_REQUEST", - }); - } - - let sectionId: string | null = null; - const sectionName = input.section ?? "General"; - - if (sectionName !== "General") { - const section = await db.query.FormSections.findFirst({ - where: (t, { eq }) => eq(t.name, sectionName), - }); - sectionId = section?.id ?? null; - } - - await db - .insert(FormsSchemas) - .values({ - ...input, - name: input.formData.name, - slugName: slug_name, - formValidatorJson: jsonSchema.schema, - sectionId, - }) - .onConflictDoUpdate({ - //If it already exists upsert it - target: FormsSchemas.id, - set: { - ...input, - name: input.formData.name, - slugName: slug_name, - formValidatorJson: jsonSchema.schema, - sectionId, - }, - }); + await createForm(input); }), updateForm: permProcedure @@ -184,7 +134,7 @@ export const formsRouter = { .input(z.object({ slug_name: z.string() })) .query(async ({ input }) => { const form = await db.query.FormsSchemas.findFirst({ - where: (t, { eq }) => eq(t.slugName, input.slug_name), + where: (t, { eq }) => eq(t.slugName, decodeURIComponent(input.slug_name)), }); if (form === undefined) { diff --git a/packages/api/src/routers/member.ts b/packages/api/src/routers/member.ts index 1c2ce4881..4ad34e7eb 100644 --- a/packages/api/src/routers/member.ts +++ b/packages/api/src/routers/member.ts @@ -30,8 +30,8 @@ import { } from "@forge/db/schemas/knight-hacks"; import { minioClient } from "../minio/minio-client"; -import { permProcedure, protectedProcedure } from "../trpc"; -import { controlPerms, log } from "../utils"; +import { permProcedure, protectedProcedure, publicProcedure } from "../trpc"; +import { controlPerms, createForm, log } from "../utils"; export const memberRouter = { createMember: protectedProcedure @@ -660,11 +660,83 @@ export const memberRouter = { const event = await db.query.Event.findFirst({ where: eq(Event.id, input.eventId), }); + if (!event) throw new TRPCError({ code: "NOT_FOUND", message: `Event with ID ${input.eventId} not found.`, }); + + const formName = event.name + " Feedback Form"; + const formSlugName = formName.toLowerCase().replaceAll(" ", "-"); + + + const form = await db.query.FormsSchemas.findFirst({ + where: (t, { eq }) => eq(t.slugName, formSlugName), + }); + + if (form === undefined) { + await createForm({ + formData: { + name: formName, + description: `Provide feedback for ${event.name} to help us make events better in the future!`, + questions: [ + { + "max": 10, + "min": 0, + "type": "LINEAR_SCALE", + "order": 0, + "optional": false, + "question": "How would you rate the event overall?" + }, + { + "max": 10, + "min": 0, + "type": "LINEAR_SCALE", + "order": 1, + "optional": false, + "question": "How much fun did you have?" + }, + { + "max": 10, + "min": 0, + "type": "LINEAR_SCALE", + "order": 2, + "optional": false, + "question": "How much did you learn?" + }, + { + "type": "MULTIPLE_CHOICE", + "order": 3, + "options": [ + "Discord", + "Instagram", + "Knightconnect", + "Word of Mouth", + "CECS Emailing List", + "Reddit", + "LinkedIn", + "Class Presentation", + "Another Club" + ], + "optional": false, + "question": "Where did you hear about us?", + "allowOther": true + }, + { + "type": "SHORT_ANSWER", + "order": 4, + "optional": true, + "question": "Do you have any additional feedback about this event?" + } + ], + }, + allowEdit: false, + allowResubmission: false, + duesOnly: false + }); + } + if (event.dues_paying) { const duesPayingMember = await db.query.DuesPayment.findFirst({ where: eq(DuesPayment.memberId, member.id), diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts index 83b147112..a0e067772 100644 --- a/packages/api/src/utils.ts +++ b/packages/api/src/utils.ts @@ -16,6 +16,8 @@ import { client } from "@forge/email"; import { env } from "./env"; import { minioClient } from "./minio/minio-client"; +import { FormSchemaSchema, FormsSchemas } from "@forge/db/schemas/knight-hacks"; +import z from "zod"; export const discord = new REST({ version: "10" }).setToken( env.DISCORD_BOT_TOKEN, @@ -491,3 +493,62 @@ export function getPermsAsList(perms: string) { } return list; } + +// All of this will be moved to @forge/utils but its here for now +export const CreateFormSchema = FormSchemaSchema.omit({ + id: true, + name: true, + slugName: true, + createdAt: true, + formData: true, + formValidatorJson: true, +}) +.extend({ formData: FORMS.FormSchemaValidator }) +.extend({ section: z.string().optional() }); + +type CreateFormType = z.infer; + +export async function createForm(input: CreateFormType) { + const jsonSchema = generateJsonSchema(input.formData); + + const slug_name = input.formData.name.toLowerCase().replaceAll(" ", "-"); + + if (!jsonSchema.success) { + throw new TRPCError({ + message: jsonSchema.msg, + code: "BAD_REQUEST", + }); + } + + let sectionId: string | null = null; + const sectionName = input.section ?? "General"; + + if (sectionName !== "General") { + const section = await db.query.FormSections.findFirst({ + where: (t, { eq }) => eq(t.name, sectionName), + }); + sectionId = section?.id ?? null; + } + + await db + .insert(FormsSchemas) + .values({ + ...input, + name: input.formData.name, + slugName: slug_name, + formValidatorJson: jsonSchema.schema, + sectionId, + }) + .onConflictDoUpdate({ + //If it already exists upsert it + target: FormsSchemas.id, + set: { + ...input, + name: input.formData.name, + slugName: slug_name, + formValidatorJson: jsonSchema.schema, + sectionId, + }, + }); +} + From cb8fa23be1808ee09be8e5f5a5496c5a95e29247 Mon Sep 17 00:00:00 2001 From: DGoel1602 Date: Fri, 13 Feb 2026 01:26:04 -0500 Subject: [PATCH 02/18] fix: encode uri for all form redirects --- apps/blade/src/components/forms/form-card.tsx | 4 ++-- apps/blade/src/components/forms/form-qr-code.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/blade/src/components/forms/form-card.tsx b/apps/blade/src/components/forms/form-card.tsx index ad6991318..d5818b34d 100644 --- a/apps/blade/src/components/forms/form-card.tsx +++ b/apps/blade/src/components/forms/form-card.tsx @@ -157,14 +157,14 @@ export function FormCard({ className="flex-1" onClick={(e) => e.stopPropagation()} > - Responses + Responses diff --git a/apps/blade/src/components/forms/form-qr-code.tsx b/apps/blade/src/components/forms/form-qr-code.tsx index 1b458250e..9cba28c57 100644 --- a/apps/blade/src/components/forms/form-qr-code.tsx +++ b/apps/blade/src/components/forms/form-qr-code.tsx @@ -21,7 +21,7 @@ export function FormQRCodeDialog({ const formUrl = typeof window !== "undefined" ? // make the qr a url to the form forms/ - `${window.location.origin}/forms/${formSlug}` + `${window.location.origin}/forms/${encodeURIComponent(formSlug)}` : ""; useEffect(() => { From 0608cd25caffbce994a1bb1895a69c8e4a62d475 Mon Sep 17 00:00:00 2001 From: DGoel1602 Date: Fri, 13 Feb 2026 02:16:07 -0500 Subject: [PATCH 03/18] feat: changed event feedback to be blade forms instead --- .../member-dashboard/event/event-feedback.tsx | 325 +----------------- .../_components/connection-handler.ts | 63 ++++ .../_components/connection-handler.tsx | 63 ++++ .../_components/form-responder-client.tsx | 9 +- apps/blade/src/app/forms/[formName]/page.tsx | 74 +--- 5 files changed, 143 insertions(+), 391 deletions(-) create mode 100644 apps/blade/src/app/forms/[formName]/_components/connection-handler.ts create mode 100644 apps/blade/src/app/forms/[formName]/_components/connection-handler.tsx diff --git a/apps/blade/src/app/dashboard/_components/member-dashboard/event/event-feedback.tsx b/apps/blade/src/app/dashboard/_components/member-dashboard/event/event-feedback.tsx index 33e96158b..539c5bd31 100644 --- a/apps/blade/src/app/dashboard/_components/member-dashboard/event/event-feedback.tsx +++ b/apps/blade/src/app/dashboard/_components/member-dashboard/event/event-feedback.tsx @@ -1,47 +1,19 @@ "use client"; -import { useEffect, useState } from "react"; -import { Loader2 } from "lucide-react"; -import { z } from "zod"; +import { useState } from "react"; +import { FormResponderWrapper } from "~/app/forms/[formName]/_components/form-responder-client"; import type { InsertMember, SelectEvent } from "@forge/db/schemas/knight-hacks"; -import { EVENTS, FORMS } from "@forge/consts"; -import { InsertEventFeedbackSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Dialog, DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, DialogTrigger, } from "@forge/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - useForm, -} from "@forge/ui/form"; -import { RadioGroup, RadioGroupItem } from "@forge/ui/radio-group"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@forge/ui/select"; -import { Slider } from "@forge/ui/slider"; -import { Textarea } from "@forge/ui/textarea"; -import { toast } from "@forge/ui/toast"; - -import { api } from "~/trpc/react"; export function EventFeedbackForm({ event, - member, + member, size, }: { event: SelectEvent; @@ -49,75 +21,14 @@ export function EventFeedbackForm({ size: "md" | "sm" | "lg" | "icon" | null | undefined; }) { const [isOpen, setIsOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isFeedbackGiven, setIsFeedbackGiven] = useState(false); - const [eventOverallValue, setEventOverallValue] = useState(5); - const [funValue, setFunValue] = useState(5); - const [learnedValue, setLearnedValue] = useState(5); - - const { data: hasGivenFeedback } = - api.eventFeedback.hasGivenFeedback.useQuery({ - eventId: event.id, - memberId: member.id ?? "", - }); - useEffect(() => { - setIsFeedbackGiven(hasGivenFeedback ?? false); - }, [hasGivenFeedback]); - - const utils = api.useUtils(); - - const createFeedback = api.eventFeedback.createEventFeedback.useMutation({ - async onSuccess() { - toast.success("Feedback submitted successfully!"); - setIsOpen(false); - await utils.eventFeedback.invalidate(); - }, - onError(error) { - if (error.data?.code === "FORBIDDEN") { - toast.error("You cannot give feedback more than once for this event!"); - } else if (error.data?.code === "NOT_FOUND") { - toast.error("Cannot find event/member!"); - } else { - toast.error("Oops! Something went wrong. Please try again later."); - } - }, - onSettled() { - setIsLoading(false); - }, - }); - - const form = useForm({ - schema: InsertEventFeedbackSchema.extend({ - memberId: z.string().nonempty(), - eventId: z.string().nonempty(), - overallEventRating: z - .number() - .min(EVENTS.EVENT_FEEDBACK_SLIDER_MINIMUM) - .max(EVENTS.EVENT_FEEDBACK_SLIDER_MAXIMUM), - funRating: z - .number() - .min(EVENTS.EVENT_FEEDBACK_SLIDER_MINIMUM) - .max(EVENTS.EVENT_FEEDBACK_SLIDER_MAXIMUM), - learnedRating: z - .number() - .min(EVENTS.EVENT_FEEDBACK_SLIDER_MINIMUM) - .max(EVENTS.EVENT_FEEDBACK_SLIDER_MAXIMUM), - heardAboutUs: z.enum(FORMS.EVENT_FEEDBACK_HEARD), - additionalFeedback: z.string(), - similarEvent: z.enum(EVENTS.EVENT_FEEDBACK_SIMILAR_EVENT), - }), - defaultValues: { - memberId: member.id, - eventId: event.id, - additionalFeedback: "", - }, - }); + const formName = event.name + " Feedback Form"; + const slugName = formName.toLowerCase().replaceAll(" ", "-"); return ( - @@ -125,229 +36,7 @@ export function EventFeedbackForm({ aria-describedby={undefined} className="max-h-[80vh] overflow-y-auto" > - - Your Feedback For {event.name} - - -
- { - setIsLoading(true); - createFeedback.mutate({ - memberId: values.memberId, - eventId: values.eventId, - overallEventRating: values.overallEventRating, - funRating: values.funRating, - learnedRating: values.learnedRating, - heardAboutUs: values.heardAboutUs, - additionalFeedback: values.additionalFeedback, - similarEvent: values.similarEvent, - }); - })} - noValidate - > -
- {/* Slider for general rating of event */} - ( - - - How would you rate this event overall? - - -
-

1

- { - field.onChange(value[0]); - setEventOverallValue(value[0] ?? 5); - }} - value={[eventOverallValue]} - className="w-1/2" - /> -

10

-
-
- -
- )} - /> - {/* Slider for fun rating of the event */} - ( - - How much fun did you have? - -
-

1

- { - field.onChange(value[0]); - setFunValue(value[0] ?? 5); - }} - value={[funValue]} - className="w-1/2" - /> -

10

-
-
- -
- )} - /> - {/* Slider for rating of how much you learned */} - ( - - How much did you learn? - -
-

1

- { - field.onChange(value[0]); - setLearnedValue(value[0] ?? 5); - }} - value={[learnedValue]} - className="w-1/2" - /> -

10

-
-
- -
- )} - /> - {/* Dropdown for where you heard about us */} - ( - - Where did you hear about us? - - - - - - )} - /> - {/* Toggle button for similar event */} - ( - - - Would you like to see similar events to this one? - - - - {EVENTS.EVENT_FEEDBACK_SIMILAR_EVENT.map((option) => ( -
- - -
- ))} -
-
- -
- )} - /> - {/* Text field for additional feedback */} - ( - - - Do you have any additional feedback about this event? - - {" "} - — Optional - - - -