From 7a948f0ce97c3aa5d98e19ce049518add04b2267 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:25:39 -0500 Subject: [PATCH 01/23] refactor(consts): complete the majority of refactoring At its current state this implementation is WASHED (all caps) --- packages/api/src/routers/csv-importer.ts | 6 +- packages/api/src/routers/dues-payment.ts | 2 +- packages/api/src/routers/event-feedback.ts | 9 +- packages/api/src/routers/event.ts | 60 +- packages/api/src/routers/forms.ts | 13 +- packages/api/src/routers/guild.ts | 2 +- packages/api/src/routers/hacker.ts | 25 +- packages/api/src/routers/member.ts | 14 +- packages/api/src/routers/misc.ts | 61 +- packages/api/src/routers/qr.ts | 5 +- packages/api/src/routers/resume.ts | 2 +- packages/api/src/routers/roles.ts | 25 +- packages/api/src/trpc.ts | 4 +- packages/api/src/utils.ts | 68 +- packages/consts/README.md | 7 + packages/consts/package.json | 6 +- packages/consts/src/discord/index.ts | 86 + packages/consts/src/discord/knight-hacks-8.ts | 84 + .../consts/src/discord/project-launch-26.ts | 1 + packages/consts/src/events.ts | 70 + packages/consts/src/forms/companies.ts | 1002 ++++++++ packages/consts/src/forms/countries.ts | 252 ++ packages/consts/src/forms/index.ts | 344 +++ .../src/{knight-hacks.ts => forms/schools.ts} | 2053 ----------------- packages/consts/src/index.ts | 7 + packages/consts/src/misc.ts | 186 ++ packages/consts/src/officers.ts | 46 + packages/consts/src/util.ts | 1 + packages/consts/tsconfig.json | 1 - packages/db/scripts/bootstrap-superadmin.ts | 2 +- packages/db/scripts/seed_devdb.ts | 18 +- packages/db/src/schemas/knight-hacks.ts | 47 +- pnpm-lock.yaml | 45 +- 33 files changed, 2266 insertions(+), 2288 deletions(-) create mode 100644 packages/consts/README.md create mode 100644 packages/consts/src/discord/index.ts create mode 100644 packages/consts/src/discord/knight-hacks-8.ts create mode 100644 packages/consts/src/discord/project-launch-26.ts create mode 100644 packages/consts/src/events.ts create mode 100644 packages/consts/src/forms/companies.ts create mode 100644 packages/consts/src/forms/countries.ts create mode 100644 packages/consts/src/forms/index.ts rename packages/consts/src/{knight-hacks.ts => forms/schools.ts} (80%) create mode 100644 packages/consts/src/index.ts create mode 100644 packages/consts/src/misc.ts create mode 100644 packages/consts/src/officers.ts create mode 100644 packages/consts/src/util.ts diff --git a/packages/api/src/routers/csv-importer.ts b/packages/api/src/routers/csv-importer.ts index fff5eea3e..d5a7aca74 100644 --- a/packages/api/src/routers/csv-importer.ts +++ b/packages/api/src/routers/csv-importer.ts @@ -1,7 +1,7 @@ import { parse } from "csv-parse/sync"; import z from "zod"; -import { DEVPOST_TEAM_MEMBER_EMAIL_OFFSET } from "@forge/consts/knight-hacks"; +import { FORMS } from "@forge/consts"; import { eq, sql } from "@forge/db"; import { db } from "@forge/db/client"; import { Challenges, Submissions, Teams } from "@forge/db/schemas/knight-hacks"; @@ -90,11 +90,11 @@ export const csvImporterRouter = { const email2 = record["Team Member 1 Email"]; const email3 = recordValues[ - teamMember1EmailIndex + DEVPOST_TEAM_MEMBER_EMAIL_OFFSET + teamMember1EmailIndex + FORMS.DEVPOST_TEAM_MEMBER_EMAIL_OFFSET ]; const email4 = recordValues[ - teamMember1EmailIndex + DEVPOST_TEAM_MEMBER_EMAIL_OFFSET * 2 + teamMember1EmailIndex + FORMS.DEVPOST_TEAM_MEMBER_EMAIL_OFFSET * 2 ]; // Combine emails into comma-separated string, filtering out empty values diff --git a/packages/api/src/routers/dues-payment.ts b/packages/api/src/routers/dues-payment.ts index 212485596..57cc3d325 100644 --- a/packages/api/src/routers/dues-payment.ts +++ b/packages/api/src/routers/dues-payment.ts @@ -3,7 +3,6 @@ import { TRPCError } from "@trpc/server"; import Stripe from "stripe"; import { z } from "zod"; -import { KNIGHTHACKS_MEMBERSHIP_PRICE } from "@forge/consts/knight-hacks"; import { eq } from "@forge/db"; import { db } from "@forge/db/client"; import { DuesPayment, Member } from "@forge/db/schemas/knight-hacks"; @@ -11,6 +10,7 @@ import { DuesPayment, Member } from "@forge/db/schemas/knight-hacks"; import { env } from "../env"; import { protectedProcedure } from "../trpc"; import { log, stripe } from "../utils"; +import { KNIGHTHACKS_MEMBERSHIP_PRICE } from '@forge/consts'; export const duesPaymentRouter = { createCheckout: protectedProcedure.mutation(async ({ ctx }) => { diff --git a/packages/api/src/routers/event-feedback.ts b/packages/api/src/routers/event-feedback.ts index 1d9f178f9..2b009ad2e 100644 --- a/packages/api/src/routers/event-feedback.ts +++ b/packages/api/src/routers/event-feedback.ts @@ -2,10 +2,6 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { - EVENT_FEEDBACK_POINTS_INCREMENT, - OFFICER_ROLE_ID, -} from "@forge/consts/knight-hacks"; import { and, eq, sql } from "@forge/db"; import { db } from "@forge/db/client"; import { @@ -16,6 +12,7 @@ import { import { permProcedure } from "../trpc"; import { controlPerms, log } from "../utils"; +import { DISCORD, EVENTS } from '@forge/consts'; export const eventFeedbackRouter = { createEventFeedback: permProcedure @@ -61,7 +58,7 @@ export const eventFeedbackRouter = { await db .update(Member) .set({ - points: sql`${Member.points} + ${EVENT_FEEDBACK_POINTS_INCREMENT}`, + points: sql`${Member.points} + ${EVENTS.EVENT_FEEDBACK_POINTS_INCREMENT}`, }) .where(eq(Member.id, input.memberId)); @@ -98,7 +95,7 @@ export const eventFeedbackRouter = { ) .mutation(async ({ input, ctx }) => { await log({ - message: `<@&${OFFICER_ROLE_ID}> ${input.description}`, + message: `<@&${DISCORD.OFFICER_ROLE}> ${input.description}`, title: "Hackathon Issue", color: "uhoh_red", userId: ctx.session.user.discordUserId, diff --git a/packages/api/src/routers/event.ts b/packages/api/src/routers/event.ts index a3b449793..c273e9ec5 100644 --- a/packages/api/src/routers/event.ts +++ b/packages/api/src/routers/event.ts @@ -7,14 +7,10 @@ import { z } from "zod"; import { CALENDAR_TIME_ZONE, - DEV_GOOGLE_CALENDAR_ID, - DEV_KNIGHTHACKS_GUILD_ID, - DISCORD_EVENT_PRIVACY_LEVEL, - DISCORD_EVENT_TYPE, - EVENT_POINTS, - PROD_GOOGLE_CALENDAR_ID, - PROD_KNIGHTHACKS_GUILD_ID, -} from "@forge/consts/knight-hacks"; + DISCORD, + EVENTS, + GOOGLE_CALENDAR_ID, +} from "@forge/consts"; import { count, desc, eq, getTableColumns } from "@forge/db"; import { db } from "@forge/db/client"; import { @@ -27,19 +23,9 @@ import { Member, } from "@forge/db/schemas/knight-hacks"; -import { env } from "../env"; import { permProcedure, publicProcedure } from "../trpc"; import { calendar, controlPerms, discord, log } from "../utils"; -const GOOGLE_CALENDAR_ID = - env.NODE_ENV === "production" - ? (PROD_GOOGLE_CALENDAR_ID as string) - : (DEV_GOOGLE_CALENDAR_ID as string); -const KNIGHTHACKS_GUILD_ID = - env.NODE_ENV === "production" - ? (PROD_KNIGHTHACKS_GUILD_ID as string) - : (DEV_KNIGHTHACKS_GUILD_ID as string); - export const eventRouter = { getEvents: publicProcedure.query(async () => { const events = await db @@ -132,21 +118,21 @@ export const eventRouter = { ? `### ⚔️ ${input.hackathonName} ⚔️\n\n` : ""; - const pointDesc = `\n\n**⭐ ${EVENT_POINTS[input.tag] || 0} Points**`; + const pointDesc = `\n\n**⭐ ${EVENTS.EVENT_POINTS[input.tag] || 0} Points**`; // Step 1: Create the event in Discord let discordEventId: string | undefined; try { const response = (await discord.post( - Routes.guildScheduledEvents(KNIGHTHACKS_GUILD_ID), + Routes.guildScheduledEvents(DISCORD.KNIGHTHACKS_GUILD), { body: { description: hackDesc + input.description + pointDesc, name: formattedName, - privacy_level: DISCORD_EVENT_PRIVACY_LEVEL, + privacy_level: DISCORD.DISCORD_EVENT_PRIVACY_LEVEL, scheduled_start_time: startLocalIso, // Use ISO for Discord scheduled_end_time: endLocalIso, // Use ISO for Discord - entity_type: DISCORD_EVENT_TYPE, + entity_type: DISCORD.DISCORD_EVENT_TYPE, entity_metadata: { location: input.location, }, @@ -189,7 +175,10 @@ export const eventRouter = { if (discordEventId) { try { await discord.delete( - Routes.guildScheduledEvent(KNIGHTHACKS_GUILD_ID, discordEventId), + Routes.guildScheduledEvent( + DISCORD.KNIGHTHACKS_GUILD, + discordEventId, + ), ); } catch (cleanupErr) { console.error(JSON.stringify(cleanupErr, null, 2)); @@ -227,7 +216,7 @@ export const eventRouter = { ...input, start_datetime: dayBeforeStart, end_datetime: dayBeforeEnd, - points: EVENT_POINTS[input.tag] || 0, + points: EVENTS.EVENT_POINTS[input.tag] || 0, discordId: discordEventId, googleId: googleEventId, }); @@ -237,7 +226,10 @@ export const eventRouter = { // Clean up the event in Discord if the database insert fails try { await discord.delete( - Routes.guildScheduledEvent(KNIGHTHACKS_GUILD_ID, discordEventId), + Routes.guildScheduledEvent( + DISCORD.KNIGHTHACKS_GUILD, + discordEventId, + ), ); } catch (cleanupErr) { console.error(JSON.stringify(cleanupErr, null, 2)); @@ -321,20 +313,23 @@ export const eventRouter = { ? `### ⚔️ ${input.hackathonName} ⚔️\n\n` : ""; - const pointDesc = `\n\n**⭐ ${EVENT_POINTS[input.tag] || 0} Points**`; + const pointDesc = `\n\n**⭐ ${EVENTS.EVENT_POINTS[input.tag] || 0} Points**`; // Step 1: Update the event in Discord try { await discord.patch( - Routes.guildScheduledEvent(KNIGHTHACKS_GUILD_ID, input.discordId), + Routes.guildScheduledEvent( + DISCORD.KNIGHTHACKS_GUILD, + input.discordId, + ), { body: { description: hackDesc + input.description + pointDesc, name: formattedName, - privacy_level: DISCORD_EVENT_PRIVACY_LEVEL, + privacy_level: DISCORD.DISCORD_EVENT_PRIVACY_LEVEL, scheduled_start_time: startLocalIso, scheduled_end_time: endLocalIso, - entity_type: DISCORD_EVENT_TYPE, + entity_type: DISCORD.DISCORD_EVENT_TYPE, entity_metadata: { location: input.location, }, @@ -460,7 +455,7 @@ export const eventRouter = { ...input, start_datetime: dayBeforeStart, end_datetime: dayBeforeEnd, - points: input.hackathonId ? EVENT_POINTS[input.tag] || 0 : 0, + points: input.hackathonId ? EVENTS.EVENT_POINTS[input.tag] || 0 : 0, }) .where(eq(Event.id, input.id)); }), @@ -487,7 +482,10 @@ export const eventRouter = { // Step 1: Delete the event in Discord try { await discord.delete( - Routes.guildScheduledEvent(KNIGHTHACKS_GUILD_ID, input.discordId), + Routes.guildScheduledEvent( + DISCORD.KNIGHTHACKS_GUILD, + input.discordId, + ), ); } catch (error) { console.error(JSON.stringify(error, null, 2)); diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index beed367fc..b0674e360 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -4,13 +4,12 @@ import { and, count, desc, eq, inArray, lt, sql } from "drizzle-orm"; import jsonSchemaToZod from "json-schema-to-zod"; import * as z from "zod"; -import type { FormType } from "@forge/consts/knight-hacks"; import { FORM_ASSETS_BUCKET, - FormSchemaValidator, + FORMS, KNIGHTHACKS_S3_BUCKET_REGION, PRESIGNED_URL_EXPIRY, -} from "@forge/consts/knight-hacks"; +} from "@forge/consts"; import { db } from "@forge/db/client"; import { Permissions, Roles } from "@forge/db/schemas/auth"; import { @@ -46,7 +45,7 @@ export const formsRouter = { formData: true, formValidatorJson: true, }) - .extend({ formData: FormSchemaValidator }) + .extend({ formData: FORMS.FormSchemaValidator }) .extend({ section: z.string().optional() }), ) .mutation(async ({ input, ctx }) => { @@ -104,7 +103,7 @@ export const formsRouter = { formData: true, formValidatorJson: true, }) - .extend({ formData: FormSchemaValidator }) + .extend({ formData: FORMS.FormSchemaValidator }) .extend({ responseRoleIds: z.array(z.string().uuid()).optional() }), ) .mutation(async ({ input, ctx }) => { @@ -187,7 +186,7 @@ export const formsRouter = { } const { formValidatorJson: _JSONValidator, ...retForm } = form; - const formData = form.formData as FormType; + const formData = form.formData as FORMS.FormType; const responseRoles = await db .select({ roleId: FormResponseRoles.roleId }) @@ -426,7 +425,7 @@ export const formsRouter = { } } - const formData = form.formData as FormType; + const formData = form.formData as FORMS.FormType; const jsonSchema = generateJsonSchema(formData); if (!jsonSchema.success) { diff --git a/packages/api/src/routers/guild.ts b/packages/api/src/routers/guild.ts index c9ccb9714..23eb56968 100644 --- a/packages/api/src/routers/guild.ts +++ b/packages/api/src/routers/guild.ts @@ -4,7 +4,7 @@ import { TRPCError } from "@trpc/server"; import { Client } from "minio"; import { z } from "zod"; -import { KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts/knight-hacks"; +import { KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts"; import { and, count, sql } from "@forge/db"; import { db } from "@forge/db/client"; import { Member } from "@forge/db/schemas/knight-hacks"; diff --git a/packages/api/src/routers/hacker.ts b/packages/api/src/routers/hacker.ts index 69e9ae017..05f4c077c 100644 --- a/packages/api/src/routers/hacker.ts +++ b/packages/api/src/routers/hacker.ts @@ -3,15 +3,13 @@ import { TRPCError } from "@trpc/server"; import QRCode from "qrcode"; import { z } from "zod"; -import type { AssignableHackerClass } from "@forge/consts/knight-hacks"; import type { HackerClass } from "@forge/db/schemas/knight-hacks"; import { BUCKET_NAME, - CLASS_ROLE_ID, - HACKATHON_APPLICATION_STATES, - KH_EVENT_ROLE_ID, + DISCORD, + FORMS, KNIGHTHACKS_S3_BUCKET_REGION, -} from "@forge/consts/knight-hacks"; +} from "@forge/consts"; import { and, count, desc, eq, gt, or, sql, sum } from "@forge/db"; import { db } from "@forge/db/client"; import { Session } from "@forge/db/schemas/auth"; @@ -25,6 +23,7 @@ import { InsertHackerSchema, } from "@forge/db/schemas/knight-hacks"; +import type { AssignableHackerClass } from "../../../consts/src/discord/knight-hacks-8"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; import { @@ -966,7 +965,7 @@ export const hackerRouter = { controlPerms.or(["READ_HACK_DATA"], ctx); const results = await Promise.all( - HACKATHON_APPLICATION_STATES.map(async (s) => { + FORMS.HACKATHON_APPLICATION_STATES.map(async (s) => { const rows = await db .select({ count: count() }) .from(HackerAttendee) @@ -981,7 +980,7 @@ export const hackerRouter = { ); const counts = Object.fromEntries(results) as Record< - (typeof HACKATHON_APPLICATION_STATES)[number], + (typeof FORMS.HACKATHON_APPLICATION_STATES)[number], number >; @@ -1171,15 +1170,21 @@ export const hackerRouter = { }); } else { try { - await addRoleToMember(discordId, KH_EVENT_ROLE_ID); + await addRoleToMember( + discordId, + DISCORD.KNIGHTHACKS_8.KH_EVENT_ROLE_ID, + ); console.log( - `Assigned role ${KH_EVENT_ROLE_ID} to user ${discordId}`, + `Assigned role ${DISCORD.KNIGHTHACKS_8.KH_EVENT_ROLE_ID} to user ${discordId}`, ); // VIP will already be given the discord role ahead of time, so no need to assign again if (assignedClass) { await addRoleToMember( discordId, - CLASS_ROLE_ID[assignedClass as AssignableHackerClass], + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unnecessary-condition + DISCORD.KNIGHTHACKS_8.CLASS_ROLE_ID[ + assignedClass as AssignableHackerClass + ] ?? "", ); } } catch (e) { diff --git a/packages/api/src/routers/member.ts b/packages/api/src/routers/member.ts index a45abcc42..dcdb5582b 100644 --- a/packages/api/src/routers/member.ts +++ b/packages/api/src/routers/member.ts @@ -5,10 +5,10 @@ import { z } from "zod"; import { BUCKET_NAME, - COMPANIES, DUES_PAYMENT, + FORMS, KNIGHTHACKS_S3_BUCKET_REGION, -} from "@forge/consts/knight-hacks"; +} from "@forge/consts"; import { and, count, @@ -90,7 +90,10 @@ export const memberRouter = { //If the company the user entered doesn't already exist, add it to the other companies db const company = input.company; - if (company && !(COMPANIES as readonly string[]).includes(company)) { + if ( + company && + !(FORMS.COMPANIES as readonly string[]).includes(company) + ) { try { await db.insert(OtherCompanies).values({ name: company, @@ -181,7 +184,10 @@ export const memberRouter = { } const company = input.company; - if (company && !(COMPANIES as readonly string[]).includes(company)) { + if ( + company && + !(FORMS.COMPANIES as readonly string[]).includes(company) + ) { try { await db.insert(OtherCompanies).values({ name: company, diff --git a/packages/api/src/routers/misc.ts b/packages/api/src/routers/misc.ts index 8a556bf50..931901e5c 100644 --- a/packages/api/src/routers/misc.ts +++ b/packages/api/src/routers/misc.ts @@ -2,14 +2,10 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { Routes } from "discord-api-types/v10"; import { z } from "zod"; -import { - ALLOWED_FORM_ASSIGNABLE_DISC_ROLES, - RECRUITING_CHANNEL, - TEAM_MAP, -} from "@forge/consts/knight-hacks"; +import { DISCORD } from "@forge/consts"; import { protectedProcedure } from "../trpc"; -import { discord, KNIGHTHACKS_GUILD_ID, sendEmail } from "../utils"; +import { discord } from "../utils"; export interface FundingRequestInput { team: string; @@ -88,7 +84,7 @@ export const miscRouter = { }), ) .mutation(async ({ input, ctx }) => { - if (!ALLOWED_FORM_ASSIGNABLE_DISC_ROLES.includes(input.roleId)) { + if (!DISCORD.ALLOWED_FORM_ASSIGNABLE_DISC_ROLES.includes(input.roleId)) { throw new Error( `Roleid: ${input.roleId} is not assignable through forms for security purposes. Add to consts and make a PR if this is a mistake.`, ); @@ -97,7 +93,11 @@ export const miscRouter = { try { const discId = ctx.session.user.discordUserId; await discord.put( - Routes.guildMemberRole(KNIGHTHACKS_GUILD_ID, discId, input.roleId), + Routes.guildMemberRole( + DISCORD.KNIGHTHACKS_GUILD, + discId, + input.roleId, + ), ); } catch { throw new Error( @@ -129,7 +129,7 @@ export const miscRouter = { }), ) .mutation(async ({ input }) => { - const team = TEAM_MAP.find((team) => team.team === input.team); + const team = DISCORD.TEAMS.find((team) => team.team === input.team); if (!team) { throw new Error("Team not found"); } @@ -139,7 +139,7 @@ export const miscRouter = { // Convert hex color string to integer for Discord API const colorInt = parseInt(team.color.replace("#", ""), 16); - await discord.post(Routes.channelMessages(RECRUITING_CHANNEL), { + await discord.post(Routes.channelMessages(DISCORD.RECRUITING_CHANNEL), { body: { content: `<@&${directorRole}> **New Applicant for ${team.team}!**`, embeds: [ @@ -188,45 +188,4 @@ export const miscRouter = { }, }); }), - - fundingRequest: protectedProcedure - .meta({ - id: "fundingRequest", - inputSchema: z.object({ - team: z.string().min(1), - description: z.string(), - amount: z.number(), - itemization: z.string(), - importance: z.number(), - dateNeeded: z.string(), - deadlineType: z.string(), - }), - }) - .input( - z.object({ - team: z.string().min(1), - description: z.string(), - amount: z.number(), - itemization: z.string(), - importance: z.number(), - dateNeeded: z.string(), - deadlineType: z.string(), - }), - ) - .mutation(async ({ input }) => { - const dateObj = - typeof input.dateNeeded === "string" - ? new Date(input.dateNeeded) - : input.dateNeeded; - const formattedDate = `${String(dateObj.getMonth() + 1).padStart(2, "0")}/${String(dateObj.getDate()).padStart(2, "0")}`; - const data = generateListmonkData(input); - - await sendEmail({ - to: ["treasurer@knighthacks.org", "exec@knighthacks.org"], - subject: `KHFR - $${input.amount.toLocaleString()} | ${formattedDate} | ${input.team}`, - template_id: 12, - from: "Funding Requests ", - data: data, - }); - }), } satisfies TRPCRouterRecord; diff --git a/packages/api/src/routers/qr.ts b/packages/api/src/routers/qr.ts index c9974ef70..b787355ab 100644 --- a/packages/api/src/routers/qr.ts +++ b/packages/api/src/routers/qr.ts @@ -1,10 +1,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; import QRCode from "qrcode"; -import { - BUCKET_NAME, - KNIGHTHACKS_S3_BUCKET_REGION, -} from "@forge/consts/knight-hacks"; +import { BUCKET_NAME, KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts"; import { minioClient } from "../minio/minio-client"; import { protectedProcedure } from "../trpc"; diff --git a/packages/api/src/routers/resume.ts b/packages/api/src/routers/resume.ts index 0a9411724..1b9797eb7 100644 --- a/packages/api/src/routers/resume.ts +++ b/packages/api/src/routers/resume.ts @@ -3,7 +3,7 @@ import { TRPCError } from "@trpc/server"; import { Client } from "minio"; import { z } from "zod"; -import { KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts/knight-hacks"; +import { KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts"; import { db } from "@forge/db/client"; import { env } from "../env"; diff --git a/packages/api/src/routers/roles.ts b/packages/api/src/routers/roles.ts index 4014ca0a2..de64aa28f 100644 --- a/packages/api/src/routers/roles.ts +++ b/packages/api/src/routers/roles.ts @@ -4,17 +4,12 @@ import { TRPCError } from "@trpc/server"; import { Routes } from "discord-api-types/v10"; import { z } from "zod"; -import type { PermissionKey } from "@forge/consts/knight-hacks"; -import { - DEV_KNIGHTHACKS_GUILD_ID, - PERMISSIONS, - PROD_KNIGHTHACKS_GUILD_ID, -} from "@forge/consts/knight-hacks"; +import type { PermissionKey } from "@forge/consts"; +import { DISCORD, PERMISSIONS } from "@forge/consts"; import { eq, inArray, sql } from "@forge/db"; import { db } from "@forge/db/client"; import { Permissions, Roles, User } from "@forge/db/schemas/auth"; -import { env } from "../env"; import { permProcedure, protectedProcedure } from "../trpc"; import { addRoleToMember, @@ -25,11 +20,6 @@ import { removeRoleFromMember, } from "../utils"; -const KNIGHTHACKS_GUILD_ID = - env.NODE_ENV === "production" - ? (PROD_KNIGHTHACKS_GUILD_ID as string) - : (DEV_KNIGHTHACKS_GUILD_ID as string); - export const rolesRouter = { // ROLES @@ -82,7 +72,10 @@ export const rolesRouter = { for (const bladeUser of bladeUsers) { try { const guildMember = (await discord.get( - Routes.guildMember(KNIGHTHACKS_GUILD_ID, bladeUser.discordUserId), + Routes.guildMember( + DISCORD.KNIGHTHACKS_GUILD, + bladeUser.discordUserId, + ), )) as APIGuildMember; checkedCount++; @@ -221,7 +214,7 @@ export const rolesRouter = { .query(async ({ input }): Promise => { try { return (await discord.get( - Routes.guildRole(KNIGHTHACKS_GUILD_ID, input.roleId), + Routes.guildRole(DISCORD.KNIGHTHACKS_GUILD, input.roleId), )) as APIRole | null; } catch { return null; @@ -239,7 +232,7 @@ export const rolesRouter = { try { ret.push( (await discord.get( - Routes.guildRole(KNIGHTHACKS_GUILD_ID, r.discordRoleId), + Routes.guildRole(DISCORD.KNIGHTHACKS_GUILD, r.discordRoleId), )) as APIRole | null, ); } catch { @@ -253,7 +246,7 @@ export const rolesRouter = { getDiscordRoleCounts: protectedProcedure.query( async (): Promise | null> => { return (await discord.get( - `/guilds/${KNIGHTHACKS_GUILD_ID}/roles/member-counts`, + `/guilds/${DISCORD.KNIGHTHACKS_GUILD}/roles/member-counts`, )) as Record; }, ), diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 972e8eb52..621518eca 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -11,9 +11,9 @@ import superjson from "superjson"; import { ZodError } from "zod"; import type { Session } from "@forge/auth/server"; -import type { PermissionKey } from "@forge/consts/knight-hacks"; +import type { PermissionKey } from "@forge/consts"; import { validateToken } from "@forge/auth/server"; -import { PERMISSIONS } from "@forge/consts/knight-hacks"; +import { PERMISSIONS } from "@forge/consts"; import { eq, sql } from "@forge/db"; import { db } from "@forge/db/client"; import { Permissions, Roles } from "@forge/db/schemas/auth"; diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts index d3514b6a2..06331bccd 100644 --- a/packages/api/src/utils.ts +++ b/packages/api/src/utils.ts @@ -9,27 +9,16 @@ import { google } from "googleapis"; import Stripe from "stripe"; import type { Session } from "@forge/auth/server"; -import type { - FormType, - PermissionIndex, - PermissionKey, - ValidatorOptions, -} from "@forge/consts/knight-hacks"; +import type { PermissionIndex, PermissionKey } from "@forge/consts"; import { - DEV_DISCORD_ADMIN_ROLE_ID, - DEV_KNIGHTHACKS_GUILD_ID, - DEV_KNIGHTHACKS_LOG_CHANNEL, + DISCORD, FORM_ASSETS_BUCKET, - getDropdownOptionsFromConst, + FORMS, GOOGLE_PERSONIFY_EMAIL, - IS_PROD, PERMISSION_DATA, PERMISSIONS, PRESIGNED_URL_EXPIRY, - PROD_DISCORD_ADMIN_ROLE_ID, - PROD_KNIGHTHACKS_GUILD_ID, - PROD_KNIGHTHACKS_LOG_CHANNEL, -} from "@forge/consts/knight-hacks"; +} from "@forge/consts"; import { db } from "@forge/db/client"; import { JudgeSession, Roles } from "@forge/db/schemas/auth"; import { client } from "@forge/email"; @@ -37,31 +26,23 @@ import { client } from "@forge/email"; import { env } from "./env"; import { minioClient } from "./minio/minio-client"; -const DISCORD_ADMIN_ROLE_ID = IS_PROD - ? (PROD_DISCORD_ADMIN_ROLE_ID as string) - : (DEV_DISCORD_ADMIN_ROLE_ID as string); - -export const KNIGHTHACKS_GUILD_ID = IS_PROD - ? (PROD_KNIGHTHACKS_GUILD_ID as string) - : (DEV_KNIGHTHACKS_GUILD_ID as string); - -const PROD_VIP_ID = "1423358570203844689"; -const DEV_VIP_ID = "1423366084874080327"; -const VIP_ID = IS_PROD ? (PROD_VIP_ID as string) : (DEV_VIP_ID as string); export const discord = new REST({ version: "10" }).setToken( env.DISCORD_BOT_TOKEN, ); -const GUILD_ID = IS_PROD ? PROD_KNIGHTHACKS_GUILD_ID : DEV_KNIGHTHACKS_GUILD_ID; export async function addRoleToMember(discordUserId: string, roleId: string) { - await discord.put(Routes.guildMemberRole(GUILD_ID, discordUserId, roleId)); + await discord.put( + Routes.guildMemberRole(DISCORD.KNIGHTHACKS_GUILD, discordUserId, roleId), + ); } export async function removeRoleFromMember( discordUserId: string, roleId: string, ) { - await discord.delete(Routes.guildMemberRole(GUILD_ID, discordUserId, roleId)); + await discord.delete( + Routes.guildMemberRole(DISCORD.KNIGHTHACKS_GUILD, discordUserId, roleId), + ); } export async function resolveDiscordUserId( @@ -69,7 +50,7 @@ export async function resolveDiscordUserId( ): Promise { const q = username.trim().toLowerCase(); const members = (await discord.get( - `${Routes.guildMembersSearch(GUILD_ID)}?query=${encodeURIComponent(q)}&limit=1`, + `${Routes.guildMembersSearch(DISCORD.KNIGHTHACKS_GUILD)}?query=${encodeURIComponent(q)}&limit=1`, )) as APIGuildMember[]; return members[0]?.user.id ?? null; } @@ -79,9 +60,9 @@ export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { typescript: true }); export const isDiscordAdmin = async (user: Session["user"]) => { try { const guildMember = (await discord.get( - Routes.guildMember(KNIGHTHACKS_GUILD_ID, user.discordUserId), + Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, user.discordUserId), )) as APIGuildMember; - return guildMember.roles.includes(DISCORD_ADMIN_ROLE_ID); + return guildMember.roles.includes(DISCORD.ADMIN_ROLE); } catch (err) { console.error("Error: ", err); return false; @@ -98,7 +79,7 @@ export const hasPermission = ( export const parsePermissions = async (discordUserId: string) => { const guildMember = (await discord.get( - Routes.guildMember(KNIGHTHACKS_GUILD_ID, discordUserId), + Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, discordUserId), )) as APIGuildMember; const permissionsLength = Object.keys(PERMISSIONS).length; @@ -180,7 +161,7 @@ export const controlPerms = { export const isDiscordMember = async (user: Session["user"]) => { try { await discord.get( - Routes.guildMember(KNIGHTHACKS_GUILD_ID, user.discordUserId), + Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, user.discordUserId), ); return true; } catch { @@ -190,9 +171,9 @@ export const isDiscordMember = async (user: Session["user"]) => { export async function isDiscordVIP(discordUserId: string) { const guildMember = (await discord.get( - Routes.guildMember(GUILD_ID, discordUserId), + Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, discordUserId), )) as APIGuildMember; - return guildMember.roles.includes(VIP_ID); + return guildMember.roles.includes(DISCORD.VIP_ROLE); } export const sendEmail = async ({ @@ -229,11 +210,6 @@ export const sendEmail = async ({ } }; -const KNIGHTHACKS_LOG_CHANNEL = - env.NODE_ENV === "production" - ? (PROD_KNIGHTHACKS_LOG_CHANNEL as string) - : (DEV_KNIGHTHACKS_LOG_CHANNEL as string); - export async function log({ title, message, @@ -245,7 +221,7 @@ export async function log({ color: "tk_blue" | "blade_purple" | "uhoh_red" | "success_green"; userId: string; }) { - await discord.post(Routes.channelMessages(KNIGHTHACKS_LOG_CHANNEL), { + await discord.post(Routes.channelMessages(DISCORD.LOG_CHANNEL), { body: { embeds: [ { @@ -349,11 +325,11 @@ function createJsonSchemaValidator({ min, max, allowOther, -}: ValidatorOptions): OptionalSchema { +}: FORMS.ValidatorOptions): OptionalSchema { const schema: JSONSchema7 = {}; const resolvedOptions = optionsConst - ? [...getDropdownOptionsFromConst(optionsConst)] + ? [...FORMS.getDropdownOptionsFromConst(optionsConst)] : options; switch (type) { @@ -440,7 +416,7 @@ function createJsonSchemaValidator({ return { success: true, schema }; } -export function generateJsonSchema(form: FormType): OptionalSchema { +export function generateJsonSchema(form: FORMS.FormType): OptionalSchema { const schema: JSONSchema7 = { type: "object", properties: {}, @@ -472,7 +448,7 @@ export function generateJsonSchema(form: FormType): OptionalSchema { // Helper to regenerate presigned URLs for media export async function regenerateMediaUrls( - instructions: FormType["instructions"], + instructions: FORMS.FormType["instructions"], ) { if (!instructions) return []; const updatedQuestions = await Promise.all( diff --git a/packages/consts/README.md b/packages/consts/README.md new file mode 100644 index 000000000..d842930e4 --- /dev/null +++ b/packages/consts/README.md @@ -0,0 +1,7 @@ +# @forge/consts + +The `consts` package is, obviously, where we store constant variables. Before you add to this, you must ask yourself: "does this need to be used between two separate apps". If the answer is yes, find a good place to put it. If the answer is no, then we probably don't want to put it here. + +Some types of data kind of go against this would be, for example, a named Discord channel that we need to remember. Storing it at the top of the file that it is used in makes no sense. Just some food for thought. + +Note: please do not use `misc.ts`. This is a placeholder until we can migrate the rest. Thanks for understanding. diff --git a/packages/consts/package.json b/packages/consts/package.json index de87c1563..d702c6ada 100644 --- a/packages/consts/package.json +++ b/packages/consts/package.json @@ -4,9 +4,9 @@ "version": "0.1.0", "type": "module", "exports": { - "./*": { - "types": "./dist/*.d.ts", - "default": "./src/*.ts" + ".": { + "types": "./dist/index.d.ts", + "default": "./src/index.ts" } }, "license": "MIT", diff --git a/packages/consts/src/discord/index.ts b/packages/consts/src/discord/index.ts new file mode 100644 index 000000000..c2a607096 --- /dev/null +++ b/packages/consts/src/discord/index.ts @@ -0,0 +1,86 @@ +// TODO: JSDOC all all of the non PROD_ or DEV_ exports +import { IS_PROD } from "../util"; +import * as KNIGHTHACKS_8 from "./knight-hacks-8"; +import * as PROJECT_LAUNCH_26 from "./project-launch-26"; + +export { KNIGHTHACKS_8 }; +export { PROJECT_LAUNCH_26 }; + +export const PROD_OFFICER_ROLE = "486629374758748180"; +export const DEV_OFFICER_ROLE = "1246637685011906560"; +export const OFFICER_ROLE = IS_PROD ? PROD_OFFICER_ROLE : DEV_OFFICER_ROLE; + +export const PROD_ADMIN_ROLE = "1319413082258411652"; +export const DEV_ADMIN_ROLE = "1321955700540309645"; +export const ADMIN_ROLE = IS_PROD ? PROD_ADMIN_ROLE : DEV_ADMIN_ROLE; + +export const PROD_VOLUNTEER_ROLE = "1415505872360312974"; +export const DEV_VOLUNTEER_ROLE = "1426947077514203279"; +export const VOLUNTEER_ROLE = IS_PROD + ? PROD_VOLUNTEER_ROLE + : DEV_VOLUNTEER_ROLE; + +// TODO: add DEV_ALUMNI_ROLE +export const PROD_ALUMNI_ROLE = "486629512101232661"; +export const ALUMNI_ROLE = PROD_ALUMNI_ROLE; + +export const PROD_VIP_ROLE = "1423358570203844689"; +export const DEV_VIP_ROLE = "1423366084874080327"; +export const VIP_ROLE = IS_PROD ? PROD_VIP_ROLE : DEV_VIP_ROLE; + +export const PROD_KNIGHTHACKS_GUILD = "486628710443778071"; +export const DEV_KNIGHTHACKS_GUILD = "1151877367434850364"; +export const KNIGHTHACKS_GUILD = IS_PROD + ? PROD_KNIGHTHACKS_GUILD + : DEV_KNIGHTHACKS_GUILD; + +export const PROD_LOG_CHANNEL = "1324885515412963531"; +export const DEV_LOG_CHANNEL = "1284582557689843785"; +export const LOG_CHANNEL = IS_PROD ? PROD_LOG_CHANNEL : DEV_LOG_CHANNEL; + +export const PROD_RECRUITING_CHANNEL = "1461758896950608104"; +export const RECRUITING_CHANNEL = IS_PROD + ? PROD_RECRUITING_CHANNEL + : DEV_LOG_CHANNEL; + +export const PERMANENT_INVITE = "https://discord.com/invite/Kv5g9vf"; + +export const DISCORD_EVENT_TYPE = 3; +export const DISCORD_EVENT_PRIVACY_LEVEL = 2; + +export const TEAMS = [ + { + team: "Outreach", + color: "#88fea1", + director_role: "779845137822908436", + }, + { + team: "Design", + color: "#eaacff", + director_role: "874028482089349172", + }, + { + team: "Development", + color: "#93ceff", + director_role: "1082124530077683772", + }, + { + team: "Sponsorship", + color: "#f5f4af", + director_role: "626815399442513920", + }, + { + team: "Workshops", + color: "#206694", + director_role: "757002949603098837", + }, + { + team: "Projects/Mentorship", + color: "#3498db", + director_role: "1244790444626280550", + }, +]; + +export const ALLOWED_FORM_ASSIGNABLE_DISC_ROLES = [ + PROJECT_LAUNCH_26.MEMBER_ROLE, +]; diff --git a/packages/consts/src/discord/knight-hacks-8.ts b/packages/consts/src/discord/knight-hacks-8.ts new file mode 100644 index 000000000..8d232fdef --- /dev/null +++ b/packages/consts/src/discord/knight-hacks-8.ts @@ -0,0 +1,84 @@ +// Because @forge/db requires @forge/consts we can't import @forge/db right +// here. Ideally there is another way but im not too sure. +// TODO: look into not doing this +import type { HackerClass } from "../../../db/src/schemas/knight-hacks"; +import { IS_PROD } from "../util"; + +export const PROD_DISCORD_ROLE_KNIGHT_HACKS_8 = "1408025502119231498"; +export const DEV_DISCORD_ROLE_KNIGHT_HACKS_8 = "1420819573816692857"; +export const KH_EVENT_ROLE_ID = IS_PROD + ? PROD_DISCORD_ROLE_KNIGHT_HACKS_8 + : DEV_DISCORD_ROLE_KNIGHT_HACKS_8; + +export const PROD_DISCORD_ROLE_OPERATORS = "1415702220825038879"; +export const DEV_DISCORD_ROLE_OPERATORS = "1420819261223600239"; + +export const PROD_DISCORD_ROLE_MACHINIST = "1415702276433248406"; +export const DEV_DISCORD_ROLE_MACHINIST = "1420819223797698683"; + +export const PROD_DISCORD_ROLE_SENTINELS = "1415702308494250136"; +export const DEV_DISCORD_ROLE_SENTINELS = "1420819277279137892"; + +export const PROD_DISCORD_ROLE_HARBINGER = "1415702341214011392"; +export const DEV_DISCORD_ROLE_HARBINGER = "1420819326075801670"; + +export const PROD_DISCORD_ROLE_MONSTOLOGIST = "1415702361653121044"; +export const DEV_DISCORD_ROLE_MONSTOLOGIST = "1420819295759237222"; + +export const PROD_DISCORD_ROLE_ALCHEMIST = "1415702383274491934"; +export const DEV_DISCORD_ROLE_ALCHEMIST = "1420819309965611140"; + +export const PROD_DISCORD_SUPERADMIN = "486629374758748180"; +export const DEV_DISCORD_SUPERADMIN = "1246637685011906560"; + +export type AssignableHackerClass = Exclude; + +export const CLASS_ROLE_ID: Record = { + Operator: IS_PROD ? PROD_DISCORD_ROLE_OPERATORS : DEV_DISCORD_ROLE_OPERATORS, + Mechanist: IS_PROD ? PROD_DISCORD_ROLE_MACHINIST : DEV_DISCORD_ROLE_MACHINIST, + Sentinel: IS_PROD ? PROD_DISCORD_ROLE_SENTINELS : DEV_DISCORD_ROLE_SENTINELS, + Harbinger: IS_PROD ? PROD_DISCORD_ROLE_HARBINGER : DEV_DISCORD_ROLE_HARBINGER, + Monstologist: IS_PROD + ? PROD_DISCORD_ROLE_MONSTOLOGIST + : DEV_DISCORD_ROLE_MONSTOLOGIST, + Alchemist: IS_PROD ? PROD_DISCORD_ROLE_ALCHEMIST : DEV_DISCORD_ROLE_ALCHEMIST, +} as const satisfies Record; + +export interface ClassInfo { + team: string; + teamColor: string; + classPfp: string; +} + +export const HACKER_CLASS_INFO: Record = { + Mechanist: { + team: "Humanity", + teamColor: "#228be6", + classPfp: "/khviii/mechanist.jpg", + }, + Operator: { + team: "Humanity", + teamColor: "#228be6", + classPfp: "/khviii/operator.jpg", + }, + Sentinel: { + team: "Humanity", + teamColor: "#228be6", + classPfp: "/khviii/sentinel.jpg", + }, + Monstologist: { + team: "Monstrosity", + teamColor: "#e03131", + classPfp: "/khviii/monstologist.jpg", + }, + Harbinger: { + team: "Monstrosity", + teamColor: "#e03131", + classPfp: "/khviii/harbinger.jpg", + }, + Alchemist: { + team: "Monstrosity", + teamColor: "#e03131", + classPfp: "/khviii/alchemist.jpg", + }, +}; diff --git a/packages/consts/src/discord/project-launch-26.ts b/packages/consts/src/discord/project-launch-26.ts new file mode 100644 index 000000000..b9ed9efbc --- /dev/null +++ b/packages/consts/src/discord/project-launch-26.ts @@ -0,0 +1 @@ +export const MEMBER_ROLE = "1467281189088788489" \ No newline at end of file diff --git a/packages/consts/src/events.ts b/packages/consts/src/events.ts new file mode 100644 index 000000000..ae8ce158c --- /dev/null +++ b/packages/consts/src/events.ts @@ -0,0 +1,70 @@ +export const EVENT_TAGS = [ + "GBM", + "Social", + "Kickstart", + "Project Launch", + "Hello World", + "Sponsorship", + "Tech Exploration", + "Class Support", + "Workshop", + "OPS", + "Collabs", + "Check-in", + "Merch", + "Food", + "Ceremony", + "CAREER-FAIR", + "RSO-FAIR", +] as const; + +export const EVENT_FEEDBACK_SIMILAR_EVENT = ["Yes", "No"] as const; + +type EventTag = (typeof EVENT_TAGS)[number]; + +export const EVENT_POINTS: Record = { + GBM: 35, + Social: 25, + Kickstart: 25, + "Project Launch": 25, + "Hello World": 25, + Sponsorship: 50, + "Tech Exploration": 25, + "Class Support": 25, + Workshop: 25, + OPS: 20, + Collabs: 40, + "Check-in": 5, + Merch: 5, + Food: 5, + Ceremony: 50, + "CAREER-FAIR": 100, + "RSO-FAIR": 50, +} as const; + +export type EventTagsColor = + | "GBM" + | "Social" + | "Kickstart" + | "Project Launch" + | "Hello World" + | "Sponsorship" + | "Tech Exploration" + | "Class Support" + | "Workshop" + | "OPS" + | "Hackathon" + | "Collabs" + | "Check-in" + | "Merch" + | "Food" + | "Ceremony" + | "CAREER-FAIR" + | "RSO-FAIR"; + +export const EVENT_FEEDBACK_SLIDER_MINIMUM = 1; +export const EVENT_FEEDBACK_SLIDER_MAXIMUM = 10; +export const EVENT_FEEDBACK_SLIDER_STEP = 1; +export const EVENT_FEEDBACK_SLIDER_VALUE = 5; +export const EVENT_FEEDBACK_TEXT_ROWS = 4; +export const EVENT_FEEDBACK_POINTS_INCREMENT = 10; diff --git a/packages/consts/src/forms/companies.ts b/packages/consts/src/forms/companies.ts new file mode 100644 index 000000000..189cac733 --- /dev/null +++ b/packages/consts/src/forms/companies.ts @@ -0,0 +1,1002 @@ +export const COMPANIES = [ + "Walmart", + "Amazon", + "Apple", + "UnitedHealth Group", + "Berkshire Hathaway", + "CVS Health", + "Exxon Mobil", + "Alphabet", + "McKesson", + "Cencora", + "Costco Wholesale", + "JPMorgan Chase", + "Microsoft", + "Cardinal Health", + "Chevron", + "Cigna", + "Ford Motor", + "Bank of America", + "General Motors", + "Elevance Health", + "Citigroup", + "Centene", + "Home Depot", + "Marathon Petroleum", + "Kroger", + "Phillips 66", + "Fannie Mae", + "Walgreens Boots Alliance", + "Valero Energy", + "Meta Platforms", + "Verizon Communications", + "AT&T", + "Comcast", + "Wells Fargo", + "Goldman Sachs Group", + "Freddie Mac", + "Target", + "Humana", + "State Farm Insurance", + "Tesla", + "Morgan Stanley", + "Johnson & Johnson", + "Archer Daniels Midland", + "PepsiCo", + "United Parcel Service", + "FedEx", + "Walt Disney", + "Dell Technologies", + "Lowe's", + "Procter & Gamble", + "Energy Transfer", + "Boeing", + "Albertsons", + "Sysco", + "RTX", + "General Electric", + "Lockheed Martin", + "American Express", + "Caterpillar", + "MetLife", + "HCA Healthcare", + "Progressive", + "IBM", + "Deere", + "Nvidia", + "StoneX Group", + "Merck", + "ConocoPhillips", + "Pfizer", + "Delta Air Lines", + "TD Synnex", + "Publix Super Markets", + "Allstate", + "Cisco Systems", + "Nationwide", + "Charter Communications", + "AbbVie", + "New York Life Insurance", + "Intel", + "TJX", + "Prudential Financial", + "HP", + "United Airlines Holdings", + "Performance Food Group", + "Tyson Foods", + "American Airlines Group", + "Liberty Mutual Insurance Group", + "Nike", + "Oracle", + "Enterprise Products Partners", + "Capital One Financial", + "Plains GP Holdings", + "World Kinect", + "AIG", + "Coca-Cola", + "TIAA", + "CHS", + "Bristol-Myers Squibb", + "Dow", + "Best Buy", + "Thermo Fisher Scientific", + "Massachusetts Mutual Life Insurance", + "USAA", + "General Dynamics", + "Travelers", + "Warner Bros. Discovery", + "U.S. Bancorp", + "Abbott Laboratories", + "Northrop Grumman", + "Northwestern Mutual", + "Dollar General", + "PBF Energy", + "Uber Technologies", + "Honeywell International", + "Mondelez International", + "Starbucks", + "Qualcomm", + "Broadcom", + "US Foods Holding", + "D.R. Horton", + "Philip Morris International", + "Paccar", + "Salesforce", + "Nucor", + "Jabil", + "Lennar", + "Eli Lilly", + "Molina Healthcare", + "Cummins", + "Bank of New York Mellon", + "Netflix", + "Truist Financial", + "Arrow Electronics", + "3M", + "Visa", + "Apollo Global Management", + "HF Sinclair", + "CBRE Group", + "PNC Financial Services Group", + "Lithia Motors", + "CarMax", + "Paramount Global", + "Dollar Tree", + "United Natural Foods", + "PayPal Holdings", + "Penske Automotive Group", + "Hewlett Packard Enterprise", + "Duke Energy", + "Occidental Petroleum", + "NRG Energy", + "Amgen", + "NextEra Energy", + "Danaher", + "Gilead Sciences", + "AutoNation", + "Kraft Heinz", + "Avnet", + "Applied Materials", + "Southwest Airlines", + "Charles Schwab", + "Baker Hughes", + "McDonald's", + "Southern Company", + "Mastercard", + "Constellation Energy", + "Hartford Financial Services Group", + "PG&E", + "Coupang", + "EOG Resources", + "Union Pacific", + "Rite Aid", + "Macy's", + "Marriott International", + "Lear", + "Genuine Parts", + "Sherwin-Williams", + "Halliburton", + "Freeport-McMoRan", + "Live Nation Entertainment", + "Marsh & McLennan", + "Advanced Micro Devices", + "First Citizens BancShares", + "WESCO International", + "Carrier Global", + "Cleveland-Cliffs", + "Block", + "Exelon", + "KKR", + "CDW", + "Booking Holdings", + "Synchrony", + "Quanta Services", + "Jones Lang LaSalle", + "Discover Financial Services", + "Tenet Healthcare", + "Altria Group", + "Stryker", + "Kimberly-Clark", + "Waste Management", + "Cheniere Energy", + "Ross Stores", + "WestRock", + "General Mills", + "Goodyear Tire & Rubber", + "BJ's Wholesale Club", + "GE HealthCare Technologies", + "Colgate-Palmolive", + "Whirlpool", + "L3Harris Technologies", + "Adobe", + "Becton Dickinson", + "Pioneer Natural Resources", + "Cognizant Technology Solutions", + "Murphy USA", + "Fiserv", + "Parker-Hannifin", + "American Electric Power", + "International Paper", + "ManpowerGroup", + "Aramark", + "Steel Dynamics", + "Aflac", + "Reinsurance Group of America", + "Emerson Electric", + "State Street", + "PPG Industries", + "United States Steel", + "Automatic Data Processing", + "Group 1 Automotive", + "Dominion Energy", + "BlackRock", + "Oneok", + "C.H. Robinson Worldwide", + "Texas Instruments", + "Kohl's", + "AutoZone", + "Lam Research", + "Corteva", + "Peter Kiewit Sons'", + "Builders FirstSource", + "Kyndryl Holdings", + "EchoStar", + "American Family Insurance Group", + "Delek US Holdings", + "Land O'Lakes", + "Sempra", + "Global Partners", + "Grainger", + "Jacobs Solutions", + "Edison International", + "MGM Resorts International", + "Guardian Life Ins. Co. of America", + "Illinois Tool Works", + "Ameriprise Financial", + "PulteGroup", + "Targa Resources", + "Ally Financial", + "BorgWarner", + "Estée Lauder", + "Loews", + "O'Reilly Automotive", + "Markel Group", + "Stanley Black & Decker", + "Micron Technology", + "Fluor", + "Leidos Holdings", + "Viatris", + "Kinder Morgan", + "Ecolab", + "Baxter International", + "Devon Energy", + "Kellanova", + "Farmers Insurance Exchange", + "Casey's General Stores", + "IQVIA Holdings", + "Republic Services", + "Fox", + "Gap", + "Keurig Dr Pepper", + "Reliance", + "Asbury Automotive Group", + "Pacific Life", + "Vistra", + "Western & Southern Financial Group", + "Andersons", + "Nordstrom", + "Omnicom Group", + "Fidelity National Information Services", + "Consolidated Edison", + "CSX", + "AECOM", + "Lumen Technologies", + "Tractor Supply", + "DXC Technology", + "AGCO", + "Sonic Automotive", + "Intuit", + "United Rentals", + "Universal Health Services", + "Boston Scientific", + "Otis Worldwide", + "Xcel Energy", + "Edward Jones", + "Ball", + "LKQ", + "Mutual of Omaha", + "Mosaic", + "Textron", + "Labcorp Holdings", + "Principal Financial", + "Regeneron Pharmaceuticals", + "Raymond James Financial", + "Dick's Sporting Goods", + "Auto-Owners Insurance", + "Expedia Group", + "J.B. Hunt Transport Services", + "M&T Bank", + "DTE Energy", + "AES", + "Berry Global Group", + "Fifth Third Bancorp", + "Air Products & Chemicals", + "Corning", + "EMCOR Group", + "Amphenol", + "Westlake", + "DuPont", + "Liberty Media", + "S&P Global", + "Community Health Systems", + "FirstEnergy", + "Unum Group", + "Henry Schein", + "Western Digital", + "Analog Devices", + "Conagra Brands", + "Citizens Financial Group", + "Norfolk Southern", + "Entergy", + "W.R. Berkley", + "DaVita", + "Northern Trust", + "Hormel Foods", + "Crown Holdings", + "Avis Budget Group", + "Wayfair", + "MasTec", + "Eversource Energy", + "Newmont", + "Ryder System", + "Fidelity National Financial", + "Molson Coors Beverage", + "Caesars Entertainment", + "Lincoln National", + "VF", + "International Flavors & Fragrances", + "Huntington Ingalls Industries", + "Advance Auto Parts", + "Public Service Enterprise Group", + "Ulta Beauty", + "Hershey", + "Chewy", + "American Tower", + "Mohawk Industries", + "Assurant", + "Thor Industries", + "Graybar Electric", + "Yum China Holdings", + "Celanese", + "Qurate Retail", + "Williams", + "Interpublic Group", + "Ovintiv", + "Icahn Enterprises", + "Huntington Bancshares", + "Erie Insurance Group", + "Carvana", + "Hess", + "Dana", + "Alcoa", + "Equitable Holdings", + "KLA", + "Darden Restaurants", + "Autoliv", + "Alaska Air Group", + "KeyCorp", + "Las Vegas Sands", + "Owens & Minor", + "Hilton Worldwide Holdings", + "Ebay", + "Arthur J. Gallagher", + "LPL Financial Holdings", + "Cincinnati Financial", + "Toll Brothers", + "Motorola Solutions", + "Airbnb", + "Intercontinental Exchange", + "News Corp.", + "Chipotle Mexican Grill", + "Vertex Pharmaceuticals", + "Biogen", + "GXO Logistics", + "SpartanNash", + "Burlington Stores", + "Thrivent Financial for Lutherans", + "NVR", + "Owens Corning", + "Westinghouse Air Brake Technologies", + "Oshkosh", + "Global Payments", + "Lululemon athletica", + "Albemarle", + "JetBlue Airways", + "Seaboard", + "Constellation Brands", + "Graphic Packaging Holding", + "Hertz Global Holdings", + "FM Global", + "Campbell Soup", + "Expeditors Intl. of Washington", + "A-Mark Precious Metals", + "Booz Allen Hamilton Holding", + "Quest Diagnostics", + "Altice USA", + "PVH", + "Eastman Chemical", + "Insight Enterprises", + "Regions Financial", + "Beacon Roofing Supply", + "Rockwell Automation", + "Polaris", + "ServiceNow", + "Sanmina", + "UGI", + "WEC Energy Group", + "BrightSpring Health Services", + "Cintas", + "Commercial Metals", + "Continental Resources", + "Chesapeake Energy", + "CenterPoint Energy", + "NGL Energy Partners", + "DoorDash", + "NOV", + "Zoetis", + "J.M. Smucker", + "Microchip Technology", + "Dover", + "Diamondback Energy", + "Avery Dennison", + "PPL", + "ON Semiconductor", + "ARKO", + "Par Pacific Holdings", + "APA", + "Equinix", + "New York Community Bancorp", + "Foot Locker", + "Ingredion", + "Newell Brands", + "ABM Industries", + "Securian Financial Group", + "Prologis", + "Blackstone", + "Skechers U.S.A.", + "Masco", + "Rush Enterprises", + "Franklin Resources", + "ODP", + "American Financial Group", + "Packaging Corp. of America", + "Vulcan Materials", + "Interactive Brokers Group", + "Williams-Sonoma", + "XPO", + "Weyerhaeuser", + "Genworth Financial", + "CMS Energy", + "Science Applications International", + "Jefferies Financial Group", + "Bath & Body Works", + "Electronic Arts", + "Taylor Morrison Home", + "Zimmer Biomet Holdings", + "Clorox", + "Xylem", + "Voya Financial", + "Fastenal", + "Watsco", + "Workday", + "Old Republic International", + "RPM International", + "UFP Industries", + "Ameren", + "Knight-Swift Transportation Hldgs.", + "Monster Beverage", + "Intuitive Surgical", + "Super Micro Computer", + "Concentrix", + "O-I Glass", + "Yum Brands", + "Domtar", + "CommScope Holding", + "Post Holdings", + "Crown Castle", + "Avantor", + "KBR", + "Opendoor Technologies", + "APi Group", + "EQT", + "EnLink Midstream", + "Palo Alto Networks", + "Xerox Holdings", + "Ingersoll Rand", + "Dillard's", + "Martin Marietta Materials", + "Vertiv Holdings", + "Moderna", + "Boise Cascade", + "Sprouts Farmers Market", + "Agilent Technologies", + "Olin", + "Darling Ingredients", + "Sonoco Products", + "CACI International", + "Core & Main", + "Marathon Oil", + "Hyatt Hotels", + "Select Medical Holdings", + "McCormick", + "Tapestry", + "Coca-Cola Consolidated", + "Howmet Aerospace", + "Welltower", + "CF Industries Holdings", + "Ametek", + "TransDigm Group", + "Wynn Resorts", + "Southwestern Energy", + "Amkor Technology", + "Insperity", + "Patterson", + "T. Rowe Price", + "Ralph Lauren", + "KB Home", + "Brunswick", + "Robert Half", + "PENN Entertainment", + "NetApp", + "Organon", + "Petco Health and Wellness", + "Regal Rexnord", + "Resideo Technologies", + "Camping World Holdings", + "Huntsman", + "Victoria's Secret", + "Levi Strauss", + "Roper Technologies", + "Academy Sports and Outdoors", + "Meritage Homes", + "American Axle & Manufacturing", + "Fortive", + "Nasdaq", + "Broadridge Financial Solutions", + "Spirit AeroSystems Holdings", + "Warner Music Group", + "Chemours", + "ADT", + "Edwards Lifesciences", + "First American Financial", + "Hanover Insurance Group", + "Silgan Holdings", + "Endeavor Group Holdings", + "Moody's", + "Coterra Energy", + "Gartner", + "Under Armour", + "Ingles Markets", + "Church & Dwight", + "Old Dominion Freight Line", + "U-Haul Holding", + "Oscar Health", + "Arista Networks", + "Synopsys", + "Harley-Davidson", + "Frontier Communications", + "Primoris Services", + "Carlisle", + "Simon Property Group", + "Hanesbrands", + "Anywhere Real Estate", + "Teledyne Technologies", + "CME Group", + "Juniper Networks", + "Coty", + "Pool", + "Evergy", + "Marvell Technology", + "NiSource", + "SS&C Technologies Holdings", + "Schneider National", + "Autodesk", + "Sealed Air", + "Iron Mountain", + "Digital Realty Trust", + "Keysight Technologies", + "Globe Life", + "Parsons", + "Mattel", + "Southwest Gas Holdings", + "CUNA Mutual Group (TruStage)", + "Clean Harbors", + "Cornerstone Building Brands", + "Hubbell", + "Spirit Airlines", + "Lamb Weston Holdings", + "Take-Two Interactive Software", + "GMS", + "Penn Mutual Life Insurance", + "Landstar System", + "Host Hotels & Resorts", + "Fortinet", + "OneMain Holdings", + "GameStop", + "Equifax", + "American Eagle Outfitters", + "Comerica", + "Greif", + "Comfort Systems USA", + "TopBuild", + "Bread Financial Holdings", + "Coherent", + "Telephone & Data Systems", + "Stifel Financial", + "Urban Outfitters", + "Terex", + "Ryerson Holding", + "Snap-on", + "Flowers Foods", + "First Horizon", + "Paychex", + "Hasbro", + "Sentry Insurance Group", + "Ares Management", + "Lennox International", + "Peabody Energy", + "Kemper", + "Nexstar Media Group", + "TelevisaUnivision", + "Tempur Sealy International", + "TriNet Group", + "Worthington Enterprises", + "Maximus", + "Compass", + "Brink's", + "Kelly Services", + "Navient", + "Puget Energy", + "AMC Entertainment Holdings", + "Encompass Health", + "Skyworks Solutions", + "Timken", + "Liberty Energy", + "Marriott Vacations Worldwide", + "Leggett & Platt", + "Big Lots", + "Weis Markets", + "Pinnacle West Capital", + "EPAM Systems", + "Antero Resources", + "Bloomin' Brands", + "MDU Resources Group", + "MDC Holdings", + "Texas Roadhouse", + "Mercury General", + "Fortune Brands Innovations", + "Zions Bancorp.", + "JELD-WEN Holding", + "Snap", + "Zebra Technologies", + "Agilon Health", + "Toro", + "Zoom Video Communications", + "Tetra Tech", + "Public Storage", + "Illumina", + "Ventas", + "FMC", + "ArcBest", + "Domino's Pizza", + "ASGN", + "Rivian Automotive", + "Elanco Animal Health", + "Graham Holdings", + "Floor & Decor Holdings", + "Applied Industrial Technologies", + "PriceSmart", + "Lyft", + "Ciena", + "IAC", + "Country Financial", + "Western Union", + "Flowserve", + "Western Alliance Bancorp.", + "Option Care Health", + "SiteOne Landscape Supply", + "Topgolf Callaway Brands", + "eXp World Holdings", + "Abercrombie & Fitch", + "Atmos Energy", + "Catalent", + "Brown & Brown", + "GoDaddy", + "American Water Works", + "Selective Insurance Group", + "Brown-Forman", + "Advantage Solutions", + "ResMed", + "Splunk", + "Plexus", + "Hub Group", + "Lincoln Electric Holdings", + "NCR Atleos", + "Calumet", + "Dycom Industries", + "Valmont Industries", + "ATI", + "Twilio", + "CNO Financial Group", + "Patterson-UTI Energy", + "Brinker International", + "Charles River Laboratories International", + "Hyster-Yale", + "Brighthouse Financial", + "Service Corp. International", + "CrossAmerica Partners", + "Cadence", + "MillerKnoll", + "Realty Income", + "Middleby", + "M/I Homes", + "Hologic", + "Alliant Energy", + "Generac Holdings", + "MSC Industrial Direct", + "Rocket Companies", + "Upbound Group", + "East West Bancorp", + "Hilton Grand Vacations", + "Grocery Outlet Holding", + "Dentsply Sirona", + "V2X", + "Crocs", + "Spectrum Brands Holdings", + "Visteon", + "Acuity Brands", + "Greenbrier", + "Webster Financial", + "Cabot", + "RXO", + "Chord Energy", + "Popular", + "Garrett Motion", + "Tutor Perini", + "Copart", + "Toast", + "Align Technology", + "A.O. Smith", + "TransUnion", + "NCR Voyix", + "Akamai Technologies", + "Trimble", + "Medical Mutual of Ohio", + "AMN Healthcare Services", + "Mettler-Toledo International", + "ScanSource", + "LCI Industries", + "Cboe Global Markets", + "Diebold Nixdorf", + "ChampionX", + "Corpay", + "iHeartMedia", + "Travel + Leisure", + "Dream Finders Homes", + "Boyd Gaming", + "NLV Financial", + "Ensign Group", + "Sally Beauty Holdings", + "Conduent", + "Sylvamo", + "Tri Pointe Homes", + "EnerSys", + "Incyte", + "Century Communities", + "Rithm Capital", + "Euronet Worldwide", + "Hawaiian Electric Industries", + "DraftKings", + "IDEXX Laboratories", + "MYR Group", + "Deckers Outdoor", + "DexCom", + "MKS Instruments", + "VICI Properties", + "Cooper Cos.", + "Frontier Group Holdings", + "Qorvo", + "Five Below", + "TreeHouse Foods", + "Scotts Miracle-Gro", + "Atlassian", + "Entegris", + "Atkore", + "H.B. Fuller", + "Granite Construction", + "Winnebago Industries", + "FTI Consulting", + "AptarGroup", + "Columbia Sportswear", + "Roku", + "Civitas Resources", + "H&R Block", + "Alpha Metallurgical Resources", + "Patrick Industries", + "Murphy Oil", + "Synovus Financial", + "Cracker Barrel Old Country Store", + "Cheesecake Factory", + "CNX Resources", + "Chefs' Warehouse", + "Donaldson", + "Mueller Industries", + "MRC Global", + "Chart Industries", + "Alight", + "Vishay Intertechnology", + "Range Resources", + "Valley National Bancorp", + "Match Group", + "Gen Digital", + "Wintrust Financial", + "Moog", + "First Solar", + "Central Garden & Pet", + "Green Plains", + "Werner Enterprises", + "AppLovin", + "ITT", + "Herc Holdings", + "Gray Television", + "IDEX", + "Boston Properties", + "Pitney Bowes", + "Steelcase", + "Sun Communities", + "AdaptHealth", + "Vertex Energy", + "Genesis Energy", + "Jackson Financial", + "FirstCash Holdings", + "Cal-Maine Foods", + "Arch Resources", + "Avient", + "Cano Health", + "BlueLinx Holdings", + "Sinclair", + "BOK Financial", + "Permian Resources", + "PACS Group", + "Coinbase Global", + "Western Midstream Partners", + "Federated Mutual Insurance", + "G-III Apparel Group", + "Vontier", + "Matson", + "Kirby", + "Kaiser Aluminum", + "Vista Outdoor", + "Designer Brands", + "Rollins", + "Advanced Drainage Systems", + "Cinemark Holdings", + "CrowdStrike", + "Pinterest", + "Instacart", + "Hillenbrand", + "Allison Transmission Holdings", + "RH", + "Brookdale Senior Living", + "QuidelOrtho", + "Trinity Industries", + "Teleflex", + "HEICO", + "Bruker", + "Carlyle Group", + "Energizer Holdings", + "Quad/Graphics", + "Rackspace Technology", + "Waters", + "West Pharmaceutical Services", + "Carter's", + "NeueHealth", + "SkyWest", + "Acadia Healthcare", + "Revvity", + "Portland General Electric", + "Woodward", + "TEGNA", + "Sabre", + "Light & Wonder", + "Vail Resorts", + "Radius Recycling", + "PennyMac Financial Services", + "Saia", + "Equity Residential", + "Helmerich & Payne", + "Korn Ferry", + "PC Connection", + "Curtiss-Wright", + "SLM", + "Benchmark Electronics", + "American National Group", + "Pure Storage", + "Knife River", + "Caleres", + "BrightView Holdings", + "Cooper-Standard Holdings", + "Primerica", + "F5", + "Matador Resources", + "Snowflake", + "Viasat", + "California Resources", + "Peloton Interactive", + "Roblox", + "Installed Building Products", + "First National of Nebraska", + "Guess", + "ESAB", + "Amica Mutual Insurance", + "AvalonBay Communities", + "DocuSign", + "Titan Machinery", + "ModivCare", + "Hovnanian Enterprises", + "Etsy", + "Adams Resources & Energy", + "Surgery Partners", + "Columbia Banking System", + "Verisk", + "MasterBrand", + "Hawaiian Holdings", + "AMC Networks", + "SBA Communications", + "Americold Realty Trust", + "NewMarket", + "Park Hotels & Resorts", + "Alexandria Real Estate Equities", + "Griffon", + "Air Lease", + "UL Solutions", + "Teradyne", + "OGE Energy", + "Bio-Rad Laboratories", + "Spire", + "Gannett", + "Stericycle", + "Cullen/Frost Bankers", + "Extra Space Storage", + "REV Group", + "ProFrac Holding", + "Nordson", + "Summit Materials", + "Valvoline", + "Kontoor Brands", + "Skyline Champion", + "Teladoc Health", + "Louisiana-Pacific", + "Universal", + "Consol Energy", + "Encore Wire", + "Playtika Holding", + "Alliance Resource Partners", + "Envista Holdings", + "Shift4 Payments", + "Talen Energy", + "Carpenter Technology", + "WEX", + "Old National Bancorp", + "AGNC Investment", + "Wabash National", + "MSCI", + "Stagwell", + "Mativ Holdings", + "Belden", + "Allegiant Travel", + "Dropbox", + "Exact Sciences", + "BWX Technologies", + "Clear Channel Outdoor Hldgs.", +] as const; diff --git a/packages/consts/src/forms/countries.ts b/packages/consts/src/forms/countries.ts new file mode 100644 index 000000000..ee42d7bdd --- /dev/null +++ b/packages/consts/src/forms/countries.ts @@ -0,0 +1,252 @@ +// https://unstats.un.org/unsd/methodology/m49/overview/ +export const COUNTRIES = [ + "United States of America", + "Afghanistan", + "Albania", + "Algeria", + "American Samoa", + "Andorra", + "Angola", + "Anguilla", + "Antarctica", + "Antigua and Barbuda", + "Argentina", + "Armenia", + "Aruba", + "Australia", + "Austria", + "Azerbaijan", + "Bahamas", + "Bahrain", + "Bangladesh", + "Barbados", + "Belarus", + "Belgium", + "Belize", + "Benin", + "Bermuda", + "Bhutan", + "Bolivia (Plurinational State of)", + "Bonaire, Sint Eustatius and Saba", + "Bosnia and Herzegovina", + "Botswana", + "Bouvet Island", + "Brazil", + "British Indian Ocean Territory", + "British Virgin Islands", + "Brunei Darussalam", + "Bulgaria", + "Burkina Faso", + "Burundi", + "Cabo Verde", + "Cambodia", + "Cameroon", + "Canada", + "Cayman Islands", + "Central African Republic", + "Chad", + "Chile", + "China", + "Christmas Island", + "Cocos (Keeling) Islands", + "Colombia", + "Comoros", + "Congo", + "Cook Islands", + "Costa Rica", + "Croatia", + "Cuba", + "Curaçao", + "Cyprus", + "Czechia", + "Côte d’Ivoire", + "Democratic People's Republic of Korea", + "Democratic Republic of the Congo", + "Denmark", + "Djibouti", + "Dominica", + "Dominican Republic", + "Ecuador", + "Egypt", + "El Salvador", + "Equatorial Guinea", + "Eritrea", + "Estonia", + "Eswatini", + "Ethiopia", + "Falkland Islands (Malvinas)", + "Faroe Islands", + "Fiji", + "Finland", + "France", + "French Guiana", + "French Polynesia", + "French Southern Territories", + "Gabon", + "Gambia", + "Georgia", + "Germany", + "Ghana", + "Gibraltar", + "Greece", + "Greenland", + "Grenada", + "Guadeloupe", + "Guam", + "Guatemala", + "Guernsey", + "Guinea", + "Guinea-Bissau", + "Guyana", + "Haiti", + "Heard Island and McDonald Islands", + "Holy See", + "Honduras", + "Hong Kong", + "Hungary", + "Iceland", + "India", + "Indonesia", + "Iran (Islamic Republic of)", + "Iraq", + "Ireland", + "Isle of Man", + "Israel", + "Italy", + "Jamaica", + "Japan", + "Jersey", + "Jordan", + "Kazakhstan", + "Kenya", + "Kiribati", + "Kuwait", + "Kyrgyzstan", + "Lao People's Democratic Republic", + "Latvia", + "Lebanon", + "Lesotho", + "Liberia", + "Libya", + "Liechtenstein", + "Lithuania", + "Luxembourg", + "Macao", + "Madagascar", + "Malawi", + "Malaysia", + "Maldives", + "Mali", + "Malta", + "Marshall Islands", + "Martinique", + "Mauritania", + "Mauritius", + "Mayotte", + "Mexico", + "Micronesia (Federated States of)", + "Monaco", + "Mongolia", + "Montenegro", + "Montserrat", + "Morocco", + "Mozambique", + "Myanmar", + "Namibia", + "Nauru", + "Nepal", + "Netherlands (Kingdom of the)", + "New Caledonia", + "New Zealand", + "Nicaragua", + "Niger", + "Nigeria", + "Niue", + "Norfolk Island", + "North Macedonia", + "Northern Mariana Islands", + "Norway", + "Oman", + "Pakistan", + "Palau", + "Panama", + "Papua New Guinea", + "Paraguay", + "Peru", + "Philippines", + "Pitcairn", + "Poland", + "Portugal", + "Puerto Rico", + "Qatar", + "Republic of Korea", + "Republic of Moldova", + "Romania", + "Russian Federation", + "Rwanda", + "Réunion", + "Saint Barthélemy", + "Saint Helena", + "Saint Kitts and Nevis", + "Saint Lucia", + "Saint Martin (French Part)", + "Saint Pierre and Miquelon", + "Saint Vincent and the Grenadines", + "Samoa", + "San Marino", + "Sao Tome and Principe", + "Saudi Arabia", + "Senegal", + "Serbia", + "Seychelles", + "Sierra Leone", + "Singapore", + "Sint Maarten (Dutch part)", + "Slovakia", + "Slovenia", + "Solomon Islands", + "Somalia", + "South Africa", + "South Georgia and the South Sandwich Islands", + "South Sudan", + "Spain", + "Sri Lanka", + "State of Palestine", + "Sudan", + "Suriname", + "Svalbard and Jan Mayen Islands", + "Sweden", + "Switzerland", + "Syrian Arab Republic", + "Taiwan, Province of China", + "Tajikistan", + "Thailand", + "Timor-Leste", + "Togo", + "Tokelau", + "Tonga", + "Trinidad and Tobago", + "Tunisia", + "Turkmenistan", + "Turks and Caicos Islands", + "Tuvalu", + "Türkiye", + "Uganda", + "Ukraine", + "United Arab Emirates", + "United Kingdom of Great Britain and Northern Ireland", + "United Republic of Tanzania", + "United States Minor Outlying Islands", + "United States Virgin Islands", + "Uruguay", + "Uzbekistan", + "Vanuatu", + "Venezuela (Bolivarian Republic of)", + "Viet Nam", + "Wallis and Futuna Islands", + "Western Sahara", + "Yemen", + "Zambia", + "Zimbabwe", + "Åland Islands", +] as const; diff --git a/packages/consts/src/forms/index.ts b/packages/consts/src/forms/index.ts new file mode 100644 index 000000000..7b59f7a31 --- /dev/null +++ b/packages/consts/src/forms/index.ts @@ -0,0 +1,344 @@ +import * as z from "zod"; + +import { COMPANIES } from "./companies"; +import { COUNTRIES } from "./countries"; +import { SCHOOLS } from "./schools"; + +export { COMPANIES, COUNTRIES, SCHOOLS }; + +export const LEVELS_OF_STUDY = [ + "Less than Secondary / High School", + "Secondary / High School", + "Undergraduate University (2 year - community college or similar)", + "Undergraduate University (3+ year)", + "Graduate University (Masters, Professional, Doctoral, etc)", + "Code School / Bootcamp", + "Other Vocational / Trade Program or Apprenticeship", + "Post Doctorate", + "Other", + "I’m not currently a student", + "Prefer not to answer", +] as const; + +export const ALLERGIES = [ + "Milk", + "Eggs", + "Fish", + "Crustacean Shellfish", + "Tree Nuts", + "Peanuts", + "Wheat", + "Soybeans", + "Halal", + "Kosher", + "Vegetarian", + "Vegan", +] as const; + +export const MAJORS = [ + "Computer Science", + "Information Technology", + "Software Engineering", + "Computer Engineering", + "Digital Media", + "Business", + "Accounting", + "Aerospace Engineering", + "Anthropology", + "Architecture", + "Art", + "Astronomy", + "Biochemistry", + "Biology", + "Biomedical Engineering", + "Chemical Engineering", + "Chemistry", + "Civil Engineering", + "Communication", + "Criminal Justice", + "Data Science", + "Design", + "Economics", + "Education", + "Electrical Engineering", + "English", + "Environmental Science", + "Finance", + "Game Design", + "Geography", + "Geology", + "Graphic Design", + "Health Sciences", + "History", + "Hospitality Management", + "Human Resources", + "Industrial Engineering", + "International Relations", + "Journalism", + "Languages", + "Law", + "Linguistics", + "Management", + "Marketing", + "Mathematics", + "Mechanical Engineering", + "Medicine", + "Music", + "Nursing", + "Philosophy", + "Physics", + "Political Science", + "Pre-Med", + "Pre-Law", + "Psychology", + "Public Administration", + "Public Health", + "Religious Studies", + "Social Work", + "Sociology", + "Statistics", + "Theater", + "Urban Planning", + "Veterinary Science", + "Interdisciplinary Studies", + "Other", +] as const; + +export type Major = (typeof MAJORS)[number]; + +export const SHORT_LEVELS_OF_STUDY = [ + "Undergraduate University (2 year)", + "Graduate University (Masters/PhD)", + "Vocational/Trade School", +] as const; + +export const GENDERS = [ + "Man", + "Woman", + "Non-binary", + "Prefer to self-describe", + "Prefer not to answer", +] as const; + +export const RACES_OR_ETHNICITIES = [ + "White", + "Black or African American", + "Hispanic / Latino / Spanish Origin", + "Asian", + "Native Hawaiian or Other Pacific Islander", + "Native American or Alaskan Native", + "Middle Eastern", + "Prefer not to answer", + "Other", +] as const; + +export const SHORT_RACES_AND_ETHNICITIES = [ + "Native Hawaiian/Pacific Islander", + "Hispanic/Latino", + "Native American/Alaskan Native", +] as const; + +export const QuestionValidator = z.object({ + question: z.string(), + image: z.string().url().optional(), + type: z.enum([ + "SHORT_ANSWER", + "PARAGRAPH", + "MULTIPLE_CHOICE", + "CHECKBOXES", + "DROPDOWN", + "LINEAR_SCALE", + "DATE", + "TIME", + "EMAIL", + "NUMBER", + "PHONE", + "FILE_UPLOAD", + "BOOLEAN", + "LINK", + ]), + options: z.array(z.string()).optional(), + optionsConst: z.string().optional(), + optional: z.boolean().optional(), + allowOther: z.boolean().optional(), + min: z.number().optional(), + max: z.number().optional(), + order: z.number().optional(), +}); + +export const InstructionValidator = z.object({ + title: z.string().max(200), + content: z.string().max(2000).optional(), + imageUrl: z.string().url().optional(), + videoUrl: z.string().url().optional(), + imageObjectName: z.string().optional(), + videoObjectName: z.string().optional(), + order: z.number().optional(), +}); + +export const FormSchemaValidator = z.object({ + banner: z.string().url().optional(), + name: z.string().max(200), + description: z.string().max(500), + questions: z.array(QuestionValidator), + instructions: z.array(InstructionValidator).optional(), +}); + +export type FormType = z.infer; +export type InstructionValidatorType = z.infer; + +type QuestionValidatorType = z.infer; +export type ValidatorOptions = Omit; + +export type QuestionsType = z.infer["type"]; + +export const AVAILABLE_DROPDOWN_CONSTANTS = { + LEVELS_OF_STUDY: "Levels of Study", + ALLERGIES: "Allergies", + MAJORS: "Majors", + GENDERS: "Genders", + RACES_OR_ETHNICITIES: "Races or Ethnicities", + COUNTRIES: "Countries", + SCHOOLS: "Schools", + COMPANIES: "Companies", + SHIRT_SIZES: "Shirt Sizes", + EVENT_FEEDBACK_HEARD: "Event Feedback - How You Heard", + SHORT_LEVELS_OF_STUDY: "Short Levels of Study", + SHORT_RACES_AND_ETHNICITIES: "Short Races and Ethnicities", +} as const; + +export type DropdownConstantKey = keyof typeof AVAILABLE_DROPDOWN_CONSTANTS; + +export const SHIRT_SIZES = ["XS", "S", "M", "L", "XL", "2XL", "3XL"] as const; + +export const EVENT_FEEDBACK_HEARD = [ + "Discord", + "Instagram", + "KnightConnect", + "Word of Mouth", + "CECS Emailing List", + "Reddit", + "LinkedIn", + "From Class Presentation", + "From Another Club", +] as const; + +export function getDropdownOptionsFromConst( + constName: string, +): readonly string[] { + switch (constName) { + case "LEVELS_OF_STUDY": + return LEVELS_OF_STUDY; + case "ALLERGIES": + return ALLERGIES; + case "MAJORS": + return MAJORS; + case "GENDERS": + return GENDERS; + case "RACES_OR_ETHNICITIES": + return RACES_OR_ETHNICITIES; + case "COUNTRIES": + return COUNTRIES; + case "SCHOOLS": + return SCHOOLS; + case "COMPANIES": + return COMPANIES; + case "SHIRT_SIZES": + return SHIRT_SIZES; + case "EVENT_FEEDBACK_HEARD": + return EVENT_FEEDBACK_HEARD; + case "SHORT_LEVELS_OF_STUDY": + return SHORT_LEVELS_OF_STUDY; + case "SHORT_RACES_AND_ETHNICITIES": + return SHORT_RACES_AND_ETHNICITIES; + default: + return []; + } +} + +export const FORM_QUESTION_TYPES = [ + { value: "SHORT_ANSWER", label: "Short answer" }, + { value: "PARAGRAPH", label: "Paragraph" }, + { value: "MULTIPLE_CHOICE", label: "Multiple choice" }, + { value: "CHECKBOXES", label: "Checkboxes" }, + { value: "DROPDOWN", label: "Dropdown" }, + { value: "FILE_UPLOAD", label: "File upload" }, + { value: "LINEAR_SCALE", label: "Linear scale" }, + { value: "DATE", label: "Date" }, + { value: "TIME", label: "Time" }, + { value: "EMAIL", label: "Email" }, + { value: "NUMBER", label: "Number" }, + { value: "PHONE", label: "Phone" }, + { value: "BOOLEAN", label: "Boolean (Yes/No)" }, + { value: "LINK", label: "Link (URL)" }, +] as const; + +export const HACKATHON_APPLICATION_STATES = [ + "withdrawn", + "pending", + "accepted", + "waitlisted", + "checkedin", + "confirmed", + "denied", +] as const; + +export const SPONSOR_TIERS = ["gold", "silver", "bronze", "other"] as const; + +export const ADMIN_PIE_CHART_COLORS: readonly string[] = [ + "#f72585", + "#b5179e", + "#7209b7", + "#3a0ca3", + "#4361ee", + "#4895ef", + "#4cc9f0", + "#560bad", + "#480ca8", +] as const; + +export const WEEKDAY_ORDER: string[] = [ + "Mon", + "Tues", + "Wed", + "Thurs", + "Fri", + "Sat/Sun", +] as const; + +export const RANKING_STYLES: string[] = [ + "md:text-lg lg:text-lg font-bold text-yellow-500", + "md:text-lg lg:text-lg font-semibold text-gray-400", + "md:text-lg lg:text-lg font-medium text-orange-500", +]; + +export const DEFAULT_COLOR = "#ffffff"; +export const DEFAULT_EMAIL_QUEUE_CRON_SCHEDULE = "0 * * * *"; + +export const SEMESTER_START_DATES = { + spring: { + month: 0, + day: 1, // first day of January + }, + summer: { + month: 4, + day: 1, // first day of May + }, + fall: { + month: 7, + day: 15, // middle of August + }, +} as const; + +export const ALL_DATES_RANGE_UNIX = { + start: -8640000000000000, + end: 8640000000000000, +} as const; + +export interface Semester { + name: string; + startDate: Date; + endDate: Date; +} + +export const DEVPOST_TEAM_MEMBER_EMAIL_OFFSET = 3; diff --git a/packages/consts/src/knight-hacks.ts b/packages/consts/src/forms/schools.ts similarity index 80% rename from packages/consts/src/knight-hacks.ts rename to packages/consts/src/forms/schools.ts index 1da2e1581..f86c60f31 100644 --- a/packages/consts/src/knight-hacks.ts +++ b/packages/consts/src/forms/schools.ts @@ -1,777 +1,3 @@ -import * as z from "zod"; - -import type { HackerClass } from "../../db/src/schemas/knight-hacks"; - -export const LEVELS_OF_STUDY = [ - "Less than Secondary / High School", - "Secondary / High School", - "Undergraduate University (2 year - community college or similar)", - "Undergraduate University (3+ year)", - "Graduate University (Masters, Professional, Doctoral, etc)", - "Code School / Bootcamp", - "Other Vocational / Trade Program or Apprenticeship", - "Post Doctorate", - "Other", - "I’m not currently a student", - "Prefer not to answer", -] as const; - -export const ALLERGIES = [ - "Milk", - "Eggs", - "Fish", - "Crustacean Shellfish", - "Tree Nuts", - "Peanuts", - "Wheat", - "Soybeans", - "Halal", - "Kosher", - "Vegetarian", - "Vegan", -] as const; - -export const MAJORS = [ - "Computer Science", - "Information Technology", - "Software Engineering", - "Computer Engineering", - "Digital Media", - "Business", - "Accounting", - "Aerospace Engineering", - "Anthropology", - "Architecture", - "Art", - "Astronomy", - "Biochemistry", - "Biology", - "Biomedical Engineering", - "Chemical Engineering", - "Chemistry", - "Civil Engineering", - "Communication", - "Criminal Justice", - "Data Science", - "Design", - "Economics", - "Education", - "Electrical Engineering", - "English", - "Environmental Science", - "Finance", - "Game Design", - "Geography", - "Geology", - "Graphic Design", - "Health Sciences", - "History", - "Hospitality Management", - "Human Resources", - "Industrial Engineering", - "International Relations", - "Journalism", - "Languages", - "Law", - "Linguistics", - "Management", - "Marketing", - "Mathematics", - "Mechanical Engineering", - "Medicine", - "Music", - "Nursing", - "Philosophy", - "Physics", - "Political Science", - "Pre-Med", - "Pre-Law", - "Psychology", - "Public Administration", - "Public Health", - "Religious Studies", - "Social Work", - "Sociology", - "Statistics", - "Theater", - "Urban Planning", - "Veterinary Science", - "Interdisciplinary Studies", - "Other", -] as const; - -export type Major = (typeof MAJORS)[number]; - -export const SHORT_LEVELS_OF_STUDY = [ - "Undergraduate University (2 year)", - "Graduate University (Masters/PhD)", - "Vocational/Trade School", -] as const; - -export const OFFICERS = [ - { - name: "Dylan Vidal", - position: "President", - image: "/officers/dylan.png", - linkedin: "https://www.linkedin.com/in/dylanvidal1204/", - major: "Computer Science", - }, - { - name: "Leonard Gofman", - position: "Vice President", - image: "/officers/leo.png", - linkedin: "https://www.linkedin.com/in/leonard-gofman-208578236/", - major: "Computer Science", - }, - { - name: "Adrian Osorio", - position: "Treasurer", - image: "/officers/adrian.png", - linkedin: "https://www.linkedin.com/in/adrianosoriob/", - major: "Computer Engineering", - }, - { - name: "Lewin Bobda", - position: "Dev Lead", - image: "/officers/bobda.png", - linkedin: "https://www.linkedin.com/in/lewin-bobda-08ba2325a/", - major: "Computer Science", - }, - { - name: "Daniel Efres", - position: "Secretary", - image: "/officers/daniel.png", - linkedin: "https://www.linkedin.com/in/daniel-efres/", - major: "Computer Science", - }, - { - name: "Richard Phillips", - position: "Hack Lead", - image: "/officers/ricky.png", - linkedin: "https://www.linkedin.com/in/rphillipscs/", - major: "Computer Science", - }, -] as const; - -export type Officer = (typeof OFFICERS)[number]; - -export const EVENT_TAGS = [ - "GBM", - "Social", - "Kickstart", - "Project Launch", - "Hello World", - "Sponsorship", - "Tech Exploration", - "Class Support", - "Workshop", - "OPS", - "Collabs", - "Check-in", - "Merch", - "Food", - "Ceremony", - "CAREER-FAIR", - "RSO-FAIR", -] as const; - -export const EVENT_FEEDBACK_HEARD = [ - "Discord", - "Instagram", - "KnightConnect", - "Word of Mouth", - "CECS Emailing List", - "Reddit", - "LinkedIn", - "From Class Presentation", - "From Another Club", -] as const; - -export const EVENT_FEEDBACK_SIMILAR_EVENT = ["Yes", "No"] as const; - -type EventTag = (typeof EVENT_TAGS)[number]; - -export const EVENT_POINTS: Record = { - GBM: 35, - Social: 25, - Kickstart: 25, - "Project Launch": 25, - "Hello World": 25, - Sponsorship: 50, - "Tech Exploration": 25, - "Class Support": 25, - Workshop: 25, - OPS: 20, - Collabs: 40, - "Check-in": 5, - Merch: 5, - Food: 5, - Ceremony: 50, - "CAREER-FAIR": 100, - "RSO-FAIR": 50, -} as const; - -export const KNIGHTHACKS_S3_BUCKET_REGION = "us-east-1"; -export const KNIGHTHACKS_MAX_RESUME_SIZE = 5 * 1000000; // 5MB -export const FORM_ASSETS_BUCKET = "form-assets"; - -export const PRESIGNED_URL_EXPIRY = 7 * 24 * 60 * 60; // 7 days - -export const KNIGHTHACKS_MAX_PROFILE_PICTURE_SIZE = 2 * 1024 * 1024; // 2MB -export const ALLOWED_PROFILE_PICTURE_TYPES = [ - "image/jpeg", - "image/png", - "image/gif", - "image/webp", -]; -export const ALLOWED_PROFILE_PICTURE_EXTENSIONS = [ - "jpg", - "jpeg", - "png", - "gif", - "webp", -]; - -export const TERM_TO_DATE = { - Spring: { month: 4, day: 2 }, // May 2 - Summer: { month: 7, day: 6 }, // Aug 6 - Fall: { month: 11, day: 10 }, // Dec 10 -} as const; -export type GradTerm = keyof typeof TERM_TO_DATE; - -export const GUILD_TAG_OPTIONS = ["alumni", "current"] as const; -export type GuildTag = (typeof GUILD_TAG_OPTIONS)[number]; - -export const MINIO_ENDPOINT = "minio-g0soogg4gs8gwcggw4ococok.knighthacks.org"; -export const BUCKET_NAME = "knight-hacks-qr"; -export const QR_CONTENT_TYPE = "image/png"; -export const QR_PATHNAME = "/knight-hacks-qr/**"; - -export const KNIGHTHACKS_MEMBERSHIP_PRICE = 2500; - -export const PROD_DISCORD_ADMIN_ROLE_ID = "1319413082258411652"; -export const DEV_DISCORD_ADMIN_ROLE_ID = "1321955700540309645"; - -export const PROD_DISCORD_VOLUNTEER_ROLE_ID = "1415505872360312974"; -export const DEV_DISCORD_VOLUNTEER_ROLE_ID = "1426947077514203279"; - -// TODO: add DEV_ALUMNI_ROLE_ID -export const PROD_ALUMNI_ROLE_ID = "486629512101232661"; - -export interface PermissionDataObj { - idx: number; - name: string; - desc: string; -} - -export const PERMISSION_DATA: Record = { - IS_OFFICER: { - idx: 0, - name: "Is Officer", - desc: "Grants access to sensitive club officer pages.", - }, - IS_JUDGE: { - idx: 1, - name: "Is Judge", - desc: "Grants access to the judging system.", - }, - READ_MEMBERS: { - idx: 2, - name: "Read Members", - desc: "Grants access to the list of club members.", - }, - EDIT_MEMBERS: { - idx: 3, - name: "Edit Members", - desc: "Allows editing member data, including deletion.", - }, - READ_HACKERS: { - idx: 4, - name: "Read Hackers", - desc: "Grants access to the list of hackers, and their hackathons.", - }, - EDIT_HACKERS: { - idx: 5, - name: "Edit Hackers", - desc: "Allows editing hacker data, including approval, rejection, deletion, etc.", - }, - READ_CLUB_DATA: { - idx: 6, - name: "Read Club Data", - desc: "Grants access to club statistics, such as demographics.", - }, - READ_HACK_DATA: { - idx: 7, - name: "Read Hackathon Data", - desc: "Grants access to hackathon statistics, such as demographics.", - }, - READ_CLUB_EVENT: { - idx: 8, - name: "Read Club Events", - desc: "Grants access to club event data, such as attendance.", - }, - EDIT_CLUB_EVENT: { - idx: 9, - name: "Edit Club Events", - desc: "Allows creating, editing, or deleting club events.", - }, - CHECKIN_CLUB_EVENT: { - idx: 10, - name: "Club Event Check-in", - desc: "Allows the user to check members into club events.", - }, - READ_HACK_EVENT: { - idx: 11, - name: "Read Hackathon Events", - desc: "Grants access to hackathon event data, such as attendance.", - }, - EDIT_HACK_EVENT: { - idx: 12, - name: "Edit Hackathon Events", - desc: "Allows creating, editing, or deleting hackathon events.", - }, - CHECKIN_HACK_EVENT: { - idx: 13, - name: "Hackathon Event Check-in", - desc: "Allows the user to check hackers into hackathon events, including the primary check-in.", - }, - EMAIL_PORTAL: { - idx: 14, - name: "Email Portal", - desc: "Grants access to the email queue portal.", - }, - READ_FORMS: { - idx: 15, - name: "Read Forms", - desc: "Grants access to created forms, but not their responses.", - }, - READ_FORM_RESPONSES: { - idx: 16, - name: "Read Form Responses", - desc: "Grants access to form responses.", - }, - EDIT_FORMS: { - idx: 17, - name: "Edit Forms", - desc: "Allows creating, editing, or deleting forms.", - }, - ASSIGN_ROLES: { - idx: 18, - name: "Assign Roles", - desc: "Allows assigning or removing roles to Blade users.", - }, - CONFIGURE_ROLES: { - idx: 19, - name: "Configure Roles", - desc: "Allows creating, editing, or deleting roles.", - }, -} as const satisfies Record; - -export const PERMISSIONS = Object.fromEntries( - Object.entries(PERMISSION_DATA).map(([key, value]) => [key, value.idx]), -) as { - [K in keyof typeof PERMISSION_DATA]: (typeof PERMISSION_DATA)[K]["idx"]; -}; - -export type PermissionKey = keyof typeof PERMISSION_DATA; -export type PermissionIndex = (typeof PERMISSION_DATA)[PermissionKey]["idx"]; - -export const PROD_KNIGHTHACKS_GUILD_ID = "486628710443778071"; -export const DEV_KNIGHTHACKS_GUILD_ID = "1151877367434850364"; - -export const PROD_KNIGHTHACKS_LOG_CHANNEL = "1324885515412963531"; -export const DEV_KNIGHTHACKS_LOG_CHANNEL = "1284582557689843785"; - -export const PROD_DISCORD_ROLE_KNIGHT_HACKS_8 = "1408025502119231498"; -export const DEV_DISCORD_ROLE_KNIGHT_HACKS_8 = "1420819573816692857"; - -export const PROD_DISCORD_ROLE_OPERATORS = "1415702220825038879"; -export const DEV_DISCORD_ROLE_OPERATORS = "1420819261223600239"; - -export const PROD_DISCORD_ROLE_MACHINIST = "1415702276433248406"; -export const DEV_DISCORD_ROLE_MACHINIST = "1420819223797698683"; - -export const PROD_DISCORD_ROLE_SENTINELS = "1415702308494250136"; -export const DEV_DISCORD_ROLE_SENTINELS = "1420819277279137892"; - -export const PROD_DISCORD_ROLE_HARBINGER = "1415702341214011392"; -export const DEV_DISCORD_ROLE_HARBINGER = "1420819326075801670"; - -export const PROD_DISCORD_ROLE_MONSTOLOGIST = "1415702361653121044"; -export const DEV_DISCORD_ROLE_MONSTOLOGIST = "1420819295759237222"; - -export const PROD_DISCORD_ROLE_ALCHEMIST = "1415702383274491934"; -export const DEV_DISCORD_ROLE_ALCHEMIST = "1420819309965611140"; - -export const PROD_DISCORD_SUPERADMIN = "486629374758748180"; -export const DEV_DISCORD_SUPERADMIN = "1246637685011906560"; - -export const IS_PROD = process.env.NODE_ENV === "production"; - -export const KH_EVENT_ROLE_ID = IS_PROD - ? PROD_DISCORD_ROLE_KNIGHT_HACKS_8 - : DEV_DISCORD_ROLE_KNIGHT_HACKS_8; -export type AssignableHackerClass = Exclude; - -export const CLASS_ROLE_ID: Record = { - Operator: IS_PROD ? PROD_DISCORD_ROLE_OPERATORS : DEV_DISCORD_ROLE_OPERATORS, - Mechanist: IS_PROD ? PROD_DISCORD_ROLE_MACHINIST : DEV_DISCORD_ROLE_MACHINIST, - Sentinel: IS_PROD ? PROD_DISCORD_ROLE_SENTINELS : DEV_DISCORD_ROLE_SENTINELS, - Harbinger: IS_PROD ? PROD_DISCORD_ROLE_HARBINGER : DEV_DISCORD_ROLE_HARBINGER, - Monstologist: IS_PROD - ? PROD_DISCORD_ROLE_MONSTOLOGIST - : DEV_DISCORD_ROLE_MONSTOLOGIST, - Alchemist: IS_PROD ? PROD_DISCORD_ROLE_ALCHEMIST : DEV_DISCORD_ROLE_ALCHEMIST, -} as const satisfies Record; - -export const MEMBER_PROFILE_ICON_SIZE = 24; - -export const EVENT_FEEDBACK_SLIDER_MINIMUM = 1; - -export const EVENT_FEEDBACK_SLIDER_MAXIMUM = 10; - -export const EVENT_FEEDBACK_SLIDER_STEP = 1; - -export const EVENT_FEEDBACK_SLIDER_VALUE = 5; - -export const EVENT_FEEDBACK_TEXT_ROWS = 4; - -export const EVENT_FEEDBACK_POINTS_INCREMENT = 10; - -export const SPONSOR_VIDEO_LINK = - "https://www.youtube.com/embed/OU1q02v1Vrw?si=dyHSQCmxzcau7-mF"; - -export const DISCORD_EVENT_TYPE = 3; - -export const DISCORD_EVENT_PRIVACY_LEVEL = 2; - -export const CALENDAR_TIME_ZONE = "America/New_York"; - -export const DUES_PAYMENT = 25; - -export const CLEAR_DUES_MESSAGE = - "I am aware of the consequences regarding this action if it were by mistake. I am absolutely sure that I want to clear all dues."; - -export const PROD_GOOGLE_CALENDAR_ID = - "c_0b9df2b0062a5d711fc16060ff3286ef404b174bfafc4cbdd4e3009e91536e94@group.calendar.google.com"; -export const DEV_GOOGLE_CALENDAR_ID = - "c_178118a9a25d9f278014b123b79b7e9caf9b29ac94bba3934237db00979e0f75@group.calendar.google.com"; - -export const GOOGLE_PERSONIFY_EMAIL = "dylan@knighthacks.org"; - -export const PERMANENT_DISCORD_INVITE = "https://discord.com/invite/Kv5g9vf"; - -export const USE_CAUTION = true; - -export type EventTagsColor = - | "GBM" - | "Social" - | "Kickstart" - | "Project Launch" - | "Hello World" - | "Sponsorship" - | "Tech Exploration" - | "Class Support" - | "Workshop" - | "OPS" - | "Hackathon" - | "Collabs" - | "Check-in" - | "Merch" - | "Food" - | "Ceremony" - | "CAREER-FAIR" - | "RSO-FAIR"; - -export const GENDERS = [ - "Man", - "Woman", - "Non-binary", - "Prefer to self-describe", - "Prefer not to answer", -] as const; - -export const RACES_OR_ETHNICITIES = [ - "White", - "Black or African American", - "Hispanic / Latino / Spanish Origin", - "Asian", - "Native Hawaiian or Other Pacific Islander", - "Native American or Alaskan Native", - "Middle Eastern", - "Prefer not to answer", - "Other", -] as const; - -export const SHORT_RACES_AND_ETHNICITIES = [ - "Native Hawaiian/Pacific Islander", - "Hispanic/Latino", - "Native American/Alaskan Native", -] as const; - -// https://unstats.un.org/unsd/methodology/m49/overview/ -export const COUNTRIES = [ - "United States of America", - "Afghanistan", - "Albania", - "Algeria", - "American Samoa", - "Andorra", - "Angola", - "Anguilla", - "Antarctica", - "Antigua and Barbuda", - "Argentina", - "Armenia", - "Aruba", - "Australia", - "Austria", - "Azerbaijan", - "Bahamas", - "Bahrain", - "Bangladesh", - "Barbados", - "Belarus", - "Belgium", - "Belize", - "Benin", - "Bermuda", - "Bhutan", - "Bolivia (Plurinational State of)", - "Bonaire, Sint Eustatius and Saba", - "Bosnia and Herzegovina", - "Botswana", - "Bouvet Island", - "Brazil", - "British Indian Ocean Territory", - "British Virgin Islands", - "Brunei Darussalam", - "Bulgaria", - "Burkina Faso", - "Burundi", - "Cabo Verde", - "Cambodia", - "Cameroon", - "Canada", - "Cayman Islands", - "Central African Republic", - "Chad", - "Chile", - "China", - "Christmas Island", - "Cocos (Keeling) Islands", - "Colombia", - "Comoros", - "Congo", - "Cook Islands", - "Costa Rica", - "Croatia", - "Cuba", - "Curaçao", - "Cyprus", - "Czechia", - "Côte d’Ivoire", - "Democratic People's Republic of Korea", - "Democratic Republic of the Congo", - "Denmark", - "Djibouti", - "Dominica", - "Dominican Republic", - "Ecuador", - "Egypt", - "El Salvador", - "Equatorial Guinea", - "Eritrea", - "Estonia", - "Eswatini", - "Ethiopia", - "Falkland Islands (Malvinas)", - "Faroe Islands", - "Fiji", - "Finland", - "France", - "French Guiana", - "French Polynesia", - "French Southern Territories", - "Gabon", - "Gambia", - "Georgia", - "Germany", - "Ghana", - "Gibraltar", - "Greece", - "Greenland", - "Grenada", - "Guadeloupe", - "Guam", - "Guatemala", - "Guernsey", - "Guinea", - "Guinea-Bissau", - "Guyana", - "Haiti", - "Heard Island and McDonald Islands", - "Holy See", - "Honduras", - "Hong Kong", - "Hungary", - "Iceland", - "India", - "Indonesia", - "Iran (Islamic Republic of)", - "Iraq", - "Ireland", - "Isle of Man", - "Israel", - "Italy", - "Jamaica", - "Japan", - "Jersey", - "Jordan", - "Kazakhstan", - "Kenya", - "Kiribati", - "Kuwait", - "Kyrgyzstan", - "Lao People's Democratic Republic", - "Latvia", - "Lebanon", - "Lesotho", - "Liberia", - "Libya", - "Liechtenstein", - "Lithuania", - "Luxembourg", - "Macao", - "Madagascar", - "Malawi", - "Malaysia", - "Maldives", - "Mali", - "Malta", - "Marshall Islands", - "Martinique", - "Mauritania", - "Mauritius", - "Mayotte", - "Mexico", - "Micronesia (Federated States of)", - "Monaco", - "Mongolia", - "Montenegro", - "Montserrat", - "Morocco", - "Mozambique", - "Myanmar", - "Namibia", - "Nauru", - "Nepal", - "Netherlands (Kingdom of the)", - "New Caledonia", - "New Zealand", - "Nicaragua", - "Niger", - "Nigeria", - "Niue", - "Norfolk Island", - "North Macedonia", - "Northern Mariana Islands", - "Norway", - "Oman", - "Pakistan", - "Palau", - "Panama", - "Papua New Guinea", - "Paraguay", - "Peru", - "Philippines", - "Pitcairn", - "Poland", - "Portugal", - "Puerto Rico", - "Qatar", - "Republic of Korea", - "Republic of Moldova", - "Romania", - "Russian Federation", - "Rwanda", - "Réunion", - "Saint Barthélemy", - "Saint Helena", - "Saint Kitts and Nevis", - "Saint Lucia", - "Saint Martin (French Part)", - "Saint Pierre and Miquelon", - "Saint Vincent and the Grenadines", - "Samoa", - "San Marino", - "Sao Tome and Principe", - "Saudi Arabia", - "Senegal", - "Serbia", - "Seychelles", - "Sierra Leone", - "Singapore", - "Sint Maarten (Dutch part)", - "Slovakia", - "Slovenia", - "Solomon Islands", - "Somalia", - "South Africa", - "South Georgia and the South Sandwich Islands", - "South Sudan", - "Spain", - "Sri Lanka", - "State of Palestine", - "Sudan", - "Suriname", - "Svalbard and Jan Mayen Islands", - "Sweden", - "Switzerland", - "Syrian Arab Republic", - "Taiwan, Province of China", - "Tajikistan", - "Thailand", - "Timor-Leste", - "Togo", - "Tokelau", - "Tonga", - "Trinidad and Tobago", - "Tunisia", - "Turkmenistan", - "Turks and Caicos Islands", - "Tuvalu", - "Türkiye", - "Uganda", - "Ukraine", - "United Arab Emirates", - "United Kingdom of Great Britain and Northern Ireland", - "United Republic of Tanzania", - "United States Minor Outlying Islands", - "United States Virgin Islands", - "Uruguay", - "Uzbekistan", - "Vanuatu", - "Venezuela (Bolivarian Republic of)", - "Viet Nam", - "Wallis and Futuna Islands", - "Western Sahara", - "Yemen", - "Zambia", - "Zimbabwe", - "Åland Islands", -] as const; - -export const HACKATHON_APPLICATION_STATES = [ - "withdrawn", - "pending", - "accepted", - "waitlisted", - "checkedin", - "confirmed", - "denied", -] as const; - export const SCHOOLS = [ "University of Central Florida", "University of South Florida", @@ -5646,1282 +4872,3 @@ export const SCHOOLS = [ "Lexington High School", "Marlboro High School", ] as const; - -export const COMPANIES = [ - "Walmart", - "Amazon", - "Apple", - "UnitedHealth Group", - "Berkshire Hathaway", - "CVS Health", - "Exxon Mobil", - "Alphabet", - "McKesson", - "Cencora", - "Costco Wholesale", - "JPMorgan Chase", - "Microsoft", - "Cardinal Health", - "Chevron", - "Cigna", - "Ford Motor", - "Bank of America", - "General Motors", - "Elevance Health", - "Citigroup", - "Centene", - "Home Depot", - "Marathon Petroleum", - "Kroger", - "Phillips 66", - "Fannie Mae", - "Walgreens Boots Alliance", - "Valero Energy", - "Meta Platforms", - "Verizon Communications", - "AT&T", - "Comcast", - "Wells Fargo", - "Goldman Sachs Group", - "Freddie Mac", - "Target", - "Humana", - "State Farm Insurance", - "Tesla", - "Morgan Stanley", - "Johnson & Johnson", - "Archer Daniels Midland", - "PepsiCo", - "United Parcel Service", - "FedEx", - "Walt Disney", - "Dell Technologies", - "Lowe's", - "Procter & Gamble", - "Energy Transfer", - "Boeing", - "Albertsons", - "Sysco", - "RTX", - "General Electric", - "Lockheed Martin", - "American Express", - "Caterpillar", - "MetLife", - "HCA Healthcare", - "Progressive", - "IBM", - "Deere", - "Nvidia", - "StoneX Group", - "Merck", - "ConocoPhillips", - "Pfizer", - "Delta Air Lines", - "TD Synnex", - "Publix Super Markets", - "Allstate", - "Cisco Systems", - "Nationwide", - "Charter Communications", - "AbbVie", - "New York Life Insurance", - "Intel", - "TJX", - "Prudential Financial", - "HP", - "United Airlines Holdings", - "Performance Food Group", - "Tyson Foods", - "American Airlines Group", - "Liberty Mutual Insurance Group", - "Nike", - "Oracle", - "Enterprise Products Partners", - "Capital One Financial", - "Plains GP Holdings", - "World Kinect", - "AIG", - "Coca-Cola", - "TIAA", - "CHS", - "Bristol-Myers Squibb", - "Dow", - "Best Buy", - "Thermo Fisher Scientific", - "Massachusetts Mutual Life Insurance", - "USAA", - "General Dynamics", - "Travelers", - "Warner Bros. Discovery", - "U.S. Bancorp", - "Abbott Laboratories", - "Northrop Grumman", - "Northwestern Mutual", - "Dollar General", - "PBF Energy", - "Uber Technologies", - "Honeywell International", - "Mondelez International", - "Starbucks", - "Qualcomm", - "Broadcom", - "US Foods Holding", - "D.R. Horton", - "Philip Morris International", - "Paccar", - "Salesforce", - "Nucor", - "Jabil", - "Lennar", - "Eli Lilly", - "Molina Healthcare", - "Cummins", - "Bank of New York Mellon", - "Netflix", - "Truist Financial", - "Arrow Electronics", - "3M", - "Visa", - "Apollo Global Management", - "HF Sinclair", - "CBRE Group", - "PNC Financial Services Group", - "Lithia Motors", - "CarMax", - "Paramount Global", - "Dollar Tree", - "United Natural Foods", - "PayPal Holdings", - "Penske Automotive Group", - "Hewlett Packard Enterprise", - "Duke Energy", - "Occidental Petroleum", - "NRG Energy", - "Amgen", - "NextEra Energy", - "Danaher", - "Gilead Sciences", - "AutoNation", - "Kraft Heinz", - "Avnet", - "Applied Materials", - "Southwest Airlines", - "Charles Schwab", - "Baker Hughes", - "McDonald's", - "Southern Company", - "Mastercard", - "Constellation Energy", - "Hartford Financial Services Group", - "PG&E", - "Coupang", - "EOG Resources", - "Union Pacific", - "Rite Aid", - "Macy's", - "Marriott International", - "Lear", - "Genuine Parts", - "Sherwin-Williams", - "Halliburton", - "Freeport-McMoRan", - "Live Nation Entertainment", - "Marsh & McLennan", - "Advanced Micro Devices", - "First Citizens BancShares", - "WESCO International", - "Carrier Global", - "Cleveland-Cliffs", - "Block", - "Exelon", - "KKR", - "CDW", - "Booking Holdings", - "Synchrony", - "Quanta Services", - "Jones Lang LaSalle", - "Discover Financial Services", - "Tenet Healthcare", - "Altria Group", - "Stryker", - "Kimberly-Clark", - "Waste Management", - "Cheniere Energy", - "Ross Stores", - "WestRock", - "General Mills", - "Goodyear Tire & Rubber", - "BJ's Wholesale Club", - "GE HealthCare Technologies", - "Colgate-Palmolive", - "Whirlpool", - "L3Harris Technologies", - "Adobe", - "Becton Dickinson", - "Pioneer Natural Resources", - "Cognizant Technology Solutions", - "Murphy USA", - "Fiserv", - "Parker-Hannifin", - "American Electric Power", - "International Paper", - "ManpowerGroup", - "Aramark", - "Steel Dynamics", - "Aflac", - "Reinsurance Group of America", - "Emerson Electric", - "State Street", - "PPG Industries", - "United States Steel", - "Automatic Data Processing", - "Group 1 Automotive", - "Dominion Energy", - "BlackRock", - "Oneok", - "C.H. Robinson Worldwide", - "Texas Instruments", - "Kohl's", - "AutoZone", - "Lam Research", - "Corteva", - "Peter Kiewit Sons'", - "Builders FirstSource", - "Kyndryl Holdings", - "EchoStar", - "American Family Insurance Group", - "Delek US Holdings", - "Land O'Lakes", - "Sempra", - "Global Partners", - "Grainger", - "Jacobs Solutions", - "Edison International", - "MGM Resorts International", - "Guardian Life Ins. Co. of America", - "Illinois Tool Works", - "Ameriprise Financial", - "PulteGroup", - "Targa Resources", - "Ally Financial", - "BorgWarner", - "Estée Lauder", - "Loews", - "O'Reilly Automotive", - "Markel Group", - "Stanley Black & Decker", - "Micron Technology", - "Fluor", - "Leidos Holdings", - "Viatris", - "Kinder Morgan", - "Ecolab", - "Baxter International", - "Devon Energy", - "Kellanova", - "Farmers Insurance Exchange", - "Casey's General Stores", - "IQVIA Holdings", - "Republic Services", - "Fox", - "Gap", - "Keurig Dr Pepper", - "Reliance", - "Asbury Automotive Group", - "Pacific Life", - "Vistra", - "Western & Southern Financial Group", - "Andersons", - "Nordstrom", - "Omnicom Group", - "Fidelity National Information Services", - "Consolidated Edison", - "CSX", - "AECOM", - "Lumen Technologies", - "Tractor Supply", - "DXC Technology", - "AGCO", - "Sonic Automotive", - "Intuit", - "United Rentals", - "Universal Health Services", - "Boston Scientific", - "Otis Worldwide", - "Xcel Energy", - "Edward Jones", - "Ball", - "LKQ", - "Mutual of Omaha", - "Mosaic", - "Textron", - "Labcorp Holdings", - "Principal Financial", - "Regeneron Pharmaceuticals", - "Raymond James Financial", - "Dick's Sporting Goods", - "Auto-Owners Insurance", - "Expedia Group", - "J.B. Hunt Transport Services", - "M&T Bank", - "DTE Energy", - "AES", - "Berry Global Group", - "Fifth Third Bancorp", - "Air Products & Chemicals", - "Corning", - "EMCOR Group", - "Amphenol", - "Westlake", - "DuPont", - "Liberty Media", - "S&P Global", - "Community Health Systems", - "FirstEnergy", - "Unum Group", - "Henry Schein", - "Western Digital", - "Analog Devices", - "Conagra Brands", - "Citizens Financial Group", - "Norfolk Southern", - "Entergy", - "W.R. Berkley", - "DaVita", - "Northern Trust", - "Hormel Foods", - "Crown Holdings", - "Avis Budget Group", - "Wayfair", - "MasTec", - "Eversource Energy", - "Newmont", - "Ryder System", - "Fidelity National Financial", - "Molson Coors Beverage", - "Caesars Entertainment", - "Lincoln National", - "VF", - "International Flavors & Fragrances", - "Huntington Ingalls Industries", - "Advance Auto Parts", - "Public Service Enterprise Group", - "Ulta Beauty", - "Hershey", - "Chewy", - "American Tower", - "Mohawk Industries", - "Assurant", - "Thor Industries", - "Graybar Electric", - "Yum China Holdings", - "Celanese", - "Qurate Retail", - "Williams", - "Interpublic Group", - "Ovintiv", - "Icahn Enterprises", - "Huntington Bancshares", - "Erie Insurance Group", - "Carvana", - "Hess", - "Dana", - "Alcoa", - "Equitable Holdings", - "KLA", - "Darden Restaurants", - "Autoliv", - "Alaska Air Group", - "KeyCorp", - "Las Vegas Sands", - "Owens & Minor", - "Hilton Worldwide Holdings", - "Ebay", - "Arthur J. Gallagher", - "LPL Financial Holdings", - "Cincinnati Financial", - "Toll Brothers", - "Motorola Solutions", - "Airbnb", - "Intercontinental Exchange", - "News Corp.", - "Chipotle Mexican Grill", - "Vertex Pharmaceuticals", - "Biogen", - "GXO Logistics", - "SpartanNash", - "Burlington Stores", - "Thrivent Financial for Lutherans", - "NVR", - "Owens Corning", - "Westinghouse Air Brake Technologies", - "Oshkosh", - "Global Payments", - "Lululemon athletica", - "Albemarle", - "JetBlue Airways", - "Seaboard", - "Constellation Brands", - "Graphic Packaging Holding", - "Hertz Global Holdings", - "FM Global", - "Campbell Soup", - "Expeditors Intl. of Washington", - "A-Mark Precious Metals", - "Booz Allen Hamilton Holding", - "Quest Diagnostics", - "Altice USA", - "PVH", - "Eastman Chemical", - "Insight Enterprises", - "Regions Financial", - "Beacon Roofing Supply", - "Rockwell Automation", - "Polaris", - "ServiceNow", - "Sanmina", - "UGI", - "WEC Energy Group", - "BrightSpring Health Services", - "Cintas", - "Commercial Metals", - "Continental Resources", - "Chesapeake Energy", - "CenterPoint Energy", - "NGL Energy Partners", - "DoorDash", - "NOV", - "Zoetis", - "J.M. Smucker", - "Microchip Technology", - "Dover", - "Diamondback Energy", - "Avery Dennison", - "PPL", - "ON Semiconductor", - "ARKO", - "Par Pacific Holdings", - "APA", - "Equinix", - "New York Community Bancorp", - "Foot Locker", - "Ingredion", - "Newell Brands", - "ABM Industries", - "Securian Financial Group", - "Prologis", - "Blackstone", - "Skechers U.S.A.", - "Masco", - "Rush Enterprises", - "Franklin Resources", - "ODP", - "American Financial Group", - "Packaging Corp. of America", - "Vulcan Materials", - "Interactive Brokers Group", - "Williams-Sonoma", - "XPO", - "Weyerhaeuser", - "Genworth Financial", - "CMS Energy", - "Science Applications International", - "Jefferies Financial Group", - "Bath & Body Works", - "Electronic Arts", - "Taylor Morrison Home", - "Zimmer Biomet Holdings", - "Clorox", - "Xylem", - "Voya Financial", - "Fastenal", - "Watsco", - "Workday", - "Old Republic International", - "RPM International", - "UFP Industries", - "Ameren", - "Knight-Swift Transportation Hldgs.", - "Monster Beverage", - "Intuitive Surgical", - "Super Micro Computer", - "Concentrix", - "O-I Glass", - "Yum Brands", - "Domtar", - "CommScope Holding", - "Post Holdings", - "Crown Castle", - "Avantor", - "KBR", - "Opendoor Technologies", - "APi Group", - "EQT", - "EnLink Midstream", - "Palo Alto Networks", - "Xerox Holdings", - "Ingersoll Rand", - "Dillard's", - "Martin Marietta Materials", - "Vertiv Holdings", - "Moderna", - "Boise Cascade", - "Sprouts Farmers Market", - "Agilent Technologies", - "Olin", - "Darling Ingredients", - "Sonoco Products", - "CACI International", - "Core & Main", - "Marathon Oil", - "Hyatt Hotels", - "Select Medical Holdings", - "McCormick", - "Tapestry", - "Coca-Cola Consolidated", - "Howmet Aerospace", - "Welltower", - "CF Industries Holdings", - "Ametek", - "TransDigm Group", - "Wynn Resorts", - "Southwestern Energy", - "Amkor Technology", - "Insperity", - "Patterson", - "T. Rowe Price", - "Ralph Lauren", - "KB Home", - "Brunswick", - "Robert Half", - "PENN Entertainment", - "NetApp", - "Organon", - "Petco Health and Wellness", - "Regal Rexnord", - "Resideo Technologies", - "Camping World Holdings", - "Huntsman", - "Victoria's Secret", - "Levi Strauss", - "Roper Technologies", - "Academy Sports and Outdoors", - "Meritage Homes", - "American Axle & Manufacturing", - "Fortive", - "Nasdaq", - "Broadridge Financial Solutions", - "Spirit AeroSystems Holdings", - "Warner Music Group", - "Chemours", - "ADT", - "Edwards Lifesciences", - "First American Financial", - "Hanover Insurance Group", - "Silgan Holdings", - "Endeavor Group Holdings", - "Moody's", - "Coterra Energy", - "Gartner", - "Under Armour", - "Ingles Markets", - "Church & Dwight", - "Old Dominion Freight Line", - "U-Haul Holding", - "Oscar Health", - "Arista Networks", - "Synopsys", - "Harley-Davidson", - "Frontier Communications", - "Primoris Services", - "Carlisle", - "Simon Property Group", - "Hanesbrands", - "Anywhere Real Estate", - "Teledyne Technologies", - "CME Group", - "Juniper Networks", - "Coty", - "Pool", - "Evergy", - "Marvell Technology", - "NiSource", - "SS&C Technologies Holdings", - "Schneider National", - "Autodesk", - "Sealed Air", - "Iron Mountain", - "Digital Realty Trust", - "Keysight Technologies", - "Globe Life", - "Parsons", - "Mattel", - "Southwest Gas Holdings", - "CUNA Mutual Group (TruStage)", - "Clean Harbors", - "Cornerstone Building Brands", - "Hubbell", - "Spirit Airlines", - "Lamb Weston Holdings", - "Take-Two Interactive Software", - "GMS", - "Penn Mutual Life Insurance", - "Landstar System", - "Host Hotels & Resorts", - "Fortinet", - "OneMain Holdings", - "GameStop", - "Equifax", - "American Eagle Outfitters", - "Comerica", - "Greif", - "Comfort Systems USA", - "TopBuild", - "Bread Financial Holdings", - "Coherent", - "Telephone & Data Systems", - "Stifel Financial", - "Urban Outfitters", - "Terex", - "Ryerson Holding", - "Snap-on", - "Flowers Foods", - "First Horizon", - "Paychex", - "Hasbro", - "Sentry Insurance Group", - "Ares Management", - "Lennox International", - "Peabody Energy", - "Kemper", - "Nexstar Media Group", - "TelevisaUnivision", - "Tempur Sealy International", - "TriNet Group", - "Worthington Enterprises", - "Maximus", - "Compass", - "Brink's", - "Kelly Services", - "Navient", - "Puget Energy", - "AMC Entertainment Holdings", - "Encompass Health", - "Skyworks Solutions", - "Timken", - "Liberty Energy", - "Marriott Vacations Worldwide", - "Leggett & Platt", - "Big Lots", - "Weis Markets", - "Pinnacle West Capital", - "EPAM Systems", - "Antero Resources", - "Bloomin' Brands", - "MDU Resources Group", - "MDC Holdings", - "Texas Roadhouse", - "Mercury General", - "Fortune Brands Innovations", - "Zions Bancorp.", - "JELD-WEN Holding", - "Snap", - "Zebra Technologies", - "Agilon Health", - "Toro", - "Zoom Video Communications", - "Tetra Tech", - "Public Storage", - "Illumina", - "Ventas", - "FMC", - "ArcBest", - "Domino's Pizza", - "ASGN", - "Rivian Automotive", - "Elanco Animal Health", - "Graham Holdings", - "Floor & Decor Holdings", - "Applied Industrial Technologies", - "PriceSmart", - "Lyft", - "Ciena", - "IAC", - "Country Financial", - "Western Union", - "Flowserve", - "Western Alliance Bancorp.", - "Option Care Health", - "SiteOne Landscape Supply", - "Topgolf Callaway Brands", - "eXp World Holdings", - "Abercrombie & Fitch", - "Atmos Energy", - "Catalent", - "Brown & Brown", - "GoDaddy", - "American Water Works", - "Selective Insurance Group", - "Brown-Forman", - "Advantage Solutions", - "ResMed", - "Splunk", - "Plexus", - "Hub Group", - "Lincoln Electric Holdings", - "NCR Atleos", - "Calumet", - "Dycom Industries", - "Valmont Industries", - "ATI", - "Twilio", - "CNO Financial Group", - "Patterson-UTI Energy", - "Brinker International", - "Charles River Laboratories International", - "Hyster-Yale", - "Brighthouse Financial", - "Service Corp. International", - "CrossAmerica Partners", - "Cadence", - "MillerKnoll", - "Realty Income", - "Middleby", - "M/I Homes", - "Hologic", - "Alliant Energy", - "Generac Holdings", - "MSC Industrial Direct", - "Rocket Companies", - "Upbound Group", - "East West Bancorp", - "Hilton Grand Vacations", - "Grocery Outlet Holding", - "Dentsply Sirona", - "V2X", - "Crocs", - "Spectrum Brands Holdings", - "Visteon", - "Acuity Brands", - "Greenbrier", - "Webster Financial", - "Cabot", - "RXO", - "Chord Energy", - "Popular", - "Garrett Motion", - "Tutor Perini", - "Copart", - "Toast", - "Align Technology", - "A.O. Smith", - "TransUnion", - "NCR Voyix", - "Akamai Technologies", - "Trimble", - "Medical Mutual of Ohio", - "AMN Healthcare Services", - "Mettler-Toledo International", - "ScanSource", - "LCI Industries", - "Cboe Global Markets", - "Diebold Nixdorf", - "ChampionX", - "Corpay", - "iHeartMedia", - "Travel + Leisure", - "Dream Finders Homes", - "Boyd Gaming", - "NLV Financial", - "Ensign Group", - "Sally Beauty Holdings", - "Conduent", - "Sylvamo", - "Tri Pointe Homes", - "EnerSys", - "Incyte", - "Century Communities", - "Rithm Capital", - "Euronet Worldwide", - "Hawaiian Electric Industries", - "DraftKings", - "IDEXX Laboratories", - "MYR Group", - "Deckers Outdoor", - "DexCom", - "MKS Instruments", - "VICI Properties", - "Cooper Cos.", - "Frontier Group Holdings", - "Qorvo", - "Five Below", - "TreeHouse Foods", - "Scotts Miracle-Gro", - "Atlassian", - "Entegris", - "Atkore", - "H.B. Fuller", - "Granite Construction", - "Winnebago Industries", - "FTI Consulting", - "AptarGroup", - "Columbia Sportswear", - "Roku", - "Civitas Resources", - "H&R Block", - "Alpha Metallurgical Resources", - "Patrick Industries", - "Murphy Oil", - "Synovus Financial", - "Cracker Barrel Old Country Store", - "Cheesecake Factory", - "CNX Resources", - "Chefs' Warehouse", - "Donaldson", - "Mueller Industries", - "MRC Global", - "Chart Industries", - "Alight", - "Vishay Intertechnology", - "Range Resources", - "Valley National Bancorp", - "Match Group", - "Gen Digital", - "Wintrust Financial", - "Moog", - "First Solar", - "Central Garden & Pet", - "Green Plains", - "Werner Enterprises", - "AppLovin", - "ITT", - "Herc Holdings", - "Gray Television", - "IDEX", - "Boston Properties", - "Pitney Bowes", - "Steelcase", - "Sun Communities", - "AdaptHealth", - "Vertex Energy", - "Genesis Energy", - "Jackson Financial", - "FirstCash Holdings", - "Cal-Maine Foods", - "Arch Resources", - "Avient", - "Cano Health", - "BlueLinx Holdings", - "Sinclair", - "BOK Financial", - "Permian Resources", - "PACS Group", - "Coinbase Global", - "Western Midstream Partners", - "Federated Mutual Insurance", - "G-III Apparel Group", - "Vontier", - "Matson", - "Kirby", - "Kaiser Aluminum", - "Vista Outdoor", - "Designer Brands", - "Rollins", - "Advanced Drainage Systems", - "Cinemark Holdings", - "CrowdStrike", - "Pinterest", - "Instacart", - "Hillenbrand", - "Allison Transmission Holdings", - "RH", - "Brookdale Senior Living", - "QuidelOrtho", - "Trinity Industries", - "Teleflex", - "HEICO", - "Bruker", - "Carlyle Group", - "Energizer Holdings", - "Quad/Graphics", - "Rackspace Technology", - "Waters", - "West Pharmaceutical Services", - "Carter's", - "NeueHealth", - "SkyWest", - "Acadia Healthcare", - "Revvity", - "Portland General Electric", - "Woodward", - "TEGNA", - "Sabre", - "Light & Wonder", - "Vail Resorts", - "Radius Recycling", - "PennyMac Financial Services", - "Saia", - "Equity Residential", - "Helmerich & Payne", - "Korn Ferry", - "PC Connection", - "Curtiss-Wright", - "SLM", - "Benchmark Electronics", - "American National Group", - "Pure Storage", - "Knife River", - "Caleres", - "BrightView Holdings", - "Cooper-Standard Holdings", - "Primerica", - "F5", - "Matador Resources", - "Snowflake", - "Viasat", - "California Resources", - "Peloton Interactive", - "Roblox", - "Installed Building Products", - "First National of Nebraska", - "Guess", - "ESAB", - "Amica Mutual Insurance", - "AvalonBay Communities", - "DocuSign", - "Titan Machinery", - "ModivCare", - "Hovnanian Enterprises", - "Etsy", - "Adams Resources & Energy", - "Surgery Partners", - "Columbia Banking System", - "Verisk", - "MasterBrand", - "Hawaiian Holdings", - "AMC Networks", - "SBA Communications", - "Americold Realty Trust", - "NewMarket", - "Park Hotels & Resorts", - "Alexandria Real Estate Equities", - "Griffon", - "Air Lease", - "UL Solutions", - "Teradyne", - "OGE Energy", - "Bio-Rad Laboratories", - "Spire", - "Gannett", - "Stericycle", - "Cullen/Frost Bankers", - "Extra Space Storage", - "REV Group", - "ProFrac Holding", - "Nordson", - "Summit Materials", - "Valvoline", - "Kontoor Brands", - "Skyline Champion", - "Teladoc Health", - "Louisiana-Pacific", - "Universal", - "Consol Energy", - "Encore Wire", - "Playtika Holding", - "Alliance Resource Partners", - "Envista Holdings", - "Shift4 Payments", - "Talen Energy", - "Carpenter Technology", - "WEX", - "Old National Bancorp", - "AGNC Investment", - "Wabash National", - "MSCI", - "Stagwell", - "Mativ Holdings", - "Belden", - "Allegiant Travel", - "Dropbox", - "Exact Sciences", - "BWX Technologies", - "Clear Channel Outdoor Hldgs.", -] as const; - -export const SHIRT_SIZES = ["XS", "S", "M", "L", "XL", "2XL", "3XL"] as const; - -export const SPONSOR_TIERS = ["gold", "silver", "bronze", "other"] as const; - -export const ADMIN_PIE_CHART_COLORS: readonly string[] = [ - "#f72585", - "#b5179e", - "#7209b7", - "#3a0ca3", - "#4361ee", - "#4895ef", - "#4cc9f0", - "#560bad", - "#480ca8", -] as const; - -export const DEFAULT_COLOR = "#ffffff"; -export const DEFAULT_EMAIL_QUEUE_CRON_SCHEDULE = "0 * * * *"; - -export const WEEKDAY_ORDER: string[] = [ - "Mon", - "Tues", - "Wed", - "Thurs", - "Fri", - "Sat/Sun", -] as const; - -export const RANKING_STYLES: string[] = [ - "md:text-lg lg:text-lg font-bold text-yellow-500", - "md:text-lg lg:text-lg font-semibold text-gray-400", - "md:text-lg lg:text-lg font-medium text-orange-500", -]; - -export const SEMESTER_START_DATES = { - spring: { - month: 0, - day: 1, // first day of January - }, - summer: { - month: 4, - day: 1, // first day of May - }, - fall: { - month: 7, - day: 15, // middle of August - }, -} as const; - -export const ALL_DATES_RANGE_UNIX = { - start: -8640000000000000, - end: 8640000000000000, -} as const; - -export interface Semester { - name: string; - startDate: Date; - endDate: Date; -} - -export interface ClassInfo { - team: string; - teamColor: string; - classPfp: string; -} - -export const HACKER_CLASS_INFO: Record = { - Mechanist: { - team: "Humanity", - teamColor: "#228be6", - classPfp: "/khviii/mechanist.jpg", - }, - Operator: { - team: "Humanity", - teamColor: "#228be6", - classPfp: "/khviii/operator.jpg", - }, - Sentinel: { - team: "Humanity", - teamColor: "#228be6", - classPfp: "/khviii/sentinel.jpg", - }, - Monstologist: { - team: "Monstrosity", - teamColor: "#e03131", - classPfp: "/khviii/monstologist.jpg", - }, - Harbinger: { - team: "Monstrosity", - teamColor: "#e03131", - classPfp: "/khviii/harbinger.jpg", - }, - Alchemist: { - team: "Monstrosity", - teamColor: "#e03131", - classPfp: "/khviii/alchemist.jpg", - }, -}; - -export const OFFICER_ROLE_ID = - process.env.NODE_ENV === "production" - ? "486629374758748180" - : "1246637685011906560"; - -export const DEVPOST_TEAM_MEMBER_EMAIL_OFFSET = 3; - -export const QuestionValidator = z.object({ - question: z.string(), - image: z.string().url().optional(), - type: z.enum([ - "SHORT_ANSWER", - "PARAGRAPH", - "MULTIPLE_CHOICE", - "CHECKBOXES", - "DROPDOWN", - "LINEAR_SCALE", - "DATE", - "TIME", - "EMAIL", - "NUMBER", - "PHONE", - "FILE_UPLOAD", - "BOOLEAN", - "LINK", - ]), - options: z.array(z.string()).optional(), - optionsConst: z.string().optional(), - optional: z.boolean().optional(), - allowOther: z.boolean().optional(), - min: z.number().optional(), - max: z.number().optional(), - order: z.number().optional(), -}); - -export const InstructionValidator = z.object({ - title: z.string().max(200), - content: z.string().max(2000).optional(), - imageUrl: z.string().url().optional(), - videoUrl: z.string().url().optional(), - imageObjectName: z.string().optional(), - videoObjectName: z.string().optional(), - order: z.number().optional(), -}); - -export const FormSchemaValidator = z.object({ - banner: z.string().url().optional(), - name: z.string().max(200), - description: z.string().max(500), - questions: z.array(QuestionValidator), - instructions: z.array(InstructionValidator).optional(), -}); - -export type FormType = z.infer; -export type InstructionValidatorType = z.infer; - -type QuestionValidatorType = z.infer; -export type ValidatorOptions = Omit; - -export type QuestionsType = z.infer["type"]; - -export const AVAILABLE_DROPDOWN_CONSTANTS = { - LEVELS_OF_STUDY: "Levels of Study", - ALLERGIES: "Allergies", - MAJORS: "Majors", - GENDERS: "Genders", - RACES_OR_ETHNICITIES: "Races or Ethnicities", - COUNTRIES: "Countries", - SCHOOLS: "Schools", - COMPANIES: "Companies", - SHIRT_SIZES: "Shirt Sizes", - EVENT_FEEDBACK_HEARD: "Event Feedback - How You Heard", - SHORT_LEVELS_OF_STUDY: "Short Levels of Study", - SHORT_RACES_AND_ETHNICITIES: "Short Races and Ethnicities", -} as const; - -export type DropdownConstantKey = keyof typeof AVAILABLE_DROPDOWN_CONSTANTS; - -export function getDropdownOptionsFromConst( - constName: string, -): readonly string[] { - switch (constName) { - case "LEVELS_OF_STUDY": - return LEVELS_OF_STUDY; - case "ALLERGIES": - return ALLERGIES; - case "MAJORS": - return MAJORS; - case "GENDERS": - return GENDERS; - case "RACES_OR_ETHNICITIES": - return RACES_OR_ETHNICITIES; - case "COUNTRIES": - return COUNTRIES; - case "SCHOOLS": - return SCHOOLS; - case "COMPANIES": - return COMPANIES; - case "SHIRT_SIZES": - return SHIRT_SIZES; - case "EVENT_FEEDBACK_HEARD": - return EVENT_FEEDBACK_HEARD; - case "SHORT_LEVELS_OF_STUDY": - return SHORT_LEVELS_OF_STUDY; - case "SHORT_RACES_AND_ETHNICITIES": - return SHORT_RACES_AND_ETHNICITIES; - default: - return []; - } -} - -export interface FundingRequestInput { - team: string; - description: string; - amount: number; - itemization: string; - importance: number; - dateNeeded: string; - deadlineType: string; -} - -export const FORM_QUESTION_TYPES = [ - { value: "SHORT_ANSWER", label: "Short answer" }, - { value: "PARAGRAPH", label: "Paragraph" }, - { value: "MULTIPLE_CHOICE", label: "Multiple choice" }, - { value: "CHECKBOXES", label: "Checkboxes" }, - { value: "DROPDOWN", label: "Dropdown" }, - { value: "FILE_UPLOAD", label: "File upload" }, - { value: "LINEAR_SCALE", label: "Linear scale" }, - { value: "DATE", label: "Date" }, - { value: "TIME", label: "Time" }, - { value: "EMAIL", label: "Email" }, - { value: "NUMBER", label: "Number" }, - { value: "PHONE", label: "Phone" }, - { value: "BOOLEAN", label: "Boolean (Yes/No)" }, - { value: "LINK", label: "Link (URL)" }, -] as const; - -export const RECRUITING_CHANNEL = IS_PROD - ? "1461758896950608104" - : DEV_KNIGHTHACKS_LOG_CHANNEL; - -export const TEAM_MAP = [ - { - team: "Outreach", - color: "#88fea1", - director_role: "779845137822908436", - }, - { - team: "Design", - color: "#eaacff", - director_role: "874028482089349172", - }, - { - team: "Development", - color: "#93ceff", - director_role: "1082124530077683772", - }, - { - team: "Sponsorship", - color: "#f5f4af", - director_role: "626815399442513920", - }, - { - team: "Workshops", - color: "#206694", - director_role: "757002949603098837", - }, - { - team: "Projects/Mentorship", - color: "#3498db", - director_role: "1244790444626280550", - }, -]; - -export const ALLOWED_FORM_ASSIGNABLE_DISC_ROLES = ["1467281189088788489"]; diff --git a/packages/consts/src/index.ts b/packages/consts/src/index.ts new file mode 100644 index 000000000..c058a01b6 --- /dev/null +++ b/packages/consts/src/index.ts @@ -0,0 +1,7 @@ +export * as DISCORD from "./discord"; +export * as FORMS from "./forms"; +export * as EVENTS from "./events"; +export * as OFFICERS from "./officers"; + +// TODO: get rid of misc +export * from "./misc"; diff --git a/packages/consts/src/misc.ts b/packages/consts/src/misc.ts new file mode 100644 index 000000000..f728f46f1 --- /dev/null +++ b/packages/consts/src/misc.ts @@ -0,0 +1,186 @@ +// This file should not be added to. I have no idea where these belong, hence +// the miscellaneous category. Please DO NOT add any more constants to this +// file. + +import { IS_PROD } from "./util"; + +export const KNIGHTHACKS_S3_BUCKET_REGION = "us-east-1"; +export const KNIGHTHACKS_MAX_RESUME_SIZE = 5 * 1000000; // 5MB +export const FORM_ASSETS_BUCKET = "form-assets"; + +export const PRESIGNED_URL_EXPIRY = 7 * 24 * 60 * 60; // 7 days + +export const KNIGHTHACKS_MAX_PROFILE_PICTURE_SIZE = 2 * 1024 * 1024; // 2MB +export const ALLOWED_PROFILE_PICTURE_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", +]; + +export const ALLOWED_PROFILE_PICTURE_EXTENSIONS = [ + "jpg", + "jpeg", + "png", + "gif", + "webp", +]; + +export const TERM_TO_DATE = { + Spring: { month: 4, day: 2 }, // May 2 + Summer: { month: 7, day: 6 }, // Aug 6 + Fall: { month: 11, day: 10 }, // Dec 10 +} as const; +export type GradTerm = keyof typeof TERM_TO_DATE; + +export const GUILD_TAG_OPTIONS = ["alumni", "current"] as const; +export type GuildTag = (typeof GUILD_TAG_OPTIONS)[number]; + +export const MINIO_ENDPOINT = "minio-g0soogg4gs8gwcggw4ococok.knighthacks.org"; +export const BUCKET_NAME = "knight-hacks-qr"; +export const QR_CONTENT_TYPE = "image/png"; +export const QR_PATHNAME = "/knight-hacks-qr/**"; + +export const KNIGHTHACKS_MEMBERSHIP_PRICE = 2500; + +export interface PermissionDataObj { + idx: number; + name: string; + desc: string; +} + +export const PERMISSION_DATA: Record = { + IS_OFFICER: { + idx: 0, + name: "Is Officer", + desc: "Grants access to sensitive club officer pages.", + }, + IS_JUDGE: { + idx: 1, + name: "Is Judge", + desc: "Grants access to the judging system.", + }, + READ_MEMBERS: { + idx: 2, + name: "Read Members", + desc: "Grants access to the list of club members.", + }, + EDIT_MEMBERS: { + idx: 3, + name: "Edit Members", + desc: "Allows editing member data, including deletion.", + }, + READ_HACKERS: { + idx: 4, + name: "Read Hackers", + desc: "Grants access to the list of hackers, and their hackathons.", + }, + EDIT_HACKERS: { + idx: 5, + name: "Edit Hackers", + desc: "Allows editing hacker data, including approval, rejection, deletion, etc.", + }, + READ_CLUB_DATA: { + idx: 6, + name: "Read Club Data", + desc: "Grants access to club statistics, such as demographics.", + }, + READ_HACK_DATA: { + idx: 7, + name: "Read Hackathon Data", + desc: "Grants access to hackathon statistics, such as demographics.", + }, + READ_CLUB_EVENT: { + idx: 8, + name: "Read Club Events", + desc: "Grants access to club event data, such as attendance.", + }, + EDIT_CLUB_EVENT: { + idx: 9, + name: "Edit Club Events", + desc: "Allows creating, editing, or deleting club events.", + }, + CHECKIN_CLUB_EVENT: { + idx: 10, + name: "Club Event Check-in", + desc: "Allows the user to check members into club events.", + }, + READ_HACK_EVENT: { + idx: 11, + name: "Read Hackathon Events", + desc: "Grants access to hackathon event data, such as attendance.", + }, + EDIT_HACK_EVENT: { + idx: 12, + name: "Edit Hackathon Events", + desc: "Allows creating, editing, or deleting hackathon events.", + }, + CHECKIN_HACK_EVENT: { + idx: 13, + name: "Hackathon Event Check-in", + desc: "Allows the user to check hackers into hackathon events, including the primary check-in.", + }, + EMAIL_PORTAL: { + idx: 14, + name: "Email Portal", + desc: "Grants access to the email queue portal.", + }, + READ_FORMS: { + idx: 15, + name: "Read Forms", + desc: "Grants access to created forms, but not their responses.", + }, + READ_FORM_RESPONSES: { + idx: 16, + name: "Read Form Responses", + desc: "Grants access to form responses.", + }, + EDIT_FORMS: { + idx: 17, + name: "Edit Forms", + desc: "Allows creating, editing, or deleting forms.", + }, + ASSIGN_ROLES: { + idx: 18, + name: "Assign Roles", + desc: "Allows assigning or removing roles to Blade users.", + }, + CONFIGURE_ROLES: { + idx: 19, + name: "Configure Roles", + desc: "Allows creating, editing, or deleting roles.", + }, +} as const satisfies Record; + +export const PERMISSIONS = Object.fromEntries( + Object.entries(PERMISSION_DATA).map(([key, value]) => [key, value.idx]), +) as { + [K in keyof typeof PERMISSION_DATA]: (typeof PERMISSION_DATA)[K]["idx"]; +}; + +export type PermissionKey = keyof typeof PERMISSION_DATA; +export type PermissionIndex = (typeof PERMISSION_DATA)[PermissionKey]["idx"]; + +export const MEMBER_PROFILE_ICON_SIZE = 24; + +export const SPONSOR_VIDEO_LINK = + "https://www.youtube.com/embed/OU1q02v1Vrw?si=dyHSQCmxzcau7-mF"; + +export const CALENDAR_TIME_ZONE = "America/New_York"; + +export const DUES_PAYMENT = 25; + +export const CLEAR_DUES_MESSAGE = + "I am aware of the consequences regarding this action if it were by mistake. I am absolutely sure that I want to clear all dues."; + +export const PROD_GOOGLE_CALENDAR_ID = + "c_0b9df2b0062a5d711fc16060ff3286ef404b174bfafc4cbdd4e3009e91536e94@group.calendar.google.com"; +export const DEV_GOOGLE_CALENDAR_ID = + "c_178118a9a25d9f278014b123b79b7e9caf9b29ac94bba3934237db00979e0f75@group.calendar.google.com"; +export const GOOGLE_CALENDAR_ID = IS_PROD + ? PROD_GOOGLE_CALENDAR_ID + : DEV_GOOGLE_CALENDAR_ID; + +export const GOOGLE_PERSONIFY_EMAIL = "dylan@knighthacks.org"; + +export const USE_CAUTION = true; diff --git a/packages/consts/src/officers.ts b/packages/consts/src/officers.ts new file mode 100644 index 000000000..072d426a5 --- /dev/null +++ b/packages/consts/src/officers.ts @@ -0,0 +1,46 @@ +export const LIST = [ + { + name: "Dylan Vidal", + position: "President", + image: "/officers/dylan.png", + linkedin: "https://www.linkedin.com/in/dylanvidal1204/", + major: "Computer Science", + }, + { + name: "Leonard Gofman", + position: "Vice President", + image: "/officers/leo.png", + linkedin: "https://www.linkedin.com/in/leonard-gofman-208578236/", + major: "Computer Science", + }, + { + name: "Adrian Osorio", + position: "Treasurer", + image: "/officers/adrian.png", + linkedin: "https://www.linkedin.com/in/adrianosoriob/", + major: "Computer Engineering", + }, + { + name: "Lewin Bobda", + position: "Dev Lead", + image: "/officers/bobda.png", + linkedin: "https://www.linkedin.com/in/lewin-bobda-08ba2325a/", + major: "Computer Science", + }, + { + name: "Daniel Efres", + position: "Secretary", + image: "/officers/daniel.png", + linkedin: "https://www.linkedin.com/in/daniel-efres/", + major: "Computer Science", + }, + { + name: "Richard Phillips", + position: "Hack Lead", + image: "/officers/ricky.png", + linkedin: "https://www.linkedin.com/in/rphillipscs/", + major: "Computer Science", + }, +] as const; + +export type Officer = (typeof LIST)[number]; diff --git a/packages/consts/src/util.ts b/packages/consts/src/util.ts new file mode 100644 index 000000000..18e9729bc --- /dev/null +++ b/packages/consts/src/util.ts @@ -0,0 +1 @@ +export const IS_PROD = process.env.NODE_ENV === "production"; \ No newline at end of file diff --git a/packages/consts/tsconfig.json b/packages/consts/tsconfig.json index 139283fc3..595256422 100644 --- a/packages/consts/tsconfig.json +++ b/packages/consts/tsconfig.json @@ -1,5 +1,4 @@ { "extends": "@forge/tsconfig/internal-package.json", "include": ["src"], - "exclude": ["node_modules"] } diff --git a/packages/db/scripts/bootstrap-superadmin.ts b/packages/db/scripts/bootstrap-superadmin.ts index 690178e1d..df0e23b40 100644 --- a/packages/db/scripts/bootstrap-superadmin.ts +++ b/packages/db/scripts/bootstrap-superadmin.ts @@ -13,7 +13,7 @@ */ import { eq } from "drizzle-orm"; -import { PERMISSIONS } from "@forge/consts/knight-hacks"; +import { PERMISSIONS } from "@forge/consts"; import { db } from "../src/client"; import { Permissions, Roles } from "../src/schemas/auth"; diff --git a/packages/db/scripts/seed_devdb.ts b/packages/db/scripts/seed_devdb.ts index 0c7186da2..08ccbd909 100644 --- a/packages/db/scripts/seed_devdb.ts +++ b/packages/db/scripts/seed_devdb.ts @@ -15,11 +15,7 @@ import { drizzle } from "drizzle-orm/node-postgres"; import Pool from "pg-pool"; import { stringify } from "superjson"; -import { - DEV_KNIGHTHACKS_GUILD_ID, - KNIGHTHACKS_S3_BUCKET_REGION, - PROD_KNIGHTHACKS_GUILD_ID, -} from "@forge/consts/knight-hacks"; +import { DISCORD, KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts"; import { minioClient } from "../../api/src/minio/minio-client"; import { discord, log } from "../../api/src/utils"; @@ -233,12 +229,12 @@ async function syncRoles() { ).map((row) => row.discordRoleId), ); let prodRoles = (await discord.get( - Routes.guildRoles(PROD_KNIGHTHACKS_GUILD_ID), + Routes.guildRoles(DISCORD.PROD_KNIGHTHACKS_GUILD), )) as DiscordRole[]; prodRoles = prodRoles.filter((role) => prodRolesWithPerms.has(role.id)); const devRolesArr = (await discord.get( - Routes.guildRoles(DEV_KNIGHTHACKS_GUILD_ID), + Routes.guildRoles(DISCORD.DEV_KNIGHTHACKS_GUILD), )) as DiscordRole[]; const devRoles = Object.fromEntries( devRolesArr.map((role) => [role.name + " " + role.permissions, role]), @@ -251,7 +247,7 @@ async function syncRoles() { } else { await new Promise((resolve) => setTimeout(resolve, 100)); const newRole = (await discord.post( - Routes.guildRoles(DEV_KNIGHTHACKS_GUILD_ID), + Routes.guildRoles(DISCORD.DEV_KNIGHTHACKS_GUILD), { body: { name: role.name, @@ -293,11 +289,11 @@ async function syncEvents() { if (!backupDb) return; const prodEvents = (await discord.get( - Routes.guildScheduledEvents(PROD_KNIGHTHACKS_GUILD_ID), + Routes.guildScheduledEvents(DISCORD.PROD_KNIGHTHACKS_GUILD), )) as DiscordGuildScheduledEvent[]; const devEventsArr = (await discord.get( - Routes.guildScheduledEvents(DEV_KNIGHTHACKS_GUILD_ID), + Routes.guildScheduledEvents(DISCORD.DEV_KNIGHTHACKS_GUILD), )) as DiscordGuildScheduledEvent[]; const devEvents = Object.fromEntries( devEventsArr.map((ev) => [ev.name + " " + ev.scheduled_start_time, ev]), @@ -310,7 +306,7 @@ async function syncEvents() { } else { await new Promise((resolve) => setTimeout(resolve, 100)); const newEvent = (await discord.post( - Routes.guildScheduledEvents(DEV_KNIGHTHACKS_GUILD_ID), + Routes.guildScheduledEvents(DISCORD.DEV_KNIGHTHACKS_GUILD), { body: { name: event.name, diff --git a/packages/db/src/schemas/knight-hacks.ts b/packages/db/src/schemas/knight-hacks.ts index b135a17db..96d07b13e 100644 --- a/packages/db/src/schemas/knight-hacks.ts +++ b/packages/db/src/schemas/knight-hacks.ts @@ -8,36 +8,23 @@ import { import { createInsertSchema } from "drizzle-zod"; import z from "zod"; -import { - COUNTRIES, - EVENT_FEEDBACK_HEARD, - EVENT_FEEDBACK_SIMILAR_EVENT, - EVENT_TAGS, - GENDERS, - HACKATHON_APPLICATION_STATES, - LEVELS_OF_STUDY, - MAJORS, - RACES_OR_ETHNICITIES, - SCHOOLS, - SHIRT_SIZES, - SPONSOR_TIERS, -} from "@forge/consts/knight-hacks"; +import { EVENTS, FORMS } from "@forge/consts"; import { Roles, User } from "./auth"; const createTable = pgTableCreator((name) => `knight_hacks_${name}`); -export const shirtSizeEnum = pgEnum("shirt_size", SHIRT_SIZES); -export const eventTagEnum = pgEnum("event_tag", EVENT_TAGS); -export const genderEnum = pgEnum("gender", GENDERS); +export const shirtSizeEnum = pgEnum("shirt_size", FORMS.SHIRT_SIZES); +export const eventTagEnum = pgEnum("event_tag", EVENTS.EVENT_TAGS); +export const genderEnum = pgEnum("gender", FORMS.GENDERS); export const raceOrEthnicityEnum = pgEnum( "race_or_ethnicity", - RACES_OR_ETHNICITIES, + FORMS.RACES_OR_ETHNICITIES, ); -export const sponsorTierEnum = pgEnum("sponsor_tier", SPONSOR_TIERS); +export const sponsorTierEnum = pgEnum("sponsor_tier", FORMS.SPONSOR_TIERS); export const hackathonApplicationStateEnum = pgEnum( "hackathon_application_state", - HACKATHON_APPLICATION_STATES, + FORMS.HACKATHON_APPLICATION_STATES, ); export const Hackathon = createTable("hackathon", (t) => ({ @@ -69,9 +56,9 @@ export const Member = createTable( age: t.integer().notNull(), email: t.varchar({ length: 255 }).notNull(), phoneNumber: t.varchar({ length: 255 }), - school: t.text({ enum: SCHOOLS }).notNull(), - levelOfStudy: t.text({ enum: LEVELS_OF_STUDY }).notNull(), - major: t.text({ enum: MAJORS }).notNull().default("Computer Science"), + school: t.text({ enum: FORMS.SCHOOLS }).notNull(), + levelOfStudy: t.text({ enum: FORMS.LEVELS_OF_STUDY }).notNull(), + major: t.text({ enum: FORMS.MAJORS }).notNull().default("Computer Science"), gender: genderEnum().default("Prefer not to answer").notNull(), raceOrEthnicity: raceOrEthnicityEnum() .default("Prefer not to answer") @@ -110,14 +97,14 @@ export const Hacker = createTable("hacker", (t) => ({ discordUser: t.varchar({ length: 255 }).notNull(), age: t.integer().notNull(), country: t - .text({ enum: COUNTRIES }) + .text({ enum: FORMS.COUNTRIES }) .notNull() .default("United States of America"), email: t.varchar({ length: 255 }).notNull(), phoneNumber: t.varchar({ length: 255 }), - school: t.text({ enum: SCHOOLS }).notNull(), - levelOfStudy: t.text({ enum: LEVELS_OF_STUDY }).notNull(), - major: t.text({ enum: MAJORS }).notNull().default("Computer Science"), + school: t.text({ enum: FORMS.SCHOOLS }).notNull(), + levelOfStudy: t.text({ enum: FORMS.LEVELS_OF_STUDY }).notNull(), + major: t.text({ enum: FORMS.MAJORS }).notNull().default("Computer Science"), raceOrEthnicity: raceOrEthnicityEnum() .default("Prefer not to answer") .notNull(), @@ -263,7 +250,7 @@ export const HackerAttendee = createTable("hacker_attendee", (t) => ({ }), status: t .text("status", { - enum: HACKATHON_APPLICATION_STATES, + enum: FORMS.HACKATHON_APPLICATION_STATES, }) .notNull() .default("pending"), @@ -333,9 +320,9 @@ export const EventFeedback = createTable("event_feedback", (t) => ({ overallEventRating: t.integer().notNull(), funRating: t.integer().notNull(), learnedRating: t.integer().notNull(), - heardAboutUs: t.text({ enum: EVENT_FEEDBACK_HEARD }).notNull(), + heardAboutUs: t.text({ enum: FORMS.EVENT_FEEDBACK_HEARD }).notNull(), additionalFeedback: t.text(), - similarEvent: t.text({ enum: EVENT_FEEDBACK_SIMILAR_EVENT }).notNull(), + similarEvent: t.text({ enum: EVENTS.EVENT_FEEDBACK_SIMILAR_EVENT }).notNull(), createdAt: t.timestamp().notNull().defaultNow(), })); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd3493cc4..4d70b86b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -243,7 +243,7 @@ importers: version: 6.6.0 geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) google-auth-library: specifier: ^9.15.0 version: 9.15.1 @@ -358,7 +358,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -534,7 +534,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -625,7 +625,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -891,6 +891,9 @@ importers: packages/consts: dependencies: + '@forge/db': + specifier: workspace:* + version: link:../db minimatch: specifier: ^10.1.2 version: 10.1.2 @@ -919,6 +922,9 @@ importers: packages/db: dependencies: + '@forge/api': + specifier: workspace:* + version: link:../api '@forge/consts': specifier: workspace:* version: link:../consts @@ -1195,7 +1201,7 @@ importers: version: 14.2.35 eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: specifier: ^6.10.0 version: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -13467,6 +13473,33 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -13767,7 +13800,7 @@ snapshots: - encoding - supports-color - geist@1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + geist@1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: next: 14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) From f7554d882f2ff32824d6036d1d06008f0e514c7d Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:43:24 -0500 Subject: [PATCH 02/23] chore(refactor): more refactoring --- apps/2025/package.json | 1 - apps/blade/src/lib/utils.ts | 4 ---- .../src/app/_components/landing/discover.tsx | 4 ++-- .../src/app/officers/_components/officers.tsx | 4 ++-- apps/cron/src/crons/alumni-assign.ts | 6 +++--- apps/gemiknights/package.json | 1 - apps/guild/src/app/_components/dock.tsx | 4 ++-- apps/guild/src/app/page.tsx | 4 ++-- apps/tk/package.json | 1 - pnpm-lock.yaml | 17 +---------------- 10 files changed, 12 insertions(+), 34 deletions(-) diff --git a/apps/2025/package.json b/apps/2025/package.json index 30573d3ae..79ca99f1c 100644 --- a/apps/2025/package.json +++ b/apps/2025/package.json @@ -17,7 +17,6 @@ }, "dependencies": { "@forge/api": "workspace:*", - "@forge/consts": "workspace:*", "@forge/ui": "workspace:*", "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-query": "^5.69.0", diff --git a/apps/blade/src/lib/utils.ts b/apps/blade/src/lib/utils.ts index d57da175b..1c2786112 100644 --- a/apps/blade/src/lib/utils.ts +++ b/apps/blade/src/lib/utils.ts @@ -1,10 +1,6 @@ import type { AnyTRPCProcedure, AnyTRPCRouter } from "@trpc/server"; import type { z } from "zod"; -import type { EventTagsColor } from "@forge/consts/knight-hacks"; -import type { HackerClass } from "@forge/db/schemas/knight-hacks"; -import { PERMISSION_DATA, PERMISSIONS } from "@forge/consts/knight-hacks"; - export const formatDateTime = (date: Date) => { // Create a new Date object 5 hours behind the original const adjustedDate = new Date(date.getTime()); diff --git a/apps/club/src/app/_components/landing/discover.tsx b/apps/club/src/app/_components/landing/discover.tsx index d5ac34d1a..b2938c99f 100644 --- a/apps/club/src/app/_components/landing/discover.tsx +++ b/apps/club/src/app/_components/landing/discover.tsx @@ -6,7 +6,7 @@ import { useGSAP } from "@gsap/react"; import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; -import { PERMANENT_DISCORD_INVITE } from "@forge/consts/knight-hacks"; +import { DISCORD } from "@forge/consts"; import CoolButton2 from "./assets/coolbutton2"; import NeonTkSVG from "./assets/neon-tk"; @@ -108,7 +108,7 @@ export default function Discover({ memberCount }: { memberCount: number }) { className="flex w-[300px] items-center justify-center md:w-[450px]" onClick={() => window.open( - PERMANENT_DISCORD_INVITE as string, + DISCORD.PERMANENT_INVITE as string, "_blank", "noopener,noreferrer", ) diff --git a/apps/club/src/app/officers/_components/officers.tsx b/apps/club/src/app/officers/_components/officers.tsx index f83809eb7..c48e12bca 100644 --- a/apps/club/src/app/officers/_components/officers.tsx +++ b/apps/club/src/app/officers/_components/officers.tsx @@ -5,7 +5,7 @@ import { useGSAP } from "@gsap/react"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; -import { OFFICERS } from "@forge/consts/knight-hacks"; +import { OFFICERS } from "@forge/consts"; import OfficerCard from "./assets/officer-card"; @@ -45,7 +45,7 @@ export default function Officers() { return (
- {OFFICERS.map((officer, index) => ( + {OFFICERS.LIST.map((officer, index) => (
{ diff --git a/apps/cron/src/crons/alumni-assign.ts b/apps/cron/src/crons/alumni-assign.ts index db42aaee8..bc017b3a0 100644 --- a/apps/cron/src/crons/alumni-assign.ts +++ b/apps/cron/src/crons/alumni-assign.ts @@ -5,7 +5,7 @@ import { removeRoleFromMember, resolveDiscordUserId, } from "@forge/api/utils"; -import { PROD_ALUMNI_ROLE_ID } from "@forge/consts/knight-hacks"; +import { DISCORD } from "@forge/consts"; import { db } from "@forge/db/client"; import { Member } from "@forge/db/schemas/knight-hacks"; @@ -28,7 +28,7 @@ export const alumniAssign = new CronBuilder({ for (const { discordUser } of graduatedMembers) { try { const discordId = await resolveDiscordUserId(discordUser); - if (discordId) await addRoleToMember(discordId, PROD_ALUMNI_ROLE_ID); + if (discordId) await addRoleToMember(discordId, DISCORD.ALUMNI_ROLE); } catch (err) { console.error(`Failed to add alumni role for ${discordUser}:`, err); } @@ -51,7 +51,7 @@ export const alumniAssign = new CronBuilder({ try { const discordId = await resolveDiscordUserId(discordUser); if (discordId) - await removeRoleFromMember(discordId, PROD_ALUMNI_ROLE_ID); + await removeRoleFromMember(discordId, DISCORD.ALUMNI_ROLE); } catch (err) { console.error(`Failed to remove alumni role for ${discordUser}:`, err); } diff --git a/apps/gemiknights/package.json b/apps/gemiknights/package.json index 2a585110a..fc2ffba55 100644 --- a/apps/gemiknights/package.json +++ b/apps/gemiknights/package.json @@ -17,7 +17,6 @@ "dependencies": { "@forge/api": "workspace:*", "@forge/auth": "workspace:*", - "@forge/consts": "workspace:*", "@forge/db": "workspace:*", "@forge/ui": "workspace:*", "@gsap/react": "^2.1.2", diff --git a/apps/guild/src/app/_components/dock.tsx b/apps/guild/src/app/_components/dock.tsx index bdaa39c92..79823fdfe 100644 --- a/apps/guild/src/app/_components/dock.tsx +++ b/apps/guild/src/app/_components/dock.tsx @@ -4,8 +4,8 @@ import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Search } from "lucide-react"; -import type { GuildTag } from "@forge/consts/knight-hacks"; -import { GUILD_TAG_OPTIONS } from "@forge/consts/knight-hacks"; +import type { GuildTag } from "@forge/consts"; +import { GUILD_TAG_OPTIONS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Input } from "@forge/ui/input"; import { diff --git a/apps/guild/src/app/page.tsx b/apps/guild/src/app/page.tsx index 6efefcc49..f176737c3 100644 --- a/apps/guild/src/app/page.tsx +++ b/apps/guild/src/app/page.tsx @@ -1,11 +1,11 @@ import Link from "next/link"; import { notFound } from "next/navigation"; +import type { GuildTag } from "@forge/consts"; +import { GUILD_TAG_OPTIONS } from "@forge/consts"; // import { ExternalLink, Github, Linkedin, Search } from "lucide-react"; // Moved to client component // Remove other imports only used by the card rendering if they are now fully in GuildMembersDisplay -import type { GuildTag } from "@forge/consts/knight-hacks"; -import { GUILD_TAG_OPTIONS } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; // import { Input } from "@forge/ui/input"; // Moved to Dock diff --git a/apps/tk/package.json b/apps/tk/package.json index d013f6ebe..8a494fe2f 100644 --- a/apps/tk/package.json +++ b/apps/tk/package.json @@ -14,7 +14,6 @@ "with-env": "dotenv -e ../../.env --" }, "dependencies": { - "@forge/consts": "workspace:*", "@forge/db": "workspace:*", "@forge/validators": "workspace:*", "@t3-oss/env-core": "^0.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d70b86b7..12651ebe0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,6 @@ importers: '@forge/api': specifier: workspace:* version: link:../../packages/api - '@forge/consts': - specifier: workspace:* - version: link:../../packages/consts '@forge/ui': specifier: workspace:* version: link:../../packages/ui @@ -404,7 +401,7 @@ importers: version: 7.4.4 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' version: 3.8.1 @@ -499,9 +496,6 @@ importers: '@forge/auth': specifier: workspace:* version: link:../../packages/auth - '@forge/consts': - specifier: workspace:* - version: link:../../packages/consts '@forge/db': specifier: workspace:* version: link:../../packages/db @@ -684,9 +678,6 @@ importers: apps/tk: dependencies: - '@forge/consts': - specifier: workspace:* - version: link:../../packages/consts '@forge/db': specifier: workspace:* version: link:../../packages/db @@ -891,9 +882,6 @@ importers: packages/consts: dependencies: - '@forge/db': - specifier: workspace:* - version: link:../db minimatch: specifier: ^10.1.2 version: 10.1.2 @@ -922,9 +910,6 @@ importers: packages/db: dependencies: - '@forge/api': - specifier: workspace:* - version: link:../api '@forge/consts': specifier: workspace:* version: link:../consts From bb03c47e412f70fc1e32be5501c13723bc9fcb08 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 01:12:46 -0500 Subject: [PATCH 03/23] chore(refactor): more refactoring --- apps/2025/package.json | 1 + .../src/app/admin/forms/[slug]/client.tsx | 12 ++--- .../app/admin/forms/[slug]/responses/page.tsx | 5 +- .../data/_components/HackerCharts.tsx | 4 +- .../data/_components/LevelOfStudyPie.tsx | 11 +++-- .../data/_components/ShirtSizePie.tsx | 14 +++--- .../events/_components/create-event.tsx | 6 +-- .../events/_components/delete-event.tsx | 6 +-- .../events/_components/update-event.tsx | 8 +-- .../hackers/_components/delete-hacker.tsx | 2 +- .../hackers/_components/hacker-profile.tsx | 32 +++++------- .../hackers/_components/update-hacker.tsx | 12 ++--- .../roles/configure/_components/roleedit.tsx | 2 +- .../hackathon-dashboard.tsx | 7 ++- .../hacker-dashboard/hacker-data.tsx | 2 +- .../member-dashboard/event/event-feedback.tsx | 49 ++++++++----------- .../_components/instruction-response-card.tsx | 4 +- .../_components/question-response-card.tsx | 11 ++--- .../_components/hacker-application-form.tsx | 30 ++++-------- .../_components/member-application-form.tsx | 26 ++++------ .../_components/delete-hacker-button.tsx | 2 +- .../_components/delete-member-button.tsx | 2 +- .../hacker-profile/hacker-profile-form.tsx | 25 +++------- .../src/app/settings/hacker-profile/page.tsx | 4 +- .../src/app/settings/member-profile-form.tsx | 32 ++++++------ apps/blade/src/app/sponsor/page.tsx | 3 +- .../admin/forms/instruction-edit-card.tsx | 4 +- .../admin/forms/question-edit-card.tsx | 29 +++++------ apps/blade/src/lib/utils.ts | 4 ++ .../src/app/_components/landing/discover.tsx | 4 +- .../src/app/officers/_components/officers.tsx | 4 +- apps/cron/src/crons/alumni-assign.ts | 6 +-- apps/gemiknights/package.json | 1 + apps/guild/src/app/_components/dock.tsx | 4 +- apps/guild/src/app/page.tsx | 4 +- apps/tk/package.json | 1 + pnpm-lock.yaml | 19 ++++++- 37 files changed, 183 insertions(+), 209 deletions(-) diff --git a/apps/2025/package.json b/apps/2025/package.json index 79ca99f1c..30573d3ae 100644 --- a/apps/2025/package.json +++ b/apps/2025/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@forge/api": "workspace:*", + "@forge/consts": "workspace:*", "@forge/ui": "workspace:*", "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-query": "^5.69.0", diff --git a/apps/blade/src/app/admin/forms/[slug]/client.tsx b/apps/blade/src/app/admin/forms/[slug]/client.tsx index 502c301e2..1931a7635 100644 --- a/apps/blade/src/app/admin/forms/[slug]/client.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/client.tsx @@ -24,11 +24,7 @@ import { import { CSS } from "@dnd-kit/utilities"; import { ArrowLeft, Loader2, Plus, Save, Users } from "lucide-react"; -import type { - FormType, - InstructionValidator, - QuestionValidator, -} from "@forge/consts/knight-hacks"; +import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; import { Checkbox } from "@forge/ui/checkbox"; @@ -55,8 +51,8 @@ import { api } from "~/trpc/react"; import { ConnectionViewer } from "./con-viewer"; import ListMatcher from "./linker"; -type FormQuestion = z.infer; -type FormInstruction = z.infer; +type FormQuestion = z.infer; +type FormInstruction = z.infer; type UIQuestion = FormQuestion & { id: string }; type UIInstruction = FormInstruction & { id: string }; @@ -150,7 +146,7 @@ function ConnectionsTab(props: { procs: Record; slug: string; id: string; - formData: FormType; + formData: FORMS.FormType; }) { const questions = props.formData.questions.map((q) => q.question); const { data: connections } = api.forms.getConnections.useQuery({ diff --git a/apps/blade/src/app/admin/forms/[slug]/responses/page.tsx b/apps/blade/src/app/admin/forms/[slug]/responses/page.tsx index 862a55ebc..798f2d239 100644 --- a/apps/blade/src/app/admin/forms/[slug]/responses/page.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/responses/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { redirect } from "next/navigation"; import { ArrowLeft } from "lucide-react"; -import type { FormType } from "@forge/consts/knight-hacks"; +import type { FORMS } from "@forge/consts"; import { auth } from "@forge/auth"; import { Button } from "@forge/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs"; @@ -58,8 +58,7 @@ export default async function FormResponsesPage({ ); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const formData = form.formData as any as FormType; + const formData = form.formData as FORMS.FormType; const apiResponses = await api.forms.getResponses({ form: form.id }); diff --git a/apps/blade/src/app/admin/hackathon/data/_components/HackerCharts.tsx b/apps/blade/src/app/admin/hackathon/data/_components/HackerCharts.tsx index b2cb269b6..1a3adf25e 100644 --- a/apps/blade/src/app/admin/hackathon/data/_components/HackerCharts.tsx +++ b/apps/blade/src/app/admin/hackathon/data/_components/HackerCharts.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { HACKATHON_APPLICATION_STATES } from "@forge/consts/knight-hacks"; +import { FORMS } from "@forge/consts"; import { ToggleGroup, ToggleGroupItem } from "@forge/ui/toggle-group"; import AgeBarChart from "~/app/admin/_components/AgeBarChart"; @@ -72,7 +72,7 @@ export default function HackerCharts({ hackathonId }: { hackathonId: string }) { all - {HACKATHON_APPLICATION_STATES.map((item) => ( + {FORMS.HACKATHON_APPLICATION_STATES.map((item) => ( diff --git a/apps/blade/src/app/admin/hackathon/data/_components/ShirtSizePie.tsx b/apps/blade/src/app/admin/hackathon/data/_components/ShirtSizePie.tsx index 1dfbeac56..aa1991b8f 100644 --- a/apps/blade/src/app/admin/hackathon/data/_components/ShirtSizePie.tsx +++ b/apps/blade/src/app/admin/hackathon/data/_components/ShirtSizePie.tsx @@ -4,9 +4,8 @@ import type { PieSectorDataItem } from "recharts/types/polar/Pie"; import { useEffect, useMemo, useState } from "react"; import { Cell, Label, Pie, PieChart, Sector } from "recharts"; -import type { SHIRT_SIZES } from "@forge/consts/knight-hacks"; import type { ChartConfig } from "@forge/ui/chart"; -import { ADMIN_PIE_CHART_COLORS } from "@forge/consts/knight-hacks"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -23,7 +22,7 @@ import { } from "@forge/ui/select"; interface Hacker { - shirtSize?: (typeof SHIRT_SIZES)[number]; + shirtSize?: (typeof FORMS.SHIRT_SIZES)[number]; } export default function ShirtSizePie({ hackers }: { hackers: Hacker[] }) { @@ -66,7 +65,10 @@ export default function ShirtSizePie({ hackers }: { hackers: Hacker[] }) { if (shirtSize && !baseConfig[shirtSize]) { baseConfig[shirtSize] = { label: shirtSize, - color: ADMIN_PIE_CHART_COLORS[colorIdx % ADMIN_PIE_CHART_COLORS.length], + color: + FORMS.ADMIN_PIE_CHART_COLORS[ + colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length + ], }; colorIdx++; } @@ -197,8 +199,8 @@ export default function ShirtSizePie({ hackers }: { hackers: Hacker[] }) { diff --git a/apps/blade/src/app/admin/hackathon/events/_components/create-event.tsx b/apps/blade/src/app/admin/hackathon/events/_components/create-event.tsx index e00c3fb0f..89ff35b70 100644 --- a/apps/blade/src/app/admin/hackathon/events/_components/create-event.tsx +++ b/apps/blade/src/app/admin/hackathon/events/_components/create-event.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Loader2, Plus } from "lucide-react"; import { z } from "zod"; -import { EVENT_TAGS } from "@forge/consts/knight-hacks"; +import { EVENTS } from "@forge/consts"; import { InsertEventSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; @@ -101,7 +101,7 @@ export function CreateEventButton() { description: "", dues_paying: false, location: "", - tag: EVENT_TAGS[0], + tag: EVENTS.EVENT_TAGS[0], hackathonId: "none", startDate: "", endDate: "", @@ -261,7 +261,7 @@ export function CreateEventButton() { - {EVENT_TAGS.map((tagOption) => ( + {EVENTS.EVENT_TAGS.map((tagOption) => ( {tagOption} diff --git a/apps/blade/src/app/admin/hackathon/events/_components/delete-event.tsx b/apps/blade/src/app/admin/hackathon/events/_components/delete-event.tsx index 65237c76d..f59e57a4c 100644 --- a/apps/blade/src/app/admin/hackathon/events/_components/delete-event.tsx +++ b/apps/blade/src/app/admin/hackathon/events/_components/delete-event.tsx @@ -3,8 +3,8 @@ import { useState } from "react"; import { Loader2, Trash2 } from "lucide-react"; -import type { EVENT_TAGS } from "@forge/consts/knight-hacks"; -import { USE_CAUTION } from "@forge/consts/knight-hacks"; +import type { EVENTS } from "@forge/consts"; +import { USE_CAUTION } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -26,7 +26,7 @@ interface DeleteEventButtonProps { discordId: string; googleId: string; name: string; - tag: (typeof EVENT_TAGS)[number]; + tag: (typeof EVENTS.EVENT_TAGS)[number]; }; } diff --git a/apps/blade/src/app/admin/hackathon/events/_components/update-event.tsx b/apps/blade/src/app/admin/hackathon/events/_components/update-event.tsx index dce6d8a9d..9b681457e 100644 --- a/apps/blade/src/app/admin/hackathon/events/_components/update-event.tsx +++ b/apps/blade/src/app/admin/hackathon/events/_components/update-event.tsx @@ -5,7 +5,7 @@ import { Loader2, Pencil } from "lucide-react"; import { z } from "zod"; import type { InsertEvent } from "@forge/db/schemas/knight-hacks"; -import { EVENT_POINTS, EVENT_TAGS } from "@forge/consts/knight-hacks"; +import { EVENTS } from "@forge/consts"; import { InsertEventSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; @@ -157,14 +157,14 @@ export function UpdateEventButton({ event }: { event: InsertEvent }) { endHour: endHour, endMinute: endMinute, endAmPm: endAmPm as "AM" | "PM", - points: EVENT_POINTS[event.tag], + points: EVENTS.EVENT_POINTS[event.tag], }, }); // Auto-update points when tag changes useEffect(() => { const currentTag = form.getValues("tag"); - const points = EVENT_POINTS[currentTag] || 0; + const points = EVENTS.EVENT_POINTS[currentTag] || 0; form.setValue("points", points); }, [form.watch("tag")]); @@ -305,7 +305,7 @@ export function UpdateEventButton({ event }: { event: InsertEvent }) { - {EVENT_TAGS.map((tagOption) => ( + {EVENTS.EVENT_TAGS.map((tagOption) => ( {tagOption} diff --git a/apps/blade/src/app/admin/hackathon/hackers/_components/delete-hacker.tsx b/apps/blade/src/app/admin/hackathon/hackers/_components/delete-hacker.tsx index 6bbf5c0bb..1438c4c6d 100644 --- a/apps/blade/src/app/admin/hackathon/hackers/_components/delete-hacker.tsx +++ b/apps/blade/src/app/admin/hackathon/hackers/_components/delete-hacker.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Loader2, Trash2 } from "lucide-react"; import type { InsertHacker } from "@forge/db/schemas/knight-hacks"; -import { USE_CAUTION } from "@forge/consts/knight-hacks"; +import { USE_CAUTION } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, diff --git a/apps/blade/src/app/admin/hackathon/hackers/_components/hacker-profile.tsx b/apps/blade/src/app/admin/hackathon/hackers/_components/hacker-profile.tsx index e125695e8..6d192939c 100644 --- a/apps/blade/src/app/admin/hackathon/hackers/_components/hacker-profile.tsx +++ b/apps/blade/src/app/admin/hackathon/hackers/_components/hacker-profile.tsx @@ -6,13 +6,6 @@ import { User } from "lucide-react"; import { FaGithub, FaGlobe, FaLinkedin } from "react-icons/fa"; import type { InsertHacker } from "@forge/db/schemas/knight-hacks"; -import { - LEVELS_OF_STUDY, - MEMBER_PROFILE_ICON_SIZE, - RACES_OR_ETHNICITIES, - SHORT_LEVELS_OF_STUDY, - SHORT_RACES_AND_ETHNICITIES, -} from "@forge/consts/knight-hacks"; import { Badge } from "@forge/ui/badge"; import { Button } from "@forge/ui/button"; import { @@ -26,6 +19,7 @@ import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; import FoodRestrictionsButton from "./food-restrictions"; +import { FORMS, MEMBER_PROFILE_ICON_SIZE } from '@forge/consts'; export default function HackerProfileButton({ hacker, @@ -127,12 +121,12 @@ export default function HackerProfileButton({

Level Of Study:{" "} - {hacker.levelOfStudy === LEVELS_OF_STUDY[2] // Undergraduate University (2 year - community college or similar) - ? SHORT_LEVELS_OF_STUDY[0] // Undergraduate University (2 year) - : hacker.levelOfStudy === LEVELS_OF_STUDY[4] // Graduate University (Masters, Professional, Doctoral, etc) - ? SHORT_LEVELS_OF_STUDY[1] // Graduate University (Masters/PhD) - : hacker.levelOfStudy === LEVELS_OF_STUDY[6] // Other Vocational / Trade Program or Apprenticeship - ? SHORT_LEVELS_OF_STUDY[2] // Vocational/Trade School + {hacker.levelOfStudy === FORMS.LEVELS_OF_STUDY[2] // Undergraduate University (2 year - community college or similar) + ? FORMS.SHORT_LEVELS_OF_STUDY[0] // Undergraduate University (2 year) + : hacker.levelOfStudy === FORMS.LEVELS_OF_STUDY[4] // Graduate University (Masters, Professional, Doctoral, etc) + ? FORMS.SHORT_LEVELS_OF_STUDY[1] // Graduate University (Masters/PhD) + : hacker.levelOfStudy === FORMS.LEVELS_OF_STUDY[6] // Other Vocational / Trade Program or Apprenticeship + ? FORMS.SHORT_LEVELS_OF_STUDY[2] // Vocational/Trade School : hacker.levelOfStudy}

@@ -150,12 +144,12 @@ export default function HackerProfileButton({

Race Or Ethnicity:{" "} - {hacker.raceOrEthnicity === RACES_OR_ETHNICITIES[4] // Native Hawaiian or Other Pacific Islander - ? SHORT_RACES_AND_ETHNICITIES[0] // Native Hawaiian/Pacific Islander - : hacker.raceOrEthnicity === RACES_OR_ETHNICITIES[2] // Hispanic / Latino / Spanish Origin - ? SHORT_RACES_AND_ETHNICITIES[1] // Hispanic/Latino - : hacker.raceOrEthnicity === RACES_OR_ETHNICITIES[5] // Native American or Alaskan Native - ? SHORT_RACES_AND_ETHNICITIES[2] // Native American/Alaskan Native + {hacker.raceOrEthnicity === FORMS.RACES_OR_ETHNICITIES[4] // Native Hawaiian or Other Pacific Islander + ? FORMS.SHORT_RACES_AND_ETHNICITIES[0] // Native Hawaiian/Pacific Islander + : hacker.raceOrEthnicity === FORMS.RACES_OR_ETHNICITIES[2] // Hispanic / Latino / Spanish Origin + ? FORMS.SHORT_RACES_AND_ETHNICITIES[1] // Hispanic/Latino + : hacker.raceOrEthnicity === FORMS.RACES_OR_ETHNICITIES[5] // Native American or Alaskan Native + ? FORMS.SHORT_RACES_AND_ETHNICITIES[2] // Native American/Alaskan Native : hacker.raceOrEthnicity}

diff --git a/apps/blade/src/app/admin/hackathon/hackers/_components/update-hacker.tsx b/apps/blade/src/app/admin/hackathon/hackers/_components/update-hacker.tsx index 86905df91..402b4bec3 100644 --- a/apps/blade/src/app/admin/hackathon/hackers/_components/update-hacker.tsx +++ b/apps/blade/src/app/admin/hackathon/hackers/_components/update-hacker.tsx @@ -5,11 +5,7 @@ import { Loader2, Pencil } from "lucide-react"; import { z } from "zod"; import type { InsertHacker } from "@forge/db/schemas/knight-hacks"; -import { - LEVELS_OF_STUDY, - SCHOOLS, - SHIRT_SIZES, -} from "@forge/consts/knight-hacks"; +import { FORMS } from "@forge/consts"; import { InsertHackerSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { @@ -255,7 +251,7 @@ export default function UpdateHackerButton({
{school}
} getItemValue={(school) => school} getItemLabel={(school) => school} @@ -308,7 +304,7 @@ export default function UpdateHackerButton({
- {LEVELS_OF_STUDY.map((level) => ( + {FORMS.LEVELS_OF_STUDY.map((level) => ( {level} @@ -342,7 +338,7 @@ export default function UpdateHackerButton({ - {SHIRT_SIZES.map((shirt_size) => ( + {FORMS.SHIRT_SIZES.map((shirt_size) => ( {shirt_size} diff --git a/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx b/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx index 08b1c8f24..a55ba0e05 100644 --- a/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx +++ b/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from "react"; import { Link, Loader2, Pencil, User, X } from "lucide-react"; import { z } from "zod"; -import { PERMISSION_DATA, PERMISSIONS } from "@forge/consts/knight-hacks"; +import { PERMISSION_DATA, PERMISSIONS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; import { diff --git a/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx b/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx index 9f38e83b4..0d756d927 100644 --- a/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx +++ b/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx @@ -1,6 +1,6 @@ import type { Metadata } from "next"; -import { HACKER_CLASS_INFO } from "@forge/consts/knight-hacks"; +import { DISCORD } from "@forge/consts"; import type { api as serverCall } from "~/trpc/server"; import { HackerAppCard } from "~/app/_components/option-cards"; @@ -42,7 +42,10 @@ export default async function HackathonDashboard({ ); } - if (!hacker.class || !(hacker.class in HACKER_CLASS_INFO)) { + if ( + !hacker.class || + !(hacker.class in DISCORD.KNIGHTHACKS_8.HACKER_CLASS_INFO) + ) { return (
diff --git a/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx b/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx index cd6faf306..ca6fe7a21 100644 --- a/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx +++ b/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx @@ -4,8 +4,8 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import { CircleCheckBig, Loader2 } from "lucide-react"; -import { USE_CAUTION } from "@forge/consts/knight-hacks"; import { HACKATHON_TEMPLATE_IDS } from "@forge/email/client"; +import { USE_CAUTION } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, 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 db465fb5d..33e96158b 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 @@ -5,14 +5,7 @@ import { Loader2 } from "lucide-react"; import { z } from "zod"; import type { InsertMember, SelectEvent } from "@forge/db/schemas/knight-hacks"; -import { - EVENT_FEEDBACK_HEARD, - EVENT_FEEDBACK_SIMILAR_EVENT, - EVENT_FEEDBACK_SLIDER_MAXIMUM, - EVENT_FEEDBACK_SLIDER_MINIMUM, - EVENT_FEEDBACK_SLIDER_STEP, - EVENT_FEEDBACK_TEXT_ROWS, -} from "@forge/consts/knight-hacks"; +import { EVENTS, FORMS } from "@forge/consts"; import { InsertEventFeedbackSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { @@ -100,19 +93,19 @@ export function EventFeedbackForm({ eventId: z.string().nonempty(), overallEventRating: z .number() - .min(EVENT_FEEDBACK_SLIDER_MINIMUM) - .max(EVENT_FEEDBACK_SLIDER_MAXIMUM), + .min(EVENTS.EVENT_FEEDBACK_SLIDER_MINIMUM) + .max(EVENTS.EVENT_FEEDBACK_SLIDER_MAXIMUM), funRating: z .number() - .min(EVENT_FEEDBACK_SLIDER_MINIMUM) - .max(EVENT_FEEDBACK_SLIDER_MAXIMUM), + .min(EVENTS.EVENT_FEEDBACK_SLIDER_MINIMUM) + .max(EVENTS.EVENT_FEEDBACK_SLIDER_MAXIMUM), learnedRating: z .number() - .min(EVENT_FEEDBACK_SLIDER_MINIMUM) - .max(EVENT_FEEDBACK_SLIDER_MAXIMUM), - heardAboutUs: z.enum(EVENT_FEEDBACK_HEARD), + .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(EVENT_FEEDBACK_SIMILAR_EVENT), + similarEvent: z.enum(EVENTS.EVENT_FEEDBACK_SIMILAR_EVENT), }), defaultValues: { memberId: member.id, @@ -167,9 +160,9 @@ export function EventFeedbackForm({

1

{ field.onChange(value[0]); setEventOverallValue(value[0] ?? 5); @@ -195,9 +188,9 @@ export function EventFeedbackForm({

1

{ field.onChange(value[0]); setFunValue(value[0] ?? 5); @@ -223,9 +216,9 @@ export function EventFeedbackForm({

1

{ field.onChange(value[0]); setLearnedValue(value[0] ?? 5); @@ -258,7 +251,7 @@ export function EventFeedbackForm({ - {EVENT_FEEDBACK_HEARD.map((place) => ( + {FORMS.EVENT_FEEDBACK_HEARD.map((place) => ( {place} @@ -285,7 +278,7 @@ export function EventFeedbackForm({ value={field.value} className="flex flex-col items-center" > - {EVENT_FEEDBACK_SIMILAR_EVENT.map((option) => ( + {EVENTS.EVENT_FEEDBACK_SIMILAR_EVENT.map((option) => (
diff --git a/apps/blade/src/app/forms/[formName]/_components/instruction-response-card.tsx b/apps/blade/src/app/forms/[formName]/_components/instruction-response-card.tsx index 114fa4736..1af12d9ae 100644 --- a/apps/blade/src/app/forms/[formName]/_components/instruction-response-card.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/instruction-response-card.tsx @@ -3,10 +3,10 @@ import type { z } from "zod"; import Image from "next/image"; -import type { InstructionValidator } from "@forge/consts/knight-hacks"; +import type { FORMS } from "@forge/consts"; import { Card } from "@forge/ui/card"; -type FormInstruction = z.infer; +type FormInstruction = z.infer; interface InstructionResponseCardProps { instruction: FormInstruction; diff --git a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx index 631dc891d..fc031d623 100644 --- a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx @@ -6,8 +6,6 @@ import { useRef, useState } from "react"; import Image from "next/image"; import { FileUp, Loader2, X } from "lucide-react"; -import type { QuestionValidator } from "@forge/consts/knight-hacks"; -import { getDropdownOptionsFromConst } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; import { Checkbox } from "@forge/ui/checkbox"; @@ -29,8 +27,9 @@ import { TimePicker } from "@forge/ui/time-picker"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; +import { FORMS } from '@forge/consts'; -type FormQuestion = z.infer; +type FormQuestion = z.infer; interface QuestionResponseCardProps { question: FormQuestion; @@ -353,7 +352,7 @@ function MultipleChoiceInput({ }) { // If optionsConst is set, load options from constants instead of question.options const options = question.optionsConst - ? getDropdownOptionsFromConst(question.optionsConst) + ? FORMS.getDropdownOptionsFromConst(question.optionsConst) : question.options || []; const questionKey = question.question.replace(/\s+/g, "-").toLowerCase(); const [otherText, setOtherText] = useState(""); @@ -465,7 +464,7 @@ function CheckboxesInput({ }) { // If optionsConst is set, load options from constants instead of question.options const options = question.optionsConst - ? getDropdownOptionsFromConst(question.optionsConst) + ? FORMS.getDropdownOptionsFromConst(question.optionsConst) : question.options || []; const selectedValues = value || []; const questionKey = question.question.replace(/\s+/g, "-").toLowerCase(); @@ -618,7 +617,7 @@ function DropdownInput({ }) { // If optionsConst is set, load options from constants instead of question.options const options = question.optionsConst - ? getDropdownOptionsFromConst(question.optionsConst) + ? FORMS.getDropdownOptionsFromConst(question.optionsConst) : question.options || []; // Use ResponsiveComboBox for dropdowns with more than 15 options diff --git a/apps/blade/src/app/hacker/application/_components/hacker-application-form.tsx b/apps/blade/src/app/hacker/application/_components/hacker-application-form.tsx index 6a5dee164..27f5fe5ec 100644 --- a/apps/blade/src/app/hacker/application/_components/hacker-application-form.tsx +++ b/apps/blade/src/app/hacker/application/_components/hacker-application-form.tsx @@ -6,17 +6,7 @@ import { useRouter } from "next/navigation"; import { Loader2 } from "lucide-react"; import { z } from "zod"; -import { - ALLERGIES, - COUNTRIES, - GENDERS, - KNIGHTHACKS_MAX_RESUME_SIZE, - LEVELS_OF_STUDY, - MAJORS, - RACES_OR_ETHNICITIES, - SCHOOLS, - SHIRT_SIZES, -} from "@forge/consts/knight-hacks"; +import { FORMS, KNIGHTHACKS_MAX_RESUME_SIZE } from "@forge/consts"; import { InsertHackerSchema } from "@forge/db/schemas/knight-hacks"; import { HACKATHON_TEMPLATE_IDS } from "@forge/email/client"; import { Badge } from "@forge/ui/badge"; @@ -355,7 +345,7 @@ export function HackerFormPage({ email: values.email, dob: values.dob, phoneNumber: values.phoneNumber, - country: values.country as (typeof COUNTRIES)[number], + country: values.country as (typeof FORMS.COUNTRIES)[number], school: values.school, major: values.major, levelOfStudy: values.levelOfStudy, @@ -510,7 +500,7 @@ export function HackerFormPage({
{country}
} getItemValue={(country) => country} getItemLabel={(country) => country} @@ -543,7 +533,7 @@ export function HackerFormPage({
- {GENDERS.map((gender) => ( + {FORMS.GENDERS.map((gender) => ( {gender} @@ -575,7 +565,7 @@ export function HackerFormPage({ - {RACES_OR_ETHNICITIES.map((race) => ( + {FORMS.RACES_OR_ETHNICITIES.map((race) => ( {race} @@ -603,7 +593,7 @@ export function HackerFormPage({ - {LEVELS_OF_STUDY.map((level) => ( + {FORMS.LEVELS_OF_STUDY.map((level) => ( {level} @@ -626,7 +616,7 @@ export function HackerFormPage({
{school}
} getItemValue={(school) => school} getItemLabel={(school) => school} @@ -650,7 +640,7 @@ export function HackerFormPage({
{major}
} getItemValue={(major) => major} getItemLabel={(major) => major} @@ -694,7 +684,7 @@ export function HackerFormPage({
- {SHIRT_SIZES.map((size) => ( + {FORMS.SHIRT_SIZES.map((size) => ( {size} @@ -884,7 +874,7 @@ export function HackerFormPage({ className="w-full min-w-[var(--radix-popover-trigger-width)] max-w-none p-1" >
- {ALLERGIES.map((allergy) => ( + {FORMS.ALLERGIES.map((allergy) => (
{ diff --git a/apps/blade/src/app/member/application/_components/member-application-form.tsx b/apps/blade/src/app/member/application/_components/member-application-form.tsx index c0f2f7a83..ee6b922b4 100644 --- a/apps/blade/src/app/member/application/_components/member-application-form.tsx +++ b/apps/blade/src/app/member/application/_components/member-application-form.tsx @@ -6,21 +6,15 @@ import { useRouter } from "next/navigation"; import { Loader2 } from "lucide-react"; import { z } from "zod"; -import type { GradTerm } from "@forge/consts/knight-hacks"; import { ALLOWED_PROFILE_PICTURE_EXTENSIONS, ALLOWED_PROFILE_PICTURE_TYPES, - COMPANIES, - GENDERS, + FORMS, + GradTerm, KNIGHTHACKS_MAX_PROFILE_PICTURE_SIZE, KNIGHTHACKS_MAX_RESUME_SIZE, - LEVELS_OF_STUDY, - MAJORS, - RACES_OR_ETHNICITIES, - SCHOOLS, - SHIRT_SIZES, TERM_TO_DATE, -} from "@forge/consts/knight-hacks"; +} from "@forge/consts"; import { InsertMemberSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; @@ -489,7 +483,7 @@ export function MemberApplicationForm() { - {GENDERS.map((item) => ( + {FORMS.GENDERS.map((item) => ( {item} @@ -519,7 +513,7 @@ export function MemberApplicationForm() { - {RACES_OR_ETHNICITIES.map((item) => ( + {FORMS.RACES_OR_ETHNICITIES.map((item) => ( {item} @@ -545,7 +539,7 @@ export function MemberApplicationForm() { - {SHIRT_SIZES.map((item) => ( + {FORMS.SHIRT_SIZES.map((item) => ( {item} @@ -573,7 +567,7 @@ export function MemberApplicationForm() { - {LEVELS_OF_STUDY.map((item) => ( + {FORMS.LEVELS_OF_STUDY.map((item) => ( {item} @@ -594,7 +588,7 @@ export function MemberApplicationForm() {
{item}
} getItemValue={(item) => item} getItemLabel={(item) => item} @@ -617,7 +611,7 @@ export function MemberApplicationForm() {
{item}
} getItemValue={(item) => item} getItemLabel={(item) => item} @@ -696,7 +690,7 @@ export function MemberApplicationForm() {
{item}
} getItemValue={(item) => item} getItemLabel={(item) => item} diff --git a/apps/blade/src/app/settings/_components/delete-hacker-button.tsx b/apps/blade/src/app/settings/_components/delete-hacker-button.tsx index c9891ef54..56551b4d7 100644 --- a/apps/blade/src/app/settings/_components/delete-hacker-button.tsx +++ b/apps/blade/src/app/settings/_components/delete-hacker-button.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { Loader2, Trash2 } from "lucide-react"; -import { USE_CAUTION } from "@forge/consts/knight-hacks"; +import { USE_CAUTION } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, diff --git a/apps/blade/src/app/settings/_components/delete-member-button.tsx b/apps/blade/src/app/settings/_components/delete-member-button.tsx index 3013cd535..3b2c611f0 100644 --- a/apps/blade/src/app/settings/_components/delete-member-button.tsx +++ b/apps/blade/src/app/settings/_components/delete-member-button.tsx @@ -4,7 +4,6 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { Loader2, Trash2 } from "lucide-react"; -import { USE_CAUTION } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -19,6 +18,7 @@ import { Input } from "@forge/ui/input"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; +import { USE_CAUTION } from '@forge/consts'; export default function DeleteMemberButton({ memberId, diff --git a/apps/blade/src/app/settings/hacker-profile/hacker-profile-form.tsx b/apps/blade/src/app/settings/hacker-profile/hacker-profile-form.tsx index 38ae4f7c1..18ee99531 100644 --- a/apps/blade/src/app/settings/hacker-profile/hacker-profile-form.tsx +++ b/apps/blade/src/app/settings/hacker-profile/hacker-profile-form.tsx @@ -5,16 +5,7 @@ import Link from "next/link"; import { Loader2 } from "lucide-react"; import { z } from "zod"; -import { - ALLERGIES, - GENDERS, - KNIGHTHACKS_MAX_RESUME_SIZE, - LEVELS_OF_STUDY, - MAJORS, - RACES_OR_ETHNICITIES, - SCHOOLS, - SHIRT_SIZES, -} from "@forge/consts/knight-hacks"; +import { FORMS, KNIGHTHACKS_MAX_RESUME_SIZE } from "@forge/consts"; import { InsertHackerSchema } from "@forge/db/schemas/knight-hacks"; import { Badge } from "@forge/ui/badge"; import { Button } from "@forge/ui/button"; @@ -426,7 +417,7 @@ export function HackerProfileForm({
- {GENDERS.map((gender) => ( + {FORMS.GENDERS.map((gender) => ( {gender} @@ -461,7 +452,7 @@ export function HackerProfileForm({
- {RACES_OR_ETHNICITIES.map((race) => ( + {FORMS.RACES_OR_ETHNICITIES.map((race) => ( {race} @@ -492,7 +483,7 @@ export function HackerProfileForm({
- {SHIRT_SIZES.map((size) => ( + {FORMS.SHIRT_SIZES.map((size) => ( {size} @@ -529,7 +520,7 @@ export function HackerProfileForm({ - {LEVELS_OF_STUDY.map((level) => ( + {FORMS.LEVELS_OF_STUDY.map((level) => ( {level} @@ -551,7 +542,7 @@ export function HackerProfileForm({
{school}
} getItemValue={(school) => school} getItemLabel={(school) => school} @@ -574,7 +565,7 @@ export function HackerProfileForm({
{major}
} getItemValue={(major) => major} getItemLabel={(major) => major} @@ -793,7 +784,7 @@ export function HackerProfileForm({ className="w-full min-w-[var(--radix-popover-trigger-width)] max-w-none p-1" >
- {ALLERGIES.map((allergy) => ( + {FORMS.ALLERGIES.map((allergy) => (
{ diff --git a/apps/blade/src/app/settings/hacker-profile/page.tsx b/apps/blade/src/app/settings/hacker-profile/page.tsx index c6cb00c6a..8d4fa37cf 100644 --- a/apps/blade/src/app/settings/hacker-profile/page.tsx +++ b/apps/blade/src/app/settings/hacker-profile/page.tsx @@ -3,11 +3,11 @@ import Link from "next/link"; import { redirect } from "next/navigation"; import { auth } from "@forge/auth"; -import { PERMANENT_DISCORD_INVITE } from "@forge/consts/knight-hacks"; import { Separator } from "@forge/ui/separator"; import { api, HydrateClient } from "~/trpc/server"; import { HackerProfileForm } from "./hacker-profile-form"; +import { DISCORD } from '@forge/consts'; export default async function SettingsProfilePage() { const session = await auth(); @@ -41,7 +41,7 @@ export default async function SettingsProfilePage() {

Please reach out to an organizer in the{" "} - {GENDERS.map((gender) => ( + {FORMS.GENDERS.map((gender) => ( {gender} @@ -513,7 +507,7 @@ export function MemberProfileForm({ - {RACES_OR_ETHNICITIES.map((raceOrEthnicity) => ( + {FORMS.RACES_OR_ETHNICITIES.map((raceOrEthnicity) => ( - {SHIRT_SIZES.map((size) => ( + {FORMS.SHIRT_SIZES.map((size) => ( {size} @@ -586,7 +580,7 @@ export function MemberProfileForm({ - {LEVELS_OF_STUDY.map((levelOfStudy) => ( + {FORMS.LEVELS_OF_STUDY.map((levelOfStudy) => ( {levelOfStudy} @@ -608,7 +602,7 @@ export function MemberProfileForm({

{school}
} getItemValue={(school) => school} getItemLabel={(school) => school} @@ -631,7 +625,7 @@ export function MemberProfileForm({
{major}
} getItemValue={(major) => major} getItemLabel={(major) => major} @@ -716,7 +710,7 @@ export function MemberProfileForm({
{item}
} getItemValue={(item) => item} getItemLabel={(item) => item} @@ -731,7 +725,9 @@ export function MemberProfileForm({ }} buttonPlaceholder={ member.company && - (COMPANIES as readonly string[]).includes(member.company) + (FORMS.COMPANIES as readonly string[]).includes( + member.company, + ) ? member.company : member.company ? "Other" diff --git a/apps/blade/src/app/sponsor/page.tsx b/apps/blade/src/app/sponsor/page.tsx index 78d3b89eb..ed48e4c50 100644 --- a/apps/blade/src/app/sponsor/page.tsx +++ b/apps/blade/src/app/sponsor/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Mail } from "lucide-react"; -import { SPONSOR_VIDEO_LINK } from "@forge/consts/knight-hacks"; +import { SPONSOR_VIDEO_LINK } from "@forge/consts"; import { Button } from "@forge/ui/button"; export const metadata: Metadata = { @@ -9,6 +9,7 @@ export const metadata: Metadata = { description: "Help us make dreams!", }; +// TODO: move to consts const SPONSOR_VIDEO_LINK_2 = "https://www.youtube.com/embed/OzW_4QeCfM0?si=G8SUf8UbEo2W5MnL"; diff --git a/apps/blade/src/components/admin/forms/instruction-edit-card.tsx b/apps/blade/src/components/admin/forms/instruction-edit-card.tsx index aa8978faa..f5a83d8d7 100644 --- a/apps/blade/src/components/admin/forms/instruction-edit-card.tsx +++ b/apps/blade/src/components/admin/forms/instruction-edit-card.tsx @@ -16,7 +16,7 @@ import { X, } from "lucide-react"; -import type { InstructionValidator } from "@forge/consts/knight-hacks"; +import type { FORMS } from "@forge/consts"; import { cn } from "@forge/ui"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; @@ -26,7 +26,7 @@ import { useMediaQuery } from "@forge/ui/use-media-query"; import { api } from "~/trpc/react"; -type FormInstruction = z.infer; +type FormInstruction = z.infer; interface InstructionEditCardProps { instruction: FormInstruction & { id: string }; diff --git a/apps/blade/src/components/admin/forms/question-edit-card.tsx b/apps/blade/src/components/admin/forms/question-edit-card.tsx index feb70bc21..e00313ac5 100644 --- a/apps/blade/src/components/admin/forms/question-edit-card.tsx +++ b/apps/blade/src/components/admin/forms/question-edit-card.tsx @@ -27,12 +27,7 @@ import { X, } from "lucide-react"; -import type { QuestionValidator } from "@forge/consts/knight-hacks"; -import { - AVAILABLE_DROPDOWN_CONSTANTS, - FORM_QUESTION_TYPES, - getDropdownOptionsFromConst, -} from "@forge/consts/knight-hacks"; +import { FORMS } from "@forge/consts"; import { cn } from "@forge/ui"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; @@ -52,7 +47,7 @@ import { Textarea } from "@forge/ui/textarea"; import { TimePicker } from "@forge/ui/time-picker"; import { useMediaQuery } from "@forge/ui/use-media-query"; -type FormQuestion = z.infer; +type FormQuestion = z.infer; type QuestionType = FormQuestion["type"]; interface QuestionEditCardProps { @@ -193,7 +188,7 @@ export function QuestionEditCard({ - {FORM_QUESTION_TYPES.map((type) => { + {FORMS.FORM_QUESTION_TYPES.map((type) => { const Icon = QUESTION_ICONS[type.value]; return ( @@ -492,13 +487,13 @@ function OptionList({ const isRestrictedType = question.type === "MULTIPLE_CHOICE" || question.type === "CHECKBOXES"; - const availableConstants = Object.entries(AVAILABLE_DROPDOWN_CONSTANTS).map( - ([key, label]) => { - const constOptions = getDropdownOptionsFromConst(key); - const isDisabled = isRestrictedType && constOptions.length >= 15; - return { key, label, isDisabled, length: constOptions.length }; - }, - ); + const availableConstants = Object.entries( + FORMS.AVAILABLE_DROPDOWN_CONSTANTS, + ).map(([key, label]) => { + const constOptions = FORMS.getDropdownOptionsFromConst(key); + const isDisabled = isRestrictedType && constOptions.length >= 15; + return { key, label, isDisabled, length: constOptions.length }; + }); return (
@@ -534,8 +529,8 @@ function OptionList({

Using constant:{" "} { - AVAILABLE_DROPDOWN_CONSTANTS[ - optionsConst as keyof typeof AVAILABLE_DROPDOWN_CONSTANTS + FORMS.AVAILABLE_DROPDOWN_CONSTANTS[ + optionsConst as keyof typeof FORMS.AVAILABLE_DROPDOWN_CONSTANTS ] }

diff --git a/apps/blade/src/lib/utils.ts b/apps/blade/src/lib/utils.ts index 1c2786112..d57da175b 100644 --- a/apps/blade/src/lib/utils.ts +++ b/apps/blade/src/lib/utils.ts @@ -1,6 +1,10 @@ import type { AnyTRPCProcedure, AnyTRPCRouter } from "@trpc/server"; import type { z } from "zod"; +import type { EventTagsColor } from "@forge/consts/knight-hacks"; +import type { HackerClass } from "@forge/db/schemas/knight-hacks"; +import { PERMISSION_DATA, PERMISSIONS } from "@forge/consts/knight-hacks"; + export const formatDateTime = (date: Date) => { // Create a new Date object 5 hours behind the original const adjustedDate = new Date(date.getTime()); diff --git a/apps/club/src/app/_components/landing/discover.tsx b/apps/club/src/app/_components/landing/discover.tsx index b2938c99f..d5ac34d1a 100644 --- a/apps/club/src/app/_components/landing/discover.tsx +++ b/apps/club/src/app/_components/landing/discover.tsx @@ -6,7 +6,7 @@ import { useGSAP } from "@gsap/react"; import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; -import { DISCORD } from "@forge/consts"; +import { PERMANENT_DISCORD_INVITE } from "@forge/consts/knight-hacks"; import CoolButton2 from "./assets/coolbutton2"; import NeonTkSVG from "./assets/neon-tk"; @@ -108,7 +108,7 @@ export default function Discover({ memberCount }: { memberCount: number }) { className="flex w-[300px] items-center justify-center md:w-[450px]" onClick={() => window.open( - DISCORD.PERMANENT_INVITE as string, + PERMANENT_DISCORD_INVITE as string, "_blank", "noopener,noreferrer", ) diff --git a/apps/club/src/app/officers/_components/officers.tsx b/apps/club/src/app/officers/_components/officers.tsx index c48e12bca..f83809eb7 100644 --- a/apps/club/src/app/officers/_components/officers.tsx +++ b/apps/club/src/app/officers/_components/officers.tsx @@ -5,7 +5,7 @@ import { useGSAP } from "@gsap/react"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; -import { OFFICERS } from "@forge/consts"; +import { OFFICERS } from "@forge/consts/knight-hacks"; import OfficerCard from "./assets/officer-card"; @@ -45,7 +45,7 @@ export default function Officers() { return (
- {OFFICERS.LIST.map((officer, index) => ( + {OFFICERS.map((officer, index) => (
{ diff --git a/apps/cron/src/crons/alumni-assign.ts b/apps/cron/src/crons/alumni-assign.ts index bc017b3a0..db42aaee8 100644 --- a/apps/cron/src/crons/alumni-assign.ts +++ b/apps/cron/src/crons/alumni-assign.ts @@ -5,7 +5,7 @@ import { removeRoleFromMember, resolveDiscordUserId, } from "@forge/api/utils"; -import { DISCORD } from "@forge/consts"; +import { PROD_ALUMNI_ROLE_ID } from "@forge/consts/knight-hacks"; import { db } from "@forge/db/client"; import { Member } from "@forge/db/schemas/knight-hacks"; @@ -28,7 +28,7 @@ export const alumniAssign = new CronBuilder({ for (const { discordUser } of graduatedMembers) { try { const discordId = await resolveDiscordUserId(discordUser); - if (discordId) await addRoleToMember(discordId, DISCORD.ALUMNI_ROLE); + if (discordId) await addRoleToMember(discordId, PROD_ALUMNI_ROLE_ID); } catch (err) { console.error(`Failed to add alumni role for ${discordUser}:`, err); } @@ -51,7 +51,7 @@ export const alumniAssign = new CronBuilder({ try { const discordId = await resolveDiscordUserId(discordUser); if (discordId) - await removeRoleFromMember(discordId, DISCORD.ALUMNI_ROLE); + await removeRoleFromMember(discordId, PROD_ALUMNI_ROLE_ID); } catch (err) { console.error(`Failed to remove alumni role for ${discordUser}:`, err); } diff --git a/apps/gemiknights/package.json b/apps/gemiknights/package.json index fc2ffba55..2a585110a 100644 --- a/apps/gemiknights/package.json +++ b/apps/gemiknights/package.json @@ -17,6 +17,7 @@ "dependencies": { "@forge/api": "workspace:*", "@forge/auth": "workspace:*", + "@forge/consts": "workspace:*", "@forge/db": "workspace:*", "@forge/ui": "workspace:*", "@gsap/react": "^2.1.2", diff --git a/apps/guild/src/app/_components/dock.tsx b/apps/guild/src/app/_components/dock.tsx index 79823fdfe..bdaa39c92 100644 --- a/apps/guild/src/app/_components/dock.tsx +++ b/apps/guild/src/app/_components/dock.tsx @@ -4,8 +4,8 @@ import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Search } from "lucide-react"; -import type { GuildTag } from "@forge/consts"; -import { GUILD_TAG_OPTIONS } from "@forge/consts"; +import type { GuildTag } from "@forge/consts/knight-hacks"; +import { GUILD_TAG_OPTIONS } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; import { Input } from "@forge/ui/input"; import { diff --git a/apps/guild/src/app/page.tsx b/apps/guild/src/app/page.tsx index f176737c3..6efefcc49 100644 --- a/apps/guild/src/app/page.tsx +++ b/apps/guild/src/app/page.tsx @@ -1,11 +1,11 @@ import Link from "next/link"; import { notFound } from "next/navigation"; -import type { GuildTag } from "@forge/consts"; -import { GUILD_TAG_OPTIONS } from "@forge/consts"; // import { ExternalLink, Github, Linkedin, Search } from "lucide-react"; // Moved to client component // Remove other imports only used by the card rendering if they are now fully in GuildMembersDisplay +import type { GuildTag } from "@forge/consts/knight-hacks"; +import { GUILD_TAG_OPTIONS } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; // import { Input } from "@forge/ui/input"; // Moved to Dock diff --git a/apps/tk/package.json b/apps/tk/package.json index 8a494fe2f..d013f6ebe 100644 --- a/apps/tk/package.json +++ b/apps/tk/package.json @@ -14,6 +14,7 @@ "with-env": "dotenv -e ../../.env --" }, "dependencies": { + "@forge/consts": "workspace:*", "@forge/db": "workspace:*", "@forge/validators": "workspace:*", "@t3-oss/env-core": "^0.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12651ebe0..bcc6432d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: '@forge/api': specifier: workspace:* version: link:../../packages/api + '@forge/consts': + specifier: workspace:* + version: link:../../packages/consts '@forge/ui': specifier: workspace:* version: link:../../packages/ui @@ -401,7 +404,7 @@ importers: version: 7.4.4 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) prettier: specifier: 'catalog:' version: 3.8.1 @@ -474,7 +477,7 @@ importers: version: 0.37.120 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' version: 3.8.1 @@ -496,6 +499,9 @@ importers: '@forge/auth': specifier: workspace:* version: link:../../packages/auth + '@forge/consts': + specifier: workspace:* + version: link:../../packages/consts '@forge/db': specifier: workspace:* version: link:../../packages/db @@ -678,6 +684,9 @@ importers: apps/tk: dependencies: + '@forge/consts': + specifier: workspace:* + version: link:../../packages/consts '@forge/db': specifier: workspace:* version: link:../../packages/db @@ -882,6 +891,9 @@ importers: packages/consts: dependencies: + '@forge/db': + specifier: workspace:* + version: link:../db minimatch: specifier: ^10.1.2 version: 10.1.2 @@ -910,6 +922,9 @@ importers: packages/db: dependencies: + '@forge/api': + specifier: workspace:* + version: link:../api '@forge/consts': specifier: workspace:* version: link:../consts From dc022077370508e95d1cb54ddfa28f848dbe686a Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 01:27:58 -0500 Subject: [PATCH 04/23] oops(consts): made a mistake not sure what I did --- apps/2025/package.json | 1 - apps/blade/src/lib/utils.ts | 4 ---- apps/club/src/app/_components/landing/discover.tsx | 4 ++-- apps/club/src/app/officers/_components/officers.tsx | 4 ++-- apps/cron/src/crons/alumni-assign.ts | 6 +++--- apps/gemiknights/package.json | 1 - apps/guild/src/app/_components/dock.tsx | 4 ++-- apps/guild/src/app/page.tsx | 4 ++-- 8 files changed, 11 insertions(+), 17 deletions(-) diff --git a/apps/2025/package.json b/apps/2025/package.json index 30573d3ae..79ca99f1c 100644 --- a/apps/2025/package.json +++ b/apps/2025/package.json @@ -17,7 +17,6 @@ }, "dependencies": { "@forge/api": "workspace:*", - "@forge/consts": "workspace:*", "@forge/ui": "workspace:*", "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-query": "^5.69.0", diff --git a/apps/blade/src/lib/utils.ts b/apps/blade/src/lib/utils.ts index d57da175b..1c2786112 100644 --- a/apps/blade/src/lib/utils.ts +++ b/apps/blade/src/lib/utils.ts @@ -1,10 +1,6 @@ import type { AnyTRPCProcedure, AnyTRPCRouter } from "@trpc/server"; import type { z } from "zod"; -import type { EventTagsColor } from "@forge/consts/knight-hacks"; -import type { HackerClass } from "@forge/db/schemas/knight-hacks"; -import { PERMISSION_DATA, PERMISSIONS } from "@forge/consts/knight-hacks"; - export const formatDateTime = (date: Date) => { // Create a new Date object 5 hours behind the original const adjustedDate = new Date(date.getTime()); diff --git a/apps/club/src/app/_components/landing/discover.tsx b/apps/club/src/app/_components/landing/discover.tsx index d5ac34d1a..b2938c99f 100644 --- a/apps/club/src/app/_components/landing/discover.tsx +++ b/apps/club/src/app/_components/landing/discover.tsx @@ -6,7 +6,7 @@ import { useGSAP } from "@gsap/react"; import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; -import { PERMANENT_DISCORD_INVITE } from "@forge/consts/knight-hacks"; +import { DISCORD } from "@forge/consts"; import CoolButton2 from "./assets/coolbutton2"; import NeonTkSVG from "./assets/neon-tk"; @@ -108,7 +108,7 @@ export default function Discover({ memberCount }: { memberCount: number }) { className="flex w-[300px] items-center justify-center md:w-[450px]" onClick={() => window.open( - PERMANENT_DISCORD_INVITE as string, + DISCORD.PERMANENT_INVITE as string, "_blank", "noopener,noreferrer", ) diff --git a/apps/club/src/app/officers/_components/officers.tsx b/apps/club/src/app/officers/_components/officers.tsx index f83809eb7..c48e12bca 100644 --- a/apps/club/src/app/officers/_components/officers.tsx +++ b/apps/club/src/app/officers/_components/officers.tsx @@ -5,7 +5,7 @@ import { useGSAP } from "@gsap/react"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; -import { OFFICERS } from "@forge/consts/knight-hacks"; +import { OFFICERS } from "@forge/consts"; import OfficerCard from "./assets/officer-card"; @@ -45,7 +45,7 @@ export default function Officers() { return (
- {OFFICERS.map((officer, index) => ( + {OFFICERS.LIST.map((officer, index) => (
{ diff --git a/apps/cron/src/crons/alumni-assign.ts b/apps/cron/src/crons/alumni-assign.ts index db42aaee8..bc017b3a0 100644 --- a/apps/cron/src/crons/alumni-assign.ts +++ b/apps/cron/src/crons/alumni-assign.ts @@ -5,7 +5,7 @@ import { removeRoleFromMember, resolveDiscordUserId, } from "@forge/api/utils"; -import { PROD_ALUMNI_ROLE_ID } from "@forge/consts/knight-hacks"; +import { DISCORD } from "@forge/consts"; import { db } from "@forge/db/client"; import { Member } from "@forge/db/schemas/knight-hacks"; @@ -28,7 +28,7 @@ export const alumniAssign = new CronBuilder({ for (const { discordUser } of graduatedMembers) { try { const discordId = await resolveDiscordUserId(discordUser); - if (discordId) await addRoleToMember(discordId, PROD_ALUMNI_ROLE_ID); + if (discordId) await addRoleToMember(discordId, DISCORD.ALUMNI_ROLE); } catch (err) { console.error(`Failed to add alumni role for ${discordUser}:`, err); } @@ -51,7 +51,7 @@ export const alumniAssign = new CronBuilder({ try { const discordId = await resolveDiscordUserId(discordUser); if (discordId) - await removeRoleFromMember(discordId, PROD_ALUMNI_ROLE_ID); + await removeRoleFromMember(discordId, DISCORD.ALUMNI_ROLE); } catch (err) { console.error(`Failed to remove alumni role for ${discordUser}:`, err); } diff --git a/apps/gemiknights/package.json b/apps/gemiknights/package.json index 2a585110a..fc2ffba55 100644 --- a/apps/gemiknights/package.json +++ b/apps/gemiknights/package.json @@ -17,7 +17,6 @@ "dependencies": { "@forge/api": "workspace:*", "@forge/auth": "workspace:*", - "@forge/consts": "workspace:*", "@forge/db": "workspace:*", "@forge/ui": "workspace:*", "@gsap/react": "^2.1.2", diff --git a/apps/guild/src/app/_components/dock.tsx b/apps/guild/src/app/_components/dock.tsx index bdaa39c92..79823fdfe 100644 --- a/apps/guild/src/app/_components/dock.tsx +++ b/apps/guild/src/app/_components/dock.tsx @@ -4,8 +4,8 @@ import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Search } from "lucide-react"; -import type { GuildTag } from "@forge/consts/knight-hacks"; -import { GUILD_TAG_OPTIONS } from "@forge/consts/knight-hacks"; +import type { GuildTag } from "@forge/consts"; +import { GUILD_TAG_OPTIONS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Input } from "@forge/ui/input"; import { diff --git a/apps/guild/src/app/page.tsx b/apps/guild/src/app/page.tsx index 6efefcc49..f176737c3 100644 --- a/apps/guild/src/app/page.tsx +++ b/apps/guild/src/app/page.tsx @@ -1,11 +1,11 @@ import Link from "next/link"; import { notFound } from "next/navigation"; +import type { GuildTag } from "@forge/consts"; +import { GUILD_TAG_OPTIONS } from "@forge/consts"; // import { ExternalLink, Github, Linkedin, Search } from "lucide-react"; // Moved to client component // Remove other imports only used by the card rendering if they are now fully in GuildMembersDisplay -import type { GuildTag } from "@forge/consts/knight-hacks"; -import { GUILD_TAG_OPTIONS } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; // import { Input } from "@forge/ui/input"; // Moved to Dock From 04fd5e56714d2f928cd8135b32c51c30bbf160e9 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:51:39 -0500 Subject: [PATCH 05/23] chore(refactor): more refactoring --- apps/blade/src/app/_components/bad-perms.tsx | 4 +-- .../src/app/_components/discord-modal.tsx | 4 +-- .../navigation/reusable-user-dropdown.tsx | 2 +- .../_components/navigation/user-dropdown.tsx | 2 +- .../src/app/admin/_components/GenderPie.tsx | 14 ++++---- .../app/admin/_components/MajorBarChart.tsx | 4 +-- .../admin/_components/RaceOrEthnicityPie.tsx | 22 ++++++------- .../app/admin/_components/SchoolBarChart.tsx | 4 +-- .../app/admin/_components/SchoolYearPie.tsx | 20 +++++------- .../data/_components/EventDemographics.tsx | 26 +++++++-------- .../event-data/AttendancesBarChart.tsx | 6 ++-- .../event-data/PopularityRanking.tsx | 4 +-- .../data/_components/event-data/TypePie.tsx | 8 ++--- .../event-data/WeekdayPopularityRadar.tsx | 13 +++----- .../_components/member-data/GenderPie.tsx | 11 ++++--- .../member-data/SchoolBarChart.tsx | 4 +-- .../_components/member-data/ShirtSizePie.tsx | 11 +++---- .../member-data/YearOfStudyPie.tsx | 8 ++--- .../club/events/_components/create-event.tsx | 6 ++-- .../club/events/_components/delete-event.tsx | 6 ++-- .../club/events/_components/update-event.tsx | 4 +-- .../members/_components/delete-member.tsx | 2 +- .../members/_components/final-dues-dialog.tsx | 2 +- .../members/_components/member-profile.tsx | 32 ++++++++----------- .../members/_components/update-member.tsx | 12 +++---- .../_components/AllResponsesView.tsx | 5 ++- .../_components/PerUserResponsesView.tsx | 4 +-- .../_components/ResponsePieChart.tsx | 12 +++---- 28 files changed, 116 insertions(+), 136 deletions(-) diff --git a/apps/blade/src/app/_components/bad-perms.tsx b/apps/blade/src/app/_components/bad-perms.tsx index bc1c605e6..64973c766 100644 --- a/apps/blade/src/app/_components/bad-perms.tsx +++ b/apps/blade/src/app/_components/bad-perms.tsx @@ -1,7 +1,7 @@ import { ShieldX } from "lucide-react"; -import type { PermissionKey } from "@forge/consts/knight-hacks"; -import { PERMISSION_DATA } from "@forge/consts/knight-hacks"; +import type { PermissionKey } from "@forge/consts"; +import { PERMISSION_DATA } from "@forge/consts"; export function BadPerms({ perms }: { perms: PermissionKey[] }) { const permNames: string[] = []; diff --git a/apps/blade/src/app/_components/discord-modal.tsx b/apps/blade/src/app/_components/discord-modal.tsx index ba40d8fc9..7594fb373 100644 --- a/apps/blade/src/app/_components/discord-modal.tsx +++ b/apps/blade/src/app/_components/discord-modal.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import Image from "next/image"; -import { PERMANENT_DISCORD_INVITE } from "@forge/consts/knight-hacks"; +import { DISCORD } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -48,7 +48,7 @@ export function TacoTuesday({ initialState }: { initialState: boolean }) { className="text-md w-3/4" onClick={() => window.open( - PERMANENT_DISCORD_INVITE, + DISCORD.PERMANENT_INVITE, "_blank", "noopener,noreferrer", ) diff --git a/apps/blade/src/app/_components/navigation/reusable-user-dropdown.tsx b/apps/blade/src/app/_components/navigation/reusable-user-dropdown.tsx index aef60f732..89336f3a3 100644 --- a/apps/blade/src/app/_components/navigation/reusable-user-dropdown.tsx +++ b/apps/blade/src/app/_components/navigation/reusable-user-dropdown.tsx @@ -12,7 +12,7 @@ import { Users, } from "lucide-react"; -import type { PermissionKey } from "@forge/consts/knight-hacks"; +import type { PermissionKey } from "@forge/consts"; import { USER_DROPDOWN_ICON_COLOR, USER_DROPDOWN_ICON_SIZE } from "~/consts"; diff --git a/apps/blade/src/app/_components/navigation/user-dropdown.tsx b/apps/blade/src/app/_components/navigation/user-dropdown.tsx index 667678f64..991de7c80 100644 --- a/apps/blade/src/app/_components/navigation/user-dropdown.tsx +++ b/apps/blade/src/app/_components/navigation/user-dropdown.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { LayoutDashboard } from "lucide-react"; -import type { PermissionKey } from "@forge/consts/knight-hacks"; +import type { PermissionKey } from "@forge/consts"; import { signOut } from "@forge/auth"; import { Avatar, AvatarFallback, AvatarImage } from "@forge/ui/avatar"; import { Button } from "@forge/ui/button"; diff --git a/apps/blade/src/app/admin/_components/GenderPie.tsx b/apps/blade/src/app/admin/_components/GenderPie.tsx index 78f3c9a3c..ef18aeca1 100644 --- a/apps/blade/src/app/admin/_components/GenderPie.tsx +++ b/apps/blade/src/app/admin/_components/GenderPie.tsx @@ -4,9 +4,8 @@ import type { PieSectorDataItem } from "recharts/types/polar/Pie"; import { useEffect, useMemo, useState } from "react"; import { Cell, Label, Pie, PieChart, Sector } from "recharts"; -import type { GENDERS } from "@forge/consts/knight-hacks"; import type { ChartConfig } from "@forge/ui/chart"; -import { ADMIN_PIE_CHART_COLORS } from "@forge/consts/knight-hacks"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -23,7 +22,7 @@ import { } from "@forge/ui/select"; interface Person { - gender?: (typeof GENDERS)[number]; + gender?: (typeof FORMS.GENDERS)[number]; } export default function GenderPie({ people }: { people: Person[] }) { @@ -61,7 +60,10 @@ export default function GenderPie({ people }: { people: Person[] }) { if (gender && !baseConfig[gender]) { baseConfig[gender] = { label: gender, - color: ADMIN_PIE_CHART_COLORS[colorIdx % ADMIN_PIE_CHART_COLORS.length], + color: + FORMS.ADMIN_PIE_CHART_COLORS[ + colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length + ], }; colorIdx++; } @@ -192,8 +194,8 @@ export default function GenderPie({ people }: { people: Person[] }) { diff --git a/apps/blade/src/app/admin/_components/MajorBarChart.tsx b/apps/blade/src/app/admin/_components/MajorBarChart.tsx index 8bc6a4ae4..19031fbcf 100644 --- a/apps/blade/src/app/admin/_components/MajorBarChart.tsx +++ b/apps/blade/src/app/admin/_components/MajorBarChart.tsx @@ -9,7 +9,7 @@ import { YAxis, } from "recharts"; -import type { MAJORS } from "@forge/consts/knight-hacks"; +import type { FORMS } from "@forge/consts"; import type { ChartConfig } from "@forge/ui/chart"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, ChartTooltip } from "@forge/ui/chart"; @@ -22,7 +22,7 @@ const chartConfig = { } satisfies ChartConfig; interface Person { - major?: (typeof MAJORS)[number]; + major?: (typeof FORMS.MAJORS)[number]; } export default function MajorBarChart({ people }: { people: Person[] }) { diff --git a/apps/blade/src/app/admin/_components/RaceOrEthnicityPie.tsx b/apps/blade/src/app/admin/_components/RaceOrEthnicityPie.tsx index c4a117895..d07657bc1 100644 --- a/apps/blade/src/app/admin/_components/RaceOrEthnicityPie.tsx +++ b/apps/blade/src/app/admin/_components/RaceOrEthnicityPie.tsx @@ -5,11 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import { Cell, Label, Pie, PieChart, Sector } from "recharts"; import type { ChartConfig } from "@forge/ui/chart"; -import { - ADMIN_PIE_CHART_COLORS, - RACES_OR_ETHNICITIES, - SHORT_RACES_AND_ETHNICITIES, -} from "@forge/consts/knight-hacks"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -26,17 +22,17 @@ import { } from "@forge/ui/select"; interface Person { - raceOrEthnicity?: (typeof RACES_OR_ETHNICITIES)[number]; + raceOrEthnicity?: (typeof FORMS.RACES_OR_ETHNICITIES)[number]; } const shortenRaceOrEthnicity = (raceOrEthnicity: string): string => { const replacements: Record = { // Native Hawaiian or Other Pacific Islander - [RACES_OR_ETHNICITIES[4]]: SHORT_RACES_AND_ETHNICITIES[0], // Native Hawaiian/Pacific Islander + [FORMS.RACES_OR_ETHNICITIES[4]]: FORMS.SHORT_RACES_AND_ETHNICITIES[0], // Native Hawaiian/Pacific Islander // Hispanic / Latino / Spanish Origin - [RACES_OR_ETHNICITIES[2]]: SHORT_RACES_AND_ETHNICITIES[1], // Hispanic/Latino + [FORMS.RACES_OR_ETHNICITIES[2]]: FORMS.SHORT_RACES_AND_ETHNICITIES[1], // Hispanic/Latino // Native American or Alaskan Native - [RACES_OR_ETHNICITIES[5]]: SHORT_RACES_AND_ETHNICITIES[2], // Native American/Alaskan Native + [FORMS.RACES_OR_ETHNICITIES[5]]: FORMS.SHORT_RACES_AND_ETHNICITIES[2], // Native American/Alaskan Native }; return replacements[raceOrEthnicity] ?? raceOrEthnicity; }; @@ -83,7 +79,9 @@ export default function RaceOrEthnicityPie({ people }: { people: Person[] }) { baseConfig[shortenedString] = { label: shortenedString, color: - ADMIN_PIE_CHART_COLORS[colorIdx % ADMIN_PIE_CHART_COLORS.length], + FORMS.ADMIN_PIE_CHART_COLORS[ + colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length + ], }; colorIdx++; } @@ -217,8 +215,8 @@ export default function RaceOrEthnicityPie({ people }: { people: Person[] }) { diff --git a/apps/blade/src/app/admin/_components/SchoolBarChart.tsx b/apps/blade/src/app/admin/_components/SchoolBarChart.tsx index 5e2e99a75..c61e012dc 100644 --- a/apps/blade/src/app/admin/_components/SchoolBarChart.tsx +++ b/apps/blade/src/app/admin/_components/SchoolBarChart.tsx @@ -9,8 +9,8 @@ import { YAxis, } from "recharts"; -import type { SCHOOLS } from "@forge/consts/knight-hacks"; import type { ChartConfig } from "@forge/ui/chart"; +import type { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, ChartTooltip } from "@forge/ui/chart"; @@ -22,7 +22,7 @@ const chartConfig = { } satisfies ChartConfig; interface Person { - school?: (typeof SCHOOLS)[number]; + school?: (typeof FORMS.SCHOOLS)[number]; } export default function SchoolBarChart({ people }: { people: Person[] }) { diff --git a/apps/blade/src/app/admin/_components/SchoolYearPie.tsx b/apps/blade/src/app/admin/_components/SchoolYearPie.tsx index a8de00f65..08f3ab249 100644 --- a/apps/blade/src/app/admin/_components/SchoolYearPie.tsx +++ b/apps/blade/src/app/admin/_components/SchoolYearPie.tsx @@ -5,11 +5,6 @@ import { useEffect, useMemo, useState } from "react"; import { Cell, Label, Pie, PieChart, Sector } from "recharts"; import type { ChartConfig } from "@forge/ui/chart"; -import { - ADMIN_PIE_CHART_COLORS, - LEVELS_OF_STUDY, - SHORT_LEVELS_OF_STUDY, -} from "@forge/consts/knight-hacks"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -24,19 +19,20 @@ import { SelectTrigger, SelectValue, } from "@forge/ui/select"; +import { FORMS } from '@forge/consts'; interface Person { - levelOfStudy?: (typeof LEVELS_OF_STUDY)[number]; + levelOfStudy?: (typeof FORMS.LEVELS_OF_STUDY)[number]; } const shortenLevelOfStudy = (levelOfStudy: string): string => { const replacements: Record = { // Undergraduate University (2 year - community college or similar) - [LEVELS_OF_STUDY[2]]: SHORT_LEVELS_OF_STUDY[0], // Undergraduate University (2 year) + [FORMS.LEVELS_OF_STUDY[2]]: FORMS.SHORT_LEVELS_OF_STUDY[0], // Undergraduate University (2 year) // Graduate University (Masters, Professional, Doctoral, etc) - [LEVELS_OF_STUDY[4]]: SHORT_LEVELS_OF_STUDY[1], // Graduate University (Masters/PhD) + [FORMS.LEVELS_OF_STUDY[4]]: FORMS.SHORT_LEVELS_OF_STUDY[1], // Graduate University (Masters/PhD) // Other Vocational / Trade Program or Apprenticeship - [LEVELS_OF_STUDY[6]]: SHORT_LEVELS_OF_STUDY[2], // Vocational/Trade School + [FORMS.LEVELS_OF_STUDY[6]]: FORMS.SHORT_LEVELS_OF_STUDY[2], // Vocational/Trade School }; return replacements[levelOfStudy] ?? levelOfStudy; }; @@ -84,7 +80,7 @@ export default function SchoolYearPie({ people }: { people: Person[] }) { if (shortenedString && !baseConfig[shortenedString]) { baseConfig[shortenedString] = { label: shortenedString, - color: ADMIN_PIE_CHART_COLORS[colorIdx % ADMIN_PIE_CHART_COLORS.length], + color: FORMS.ADMIN_PIE_CHART_COLORS[colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length], }; colorIdx++; } @@ -217,8 +213,8 @@ export default function SchoolYearPie({ people }: { people: Person[] }) { diff --git a/apps/blade/src/app/admin/club/data/_components/EventDemographics.tsx b/apps/blade/src/app/admin/club/data/_components/EventDemographics.tsx index 878d465f4..88ea85062 100644 --- a/apps/blade/src/app/admin/club/data/_components/EventDemographics.tsx +++ b/apps/blade/src/app/admin/club/data/_components/EventDemographics.tsx @@ -2,11 +2,6 @@ import { useState } from "react"; -import type { Semester } from "@forge/consts/knight-hacks"; -import { - ALL_DATES_RANGE_UNIX, - SEMESTER_START_DATES, -} from "@forge/consts/knight-hacks"; import { Checkbox } from "@forge/ui/checkbox"; import { Select, @@ -22,19 +17,20 @@ import AttendancesMobile from "./event-data/AttendancesMobile"; import PopularityRanking from "./event-data/PopularityRanking"; import TypePie from "./event-data/TypePie"; import { WeekdayPopularityRadar } from "./event-data/WeekdayPopularityRadar"; +import { FORMS } from '@forge/consts'; export default function EventDemographics() { const { data: events } = api.event.getEvents.useQuery(); - const semestersArr: Semester[] = [ + const semestersArr: FORMS.Semester[] = [ { name: "All Semesters", - startDate: new Date(ALL_DATES_RANGE_UNIX.start), - endDate: new Date(ALL_DATES_RANGE_UNIX.end), + startDate: new Date(FORMS.ALL_DATES_RANGE_UNIX.start), + endDate: new Date(FORMS.ALL_DATES_RANGE_UNIX.end), }, ]; // for select options const defaultSemester = semestersArr[0] ?? null; - const [activeSemester, setActiveSemester] = useState( + const [activeSemester, setActiveSemester] = useState( defaultSemester, ); const [includeHackathons, setIncludeHackathons] = useState(false); @@ -43,13 +39,13 @@ export default function EventDemographics() { events?.forEach(({ start_datetime }) => { const year = start_datetime.getFullYear(); const springStart = new Date( - `${year}-${SEMESTER_START_DATES.spring.month + 1}-${SEMESTER_START_DATES.spring.day}`, + `${year}-${FORMS.SEMESTER_START_DATES.spring.month + 1}-${FORMS.SEMESTER_START_DATES.spring.day}`, ); const summerStart = new Date( - `${year}-${SEMESTER_START_DATES.summer.month + 1}-${SEMESTER_START_DATES.summer.day}`, + `${year}-${FORMS.SEMESTER_START_DATES.summer.month + 1}-${FORMS.SEMESTER_START_DATES.summer.day}`, ); const fallStart = new Date( - `${year}-${SEMESTER_START_DATES.fall.month + 1}-${SEMESTER_START_DATES.fall.day}`, + `${year}-${FORMS.SEMESTER_START_DATES.fall.month + 1}-${FORMS.SEMESTER_START_DATES.fall.day}`, ); // keep track of semesters that exist in events table of db @@ -58,7 +54,7 @@ export default function EventDemographics() { if (!semestersSet.has(semesterName)) { semestersSet.add(semesterName); const springEnd = new Date( - `${year}-${SEMESTER_START_DATES.summer.month + 1}-${SEMESTER_START_DATES.summer.day}`, + `${year}-${FORMS.SEMESTER_START_DATES.summer.month + 1}-${FORMS.SEMESTER_START_DATES.summer.day}`, ); semestersArr.push({ name: semesterName, @@ -71,7 +67,7 @@ export default function EventDemographics() { if (!semestersSet.has(semesterName)) { semestersSet.add(semesterName); const summerEnd = new Date( - `${year}-${SEMESTER_START_DATES.fall.month + 1}-${SEMESTER_START_DATES.fall.day}`, + `${year}-${FORMS.SEMESTER_START_DATES.fall.month + 1}-${FORMS.SEMESTER_START_DATES.fall.day}`, ); semestersArr.push({ name: semesterName, @@ -87,7 +83,7 @@ export default function EventDemographics() { if (!semestersSet.has(semesterName)) { semestersSet.add(semesterName); const fallEnd = new Date( - `${year + 1}-${SEMESTER_START_DATES.spring.month + 1}-${SEMESTER_START_DATES.spring.day}`, + `${year + 1}-${FORMS.SEMESTER_START_DATES.spring.month + 1}-${FORMS.SEMESTER_START_DATES.spring.day}`, ); semestersArr.push({ name: semesterName, diff --git a/apps/blade/src/app/admin/club/data/_components/event-data/AttendancesBarChart.tsx b/apps/blade/src/app/admin/club/data/_components/event-data/AttendancesBarChart.tsx index 96830ec29..70bef30de 100644 --- a/apps/blade/src/app/admin/club/data/_components/event-data/AttendancesBarChart.tsx +++ b/apps/blade/src/app/admin/club/data/_components/event-data/AttendancesBarChart.tsx @@ -11,13 +11,13 @@ import { import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import type { ChartConfig } from "@forge/ui/chart"; -import { ADMIN_PIE_CHART_COLORS } from "@forge/consts/knight-hacks"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, ChartTooltip, ChartTooltipContent, } from "@forge/ui/chart"; +import { FORMS } from '@forge/consts'; export default function AttendancesBarChart({ events, @@ -33,7 +33,7 @@ export default function AttendancesBarChart({ if (!baseConfig[tag]) { baseConfig[tag] = { label: tag, - color: ADMIN_PIE_CHART_COLORS[6], + color: FORMS.ADMIN_PIE_CHART_COLORS[6], }; } }); @@ -57,7 +57,7 @@ export default function AttendancesBarChart({ ([tag, { totalAttendees, totalEvents }]) => ({ tag: tag, avgAttendees: (totalAttendees / totalEvents).toFixed(0), - fill: baseConfig[tag]?.color ?? ADMIN_PIE_CHART_COLORS[6], + fill: baseConfig[tag]?.color ?? FORMS.ADMIN_PIE_CHART_COLORS[6], }), ); diff --git a/apps/blade/src/app/admin/club/data/_components/event-data/PopularityRanking.tsx b/apps/blade/src/app/admin/club/data/_components/event-data/PopularityRanking.tsx index d78a369ce..9a7d0f43a 100644 --- a/apps/blade/src/app/admin/club/data/_components/event-data/PopularityRanking.tsx +++ b/apps/blade/src/app/admin/club/data/_components/event-data/PopularityRanking.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; -import { RANKING_STYLES } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; +import { FORMS } from '@forge/consts'; export default function PopularityRanking({ events, @@ -36,7 +36,7 @@ export default function PopularityRanking({ (event, index: number) => (
  • {index + 1}. {event.name} [{event.tag.toUpperCase()} diff --git a/apps/blade/src/app/admin/club/data/_components/event-data/TypePie.tsx b/apps/blade/src/app/admin/club/data/_components/event-data/TypePie.tsx index 45f260187..17849c9e0 100644 --- a/apps/blade/src/app/admin/club/data/_components/event-data/TypePie.tsx +++ b/apps/blade/src/app/admin/club/data/_components/event-data/TypePie.tsx @@ -6,7 +6,6 @@ import { Cell, Label, Pie, PieChart, Sector } from "recharts"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import type { ChartConfig } from "@forge/ui/chart"; -import { ADMIN_PIE_CHART_COLORS } from "@forge/consts/knight-hacks"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -21,6 +20,7 @@ import { SelectTrigger, SelectValue, } from "@forge/ui/select"; +import { FORMS } from '@forge/consts'; export default function TypePie({ events }: { events: ReturnEvent[] }) { const id = "pie-interactive"; @@ -66,7 +66,7 @@ export default function TypePie({ events }: { events: ReturnEvent[] }) { if (!baseConfig[tag]) { baseConfig[tag] = { label: tag, - color: ADMIN_PIE_CHART_COLORS[colorIdx % ADMIN_PIE_CHART_COLORS.length], + color: FORMS.ADMIN_PIE_CHART_COLORS[colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length], }; colorIdx++; } @@ -183,8 +183,8 @@ export default function TypePie({ events }: { events: ReturnEvent[] }) { diff --git a/apps/blade/src/app/admin/club/data/_components/event-data/WeekdayPopularityRadar.tsx b/apps/blade/src/app/admin/club/data/_components/event-data/WeekdayPopularityRadar.tsx index 659754b1d..bbd4f53d2 100644 --- a/apps/blade/src/app/admin/club/data/_components/event-data/WeekdayPopularityRadar.tsx +++ b/apps/blade/src/app/admin/club/data/_components/event-data/WeekdayPopularityRadar.tsx @@ -10,22 +10,19 @@ import { import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import type { ChartConfig } from "@forge/ui/chart"; -import { - ADMIN_PIE_CHART_COLORS, - WEEKDAY_ORDER, -} from "@forge/consts/knight-hacks"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, ChartTooltip, ChartTooltipContent, } from "@forge/ui/chart"; +import { FORMS } from '@forge/consts'; export function WeekdayPopularityRadar({ events }: { events: ReturnEvent[] }) { const chartConfig = { attendees: { label: "Average attendees", - color: ADMIN_PIE_CHART_COLORS[1], + color: FORMS.ADMIN_PIE_CHART_COLORS[1], }, } satisfies ChartConfig; @@ -106,11 +103,11 @@ export function WeekdayPopularityRadar({ events }: { events: ReturnEvent[] }) { .map(([weekday, { totalAttendees, totalEvents }]) => ({ weekday: weekday, avgAttendees: (totalAttendees / totalEvents).toFixed(0), - fill: ADMIN_PIE_CHART_COLORS[1], + fill: FORMS.ADMIN_PIE_CHART_COLORS[1], })) .sort( (a, b) => - WEEKDAY_ORDER.indexOf(a.weekday) - WEEKDAY_ORDER.indexOf(b.weekday), + FORMS.WEEKDAY_ORDER.indexOf(a.weekday) - FORMS.WEEKDAY_ORDER.indexOf(b.weekday), ); const maxAvgAttendees = Math.max( @@ -145,7 +142,7 @@ export function WeekdayPopularityRadar({ events }: { events: ReturnEvent[] }) { diff --git a/apps/blade/src/app/admin/club/data/_components/member-data/SchoolBarChart.tsx b/apps/blade/src/app/admin/club/data/_components/member-data/SchoolBarChart.tsx index a1ccd19d8..262065d0d 100644 --- a/apps/blade/src/app/admin/club/data/_components/member-data/SchoolBarChart.tsx +++ b/apps/blade/src/app/admin/club/data/_components/member-data/SchoolBarChart.tsx @@ -9,7 +9,7 @@ import { YAxis, } from "recharts"; -import type { SCHOOLS } from "@forge/consts/knight-hacks"; +import type { FORMS } from "@forge/consts"; import type { ChartConfig } from "@forge/ui/chart"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, ChartTooltip } from "@forge/ui/chart"; @@ -22,7 +22,7 @@ const chartConfig = { } satisfies ChartConfig; interface Member { - school?: (typeof SCHOOLS)[number]; + school?: (typeof FORMS.SCHOOLS)[number]; } export default function SchoolBarChart({ members }: { members: Member[] }) { diff --git a/apps/blade/src/app/admin/club/data/_components/member-data/ShirtSizePie.tsx b/apps/blade/src/app/admin/club/data/_components/member-data/ShirtSizePie.tsx index 939dbc102..b178dc716 100644 --- a/apps/blade/src/app/admin/club/data/_components/member-data/ShirtSizePie.tsx +++ b/apps/blade/src/app/admin/club/data/_components/member-data/ShirtSizePie.tsx @@ -4,9 +4,8 @@ import type { PieSectorDataItem } from "recharts/types/polar/Pie"; import { useEffect, useMemo, useState } from "react"; import { Cell, Label, Pie, PieChart, Sector } from "recharts"; -import type { SHIRT_SIZES } from "@forge/consts/knight-hacks"; +import { FORMS } from "@forge/consts"; import type { ChartConfig } from "@forge/ui/chart"; -import { ADMIN_PIE_CHART_COLORS } from "@forge/consts/knight-hacks"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -23,7 +22,7 @@ import { } from "@forge/ui/select"; interface Member { - shirtSize?: (typeof SHIRT_SIZES)[number]; + shirtSize?: (typeof FORMS.SHIRT_SIZES)[number]; } export default function ShirtSizePie({ members }: { members: Member[] }) { @@ -66,7 +65,7 @@ export default function ShirtSizePie({ members }: { members: Member[] }) { if (shirtSize && !baseConfig[shirtSize]) { baseConfig[shirtSize] = { label: shirtSize, - color: ADMIN_PIE_CHART_COLORS[colorIdx % ADMIN_PIE_CHART_COLORS.length], + color: FORMS.ADMIN_PIE_CHART_COLORS[colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length], }; colorIdx++; } @@ -197,8 +196,8 @@ export default function ShirtSizePie({ members }: { members: Member[] }) { diff --git a/apps/blade/src/app/admin/club/data/_components/member-data/YearOfStudyPie.tsx b/apps/blade/src/app/admin/club/data/_components/member-data/YearOfStudyPie.tsx index f0d46b3dd..5194ee8e4 100644 --- a/apps/blade/src/app/admin/club/data/_components/member-data/YearOfStudyPie.tsx +++ b/apps/blade/src/app/admin/club/data/_components/member-data/YearOfStudyPie.tsx @@ -5,7 +5,6 @@ import { useEffect, useMemo, useState } from "react"; import { Cell, Label, Pie, PieChart, Sector } from "recharts"; import type { ChartConfig } from "@forge/ui/chart"; -import { ADMIN_PIE_CHART_COLORS } from "@forge/consts/knight-hacks"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -20,6 +19,7 @@ import { SelectTrigger, SelectValue, } from "@forge/ui/select"; +import { FORMS } from '@forge/consts'; interface Member { gradDate: Date | string; @@ -129,7 +129,7 @@ export default function YearOfStudyPie({ members }: YearOfStudyPieProps) { if (!baseConfig[name]) { baseConfig[name] = { label: name, - color: ADMIN_PIE_CHART_COLORS[colorIdx % ADMIN_PIE_CHART_COLORS.length], + color: FORMS.ADMIN_PIE_CHART_COLORS[colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length], }; colorIdx++; } @@ -258,8 +258,8 @@ export default function YearOfStudyPie({ members }: YearOfStudyPieProps) { diff --git a/apps/blade/src/app/admin/club/events/_components/create-event.tsx b/apps/blade/src/app/admin/club/events/_components/create-event.tsx index e00c3fb0f..19d74ed47 100644 --- a/apps/blade/src/app/admin/club/events/_components/create-event.tsx +++ b/apps/blade/src/app/admin/club/events/_components/create-event.tsx @@ -4,7 +4,6 @@ import { useState } from "react"; import { Loader2, Plus } from "lucide-react"; import { z } from "zod"; -import { EVENT_TAGS } from "@forge/consts/knight-hacks"; import { InsertEventSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; @@ -39,6 +38,7 @@ import { Textarea } from "@forge/ui/textarea"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; +import { EVENTS } from '@forge/consts'; // 12-hour-based hours (1–12), displayed as "01", "02", ..., "12" const hours = Array.from({ length: 12 }, (_, i) => @@ -101,7 +101,7 @@ export function CreateEventButton() { description: "", dues_paying: false, location: "", - tag: EVENT_TAGS[0], + tag: EVENTS.EVENT_TAGS[0], hackathonId: "none", startDate: "", endDate: "", @@ -261,7 +261,7 @@ export function CreateEventButton() { - {EVENT_TAGS.map((tagOption) => ( + {EVENTS.EVENT_TAGS.map((tagOption) => ( {tagOption} diff --git a/apps/blade/src/app/admin/club/events/_components/delete-event.tsx b/apps/blade/src/app/admin/club/events/_components/delete-event.tsx index 65237c76d..f59e57a4c 100644 --- a/apps/blade/src/app/admin/club/events/_components/delete-event.tsx +++ b/apps/blade/src/app/admin/club/events/_components/delete-event.tsx @@ -3,8 +3,8 @@ import { useState } from "react"; import { Loader2, Trash2 } from "lucide-react"; -import type { EVENT_TAGS } from "@forge/consts/knight-hacks"; -import { USE_CAUTION } from "@forge/consts/knight-hacks"; +import type { EVENTS } from "@forge/consts"; +import { USE_CAUTION } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -26,7 +26,7 @@ interface DeleteEventButtonProps { discordId: string; googleId: string; name: string; - tag: (typeof EVENT_TAGS)[number]; + tag: (typeof EVENTS.EVENT_TAGS)[number]; }; } diff --git a/apps/blade/src/app/admin/club/events/_components/update-event.tsx b/apps/blade/src/app/admin/club/events/_components/update-event.tsx index 748295702..c52bc5bca 100644 --- a/apps/blade/src/app/admin/club/events/_components/update-event.tsx +++ b/apps/blade/src/app/admin/club/events/_components/update-event.tsx @@ -5,7 +5,6 @@ import { Loader2, Pencil } from "lucide-react"; import { z } from "zod"; import type { InsertEvent } from "@forge/db/schemas/knight-hacks"; -import { EVENT_TAGS } from "@forge/consts/knight-hacks"; import { InsertEventSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; @@ -40,6 +39,7 @@ import { Textarea } from "@forge/ui/textarea"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; +import { EVENTS } from '@forge/consts'; // 12-hour-based hours (1–12) const hours = Array.from({ length: 12 }, (_, i) => @@ -295,7 +295,7 @@ export function UpdateEventButton({ event }: { event: InsertEvent }) { - {EVENT_TAGS.map((tagOption) => ( + {EVENTS.EVENT_TAGS.map((tagOption) => ( {tagOption} diff --git a/apps/blade/src/app/admin/club/members/_components/delete-member.tsx b/apps/blade/src/app/admin/club/members/_components/delete-member.tsx index 22f5480ec..a7f8d515a 100644 --- a/apps/blade/src/app/admin/club/members/_components/delete-member.tsx +++ b/apps/blade/src/app/admin/club/members/_components/delete-member.tsx @@ -3,7 +3,6 @@ import { useState } from "react"; import { Loader2, Trash2 } from "lucide-react"; -import { USE_CAUTION } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -18,6 +17,7 @@ import { Input } from "@forge/ui/input"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; +import { USE_CAUTION } from '@forge/consts'; export default function DeleteMemberButton({ member, diff --git a/apps/blade/src/app/admin/club/members/_components/final-dues-dialog.tsx b/apps/blade/src/app/admin/club/members/_components/final-dues-dialog.tsx index 05c2c3b1a..f89e1ac2d 100644 --- a/apps/blade/src/app/admin/club/members/_components/final-dues-dialog.tsx +++ b/apps/blade/src/app/admin/club/members/_components/final-dues-dialog.tsx @@ -3,7 +3,6 @@ import { useState } from "react"; import { Loader2 } from "lucide-react"; -import { CLEAR_DUES_MESSAGE, USE_CAUTION } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -18,6 +17,7 @@ import { Input } from "@forge/ui/input"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; +import { CLEAR_DUES_MESSAGE, USE_CAUTION } from '@forge/consts'; export default function FinalDuesDialogButton({ disabled, diff --git a/apps/blade/src/app/admin/club/members/_components/member-profile.tsx b/apps/blade/src/app/admin/club/members/_components/member-profile.tsx index 1f734aea8..d51f6b5d3 100644 --- a/apps/blade/src/app/admin/club/members/_components/member-profile.tsx +++ b/apps/blade/src/app/admin/club/members/_components/member-profile.tsx @@ -6,13 +6,6 @@ import { User } from "lucide-react"; import { FaGithub, FaGlobe, FaLinkedin } from "react-icons/fa"; import type { InsertMember } from "@forge/db/schemas/knight-hacks"; -import { - LEVELS_OF_STUDY, - MEMBER_PROFILE_ICON_SIZE, - RACES_OR_ETHNICITIES, - SHORT_LEVELS_OF_STUDY, - SHORT_RACES_AND_ETHNICITIES, -} from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -23,6 +16,7 @@ import { } from "@forge/ui/dialog"; import { api } from "~/trpc/react"; +import { FORMS, MEMBER_PROFILE_ICON_SIZE } from '@forge/consts'; export default function MemberProfileButton({ member, @@ -109,12 +103,12 @@ export default function MemberProfileButton({

    Level Of Study:{" "} - {member.levelOfStudy === LEVELS_OF_STUDY[2] // Undergraduate University (2 year - community college or similar) - ? SHORT_LEVELS_OF_STUDY[0] // Undergraduate University (2 year) - : member.levelOfStudy === LEVELS_OF_STUDY[4] // Graduate University (Masters, Professional, Doctoral, etc) - ? SHORT_LEVELS_OF_STUDY[1] // Graduate University (Masters/PhD) - : member.levelOfStudy === LEVELS_OF_STUDY[6] // Other Vocational / Trade Program or Apprenticeship - ? SHORT_LEVELS_OF_STUDY[2] // Vocational/Trade School + {member.levelOfStudy === FORMS.LEVELS_OF_STUDY[2] // Undergraduate University (2 year - community college or similar) + ? FORMS.SHORT_LEVELS_OF_STUDY[0] // Undergraduate University (2 year) + : member.levelOfStudy === FORMS.LEVELS_OF_STUDY[4] // Graduate University (Masters, Professional, Doctoral, etc) + ? FORMS.SHORT_LEVELS_OF_STUDY[1] // Graduate University (Masters/PhD) + : member.levelOfStudy === FORMS.LEVELS_OF_STUDY[6] // Other Vocational / Trade Program or Apprenticeship + ? FORMS.SHORT_LEVELS_OF_STUDY[2] // Vocational/Trade School : member.levelOfStudy}

    @@ -132,12 +126,12 @@ export default function MemberProfileButton({

    Race Or Ethnicity:{" "} - {member.raceOrEthnicity === RACES_OR_ETHNICITIES[4] // Native Hawaiian or Other Pacific Islander - ? SHORT_RACES_AND_ETHNICITIES[0] // Native Hawaiian/Pacific Islander - : member.raceOrEthnicity === RACES_OR_ETHNICITIES[2] // Hispanic / Latino / Spanish Origin - ? SHORT_RACES_AND_ETHNICITIES[1] // Hispanic/Latino - : member.raceOrEthnicity === RACES_OR_ETHNICITIES[5] // Native American or Alaskan Native - ? SHORT_RACES_AND_ETHNICITIES[2] // Native American/Alaskan Native + {member.raceOrEthnicity === FORMS.RACES_OR_ETHNICITIES[4] // Native Hawaiian or Other Pacific Islander + ? FORMS.SHORT_RACES_AND_ETHNICITIES[0] // Native Hawaiian/Pacific Islander + : member.raceOrEthnicity === FORMS.RACES_OR_ETHNICITIES[2] // Hispanic / Latino / Spanish Origin + ? FORMS.SHORT_RACES_AND_ETHNICITIES[1] // Hispanic/Latino + : member.raceOrEthnicity === FORMS.RACES_OR_ETHNICITIES[5] // Native American or Alaskan Native + ? FORMS.SHORT_RACES_AND_ETHNICITIES[2] // Native American/Alaskan Native : member.raceOrEthnicity}

  • diff --git a/apps/blade/src/app/admin/club/members/_components/update-member.tsx b/apps/blade/src/app/admin/club/members/_components/update-member.tsx index 4971d32f0..34565b8e6 100644 --- a/apps/blade/src/app/admin/club/members/_components/update-member.tsx +++ b/apps/blade/src/app/admin/club/members/_components/update-member.tsx @@ -5,11 +5,6 @@ import { Loader2, Pencil } from "lucide-react"; import { z } from "zod"; import type { InsertMember } from "@forge/db/schemas/knight-hacks"; -import { - LEVELS_OF_STUDY, - SCHOOLS, - SHIRT_SIZES, -} from "@forge/consts/knight-hacks"; import { InsertMemberSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { @@ -42,6 +37,7 @@ import { import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; +import { FORMS } from '@forge/consts'; export default function UpdateMemberButton({ member, @@ -267,7 +263,7 @@ export default function UpdateMemberButton({
    {school}
    } getItemValue={(school) => school} getItemLabel={(school) => school} @@ -320,7 +316,7 @@ export default function UpdateMemberButton({
    - {LEVELS_OF_STUDY.map((level) => ( + {FORMS.LEVELS_OF_STUDY.map((level) => ( {level} @@ -354,7 +350,7 @@ export default function UpdateMemberButton({ - {SHIRT_SIZES.map((shirt_size) => ( + {FORMS.SHIRT_SIZES.map((shirt_size) => ( {shirt_size} diff --git a/apps/blade/src/app/admin/forms/[slug]/responses/_components/AllResponsesView.tsx b/apps/blade/src/app/admin/forms/[slug]/responses/_components/AllResponsesView.tsx index 3573fd114..5b30fc0ab 100644 --- a/apps/blade/src/app/admin/forms/[slug]/responses/_components/AllResponsesView.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/responses/_components/AllResponsesView.tsx @@ -1,5 +1,4 @@ -import type { FormType } from "@forge/consts/knight-hacks"; - +import type { FORMS } from '@forge/consts'; import { FileUploadResponsesTable } from "./FileUploadResponsesTable"; import { ResponseBarChart } from "./ResponseBarChart"; import { ResponseHorizontalBarChart } from "./ResponseHorizontalBarChart"; @@ -7,7 +6,7 @@ import { ResponsePieChart } from "./ResponsePieChart"; import { ResponsesTable } from "./ResponsesTable"; interface AllResponsesViewProps { - formData: FormType; + formData: FORMS.FormType; responses: { submittedAt: Date; responseData: Record; diff --git a/apps/blade/src/app/admin/forms/[slug]/responses/_components/PerUserResponsesView.tsx b/apps/blade/src/app/admin/forms/[slug]/responses/_components/PerUserResponsesView.tsx index 44890b6e6..7fde2dd3c 100644 --- a/apps/blade/src/app/admin/forms/[slug]/responses/_components/PerUserResponsesView.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/responses/_components/PerUserResponsesView.tsx @@ -15,16 +15,16 @@ import { X, } from "lucide-react"; -import type { FormType } from "@forge/consts/knight-hacks"; import { Button } from "@forge/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { Separator } from "@forge/ui/separator"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; +import type { FORMS } from '@forge/consts'; interface PerUserResponsesViewProps { - formData: FormType; + formData: FORMS.FormType; responses: { id: string; submittedAt: Date; diff --git a/apps/blade/src/app/admin/forms/[slug]/responses/_components/ResponsePieChart.tsx b/apps/blade/src/app/admin/forms/[slug]/responses/_components/ResponsePieChart.tsx index bba25b2a4..92ff50b91 100644 --- a/apps/blade/src/app/admin/forms/[slug]/responses/_components/ResponsePieChart.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/responses/_components/ResponsePieChart.tsx @@ -4,7 +4,6 @@ import { Cell, Pie, PieChart } from "recharts"; -import { ADMIN_PIE_CHART_COLORS } from "@forge/consts/knight-hacks"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -12,6 +11,7 @@ import { ChartTooltip, ChartTooltipContent, } from "@forge/ui/chart"; +import { FORMS } from '@forge/consts'; // props - expects a question string and array of responses interface ResponsePieChartProps { @@ -78,7 +78,7 @@ export function ResponsePieChart({ chartConfig[item.name] = { label: item.name, color: - ADMIN_PIE_CHART_COLORS[index % ADMIN_PIE_CHART_COLORS.length] ?? + FORMS.ADMIN_PIE_CHART_COLORS[index % FORMS.ADMIN_PIE_CHART_COLORS.length] ?? "#000000", }; }); @@ -146,8 +146,8 @@ export function ResponsePieChart({ @@ -201,8 +201,8 @@ export function ResponsePieChart({ From 4be1b10b4d622892ad4ce465bc348930ff6c1c52 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:03:07 -0500 Subject: [PATCH 06/23] chore(lint): lint fixes --- .../application/_components/member-application-form.tsx | 3 ++- apps/blade/src/lib/utils.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/blade/src/app/member/application/_components/member-application-form.tsx b/apps/blade/src/app/member/application/_components/member-application-form.tsx index ee6b922b4..45fbde29a 100644 --- a/apps/blade/src/app/member/application/_components/member-application-form.tsx +++ b/apps/blade/src/app/member/application/_components/member-application-form.tsx @@ -6,11 +6,12 @@ import { useRouter } from "next/navigation"; import { Loader2 } from "lucide-react"; import { z } from "zod"; +import type { + GradTerm} from "@forge/consts"; import { ALLOWED_PROFILE_PICTURE_EXTENSIONS, ALLOWED_PROFILE_PICTURE_TYPES, FORMS, - GradTerm, KNIGHTHACKS_MAX_PROFILE_PICTURE_SIZE, KNIGHTHACKS_MAX_RESUME_SIZE, TERM_TO_DATE, diff --git a/apps/blade/src/lib/utils.ts b/apps/blade/src/lib/utils.ts index 1c2786112..1268260fe 100644 --- a/apps/blade/src/lib/utils.ts +++ b/apps/blade/src/lib/utils.ts @@ -1,6 +1,10 @@ import type { AnyTRPCProcedure, AnyTRPCRouter } from "@trpc/server"; import type { z } from "zod"; +import type { EVENTS } from "@forge/consts"; +import type { HackerClass } from "@forge/db/schemas/knight-hacks"; +import { PERMISSION_DATA, PERMISSIONS } from "@forge/consts"; + export const formatDateTime = (date: Date) => { // Create a new Date object 5 hours behind the original const adjustedDate = new Date(date.getTime()); @@ -35,8 +39,8 @@ export const formatDateRange = (startDate: Date, endDate: Date) => { return `${start} - ${end}`; }; -export const getTagColor = (tag: EventTagsColor) => { - const colors: Record = { +export const getTagColor = (tag: EVENTS.EventTagsColor) => { + const colors: Record = { GBM: "bg-blue-100 text-blue-800", Social: "bg-pink-100 text-pink-800", Kickstart: "bg-green-100 text-green-800", From 916d3172010b4a6403b6e1ef5d016de99b9f7f5c Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 21:25:29 -0500 Subject: [PATCH 07/23] fix(consts): use correct import --- .../_components/hackathon-dashboard/hackathon-dashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx b/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx index 0d756d927..e85e1c7ed 100644 --- a/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx +++ b/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx @@ -73,7 +73,7 @@ export default async function HackathonDashboard({ } const HACKER_CLASS_INFO_TYPED: Record = - HACKER_CLASS_INFO as Record; + DISCORD.KNIGHTHACKS_8.HACKER_CLASS_INFO as Record; const classInfo = HACKER_CLASS_INFO_TYPED[hacker.class] ?? { teamColor: "#000000", From ec8c1f393548579d21cca5129a7ffe9d177c414b Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 21:36:53 -0500 Subject: [PATCH 08/23] chore(consts): update coderabbit for consts package Added applicable checks --- .coderabbit.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.coderabbit.yml b/.coderabbit.yml index 80add6faf..0ef78013f 100644 --- a/.coderabbit.yml +++ b/.coderabbit.yml @@ -62,6 +62,8 @@ reviews: # Package labels - label: "API" instructions: "Apply when PR changes files in packages/api/** (tRPC routers, procedures)" + - label: "Constants" + instructions: "Apply when PR changes files in packages/consts/** (constants)" - label: "UI" instructions: "Apply when PR changes files in packages/ui/** (shared component library)" - label: "Database" @@ -170,8 +172,14 @@ reviews: - path: "packages/consts/**" instructions: | - Shared constants. Changes here affect the entire monorepo. - Ensure values are correct and changes are intentional. + Shared constants used across all apps and packages. Changes here affect the entire monorepo. Check: + - All constant names follow SCREAMING_SNAKE_CASE convention + - Values are intentional and well-documented (Discord IDs, API endpoints, role IDs, etc.) + - URLs and external API endpoints are correct and won't break integrations + - Type definitions are properly exported and used + - Constants do not contain sensitive data + - PROD and DEV constants are prefixed accordingly + - Exported values have valid reason to be shared repository wide # Applications - path: "apps/blade/**" From fca1f9e7df5b749f8ca6562b0c7a1d85d2bfb9a3 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:03:49 -0500 Subject: [PATCH 09/23] lint(*): lots of random fixes --- .../app/admin/_components/SchoolBarChart.tsx | 2 +- .../app/admin/_components/SchoolYearPie.tsx | 7 ++-- .../data/_components/EventDemographics.tsx | 2 +- .../event-data/AttendancesBarChart.tsx | 2 +- .../event-data/PopularityRanking.tsx | 2 +- .../data/_components/event-data/TypePie.tsx | 7 ++-- .../event-data/WeekdayPopularityRadar.tsx | 5 +-- .../_components/member-data/ShirtSizePie.tsx | 7 ++-- .../member-data/YearOfStudyPie.tsx | 7 ++-- .../club/events/_components/create-event.tsx | 2 +- .../club/events/_components/update-event.tsx | 2 +- .../members/_components/delete-member.tsx | 2 +- .../members/_components/final-dues-dialog.tsx | 2 +- .../members/_components/member-profile.tsx | 2 +- .../members/_components/update-member.tsx | 2 +- .../src/app/admin/forms/[slug]/client.tsx | 33 +++++++++---------- .../_components/AllResponsesView.tsx | 3 +- .../_components/PerUserResponsesView.tsx | 2 +- .../_components/ResponsePieChart.tsx | 7 ++-- .../events/_components/update-event.tsx | 2 +- .../hackers/_components/hacker-profile.tsx | 2 +- .../roles/configure/_components/roleedit.tsx | 31 +++++++++-------- .../roles/configure/_components/roletable.tsx | 2 +- .../hackathon-dashboard.tsx | 4 +-- .../hacker-dashboard/hacker-data.tsx | 2 +- .../_components/form-responder-client.tsx | 3 +- .../_components/question-response-card.tsx | 2 +- .../_components/member-application-form.tsx | 3 +- .../_components/delete-member-button.tsx | 2 +- .../src/app/settings/hacker-profile/page.tsx | 2 +- apps/blade/src/components/forms/form-card.tsx | 4 ++- apps/cron/src/crons/role-sync.ts | 5 +-- .../ui/background-gradient-animation.tsx | 15 +++++++-- packages/api/src/routers/dues-payment.ts | 2 +- packages/api/src/routers/event-feedback.ts | 2 +- packages/api/src/routers/hacker.ts | 2 +- .../consts/src/discord/project-launch-26.ts | 2 +- packages/consts/src/util.ts | 2 +- packages/consts/tsconfig.json | 2 +- 39 files changed, 108 insertions(+), 81 deletions(-) diff --git a/apps/blade/src/app/admin/_components/SchoolBarChart.tsx b/apps/blade/src/app/admin/_components/SchoolBarChart.tsx index c61e012dc..2ff31a0d8 100644 --- a/apps/blade/src/app/admin/_components/SchoolBarChart.tsx +++ b/apps/blade/src/app/admin/_components/SchoolBarChart.tsx @@ -9,8 +9,8 @@ import { YAxis, } from "recharts"; -import type { ChartConfig } from "@forge/ui/chart"; import type { FORMS } from "@forge/consts"; +import type { ChartConfig } from "@forge/ui/chart"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, ChartTooltip } from "@forge/ui/chart"; diff --git a/apps/blade/src/app/admin/_components/SchoolYearPie.tsx b/apps/blade/src/app/admin/_components/SchoolYearPie.tsx index 08f3ab249..3cb2e2166 100644 --- a/apps/blade/src/app/admin/_components/SchoolYearPie.tsx +++ b/apps/blade/src/app/admin/_components/SchoolYearPie.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import { Cell, Label, Pie, PieChart, Sector } from "recharts"; import type { ChartConfig } from "@forge/ui/chart"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -19,7 +20,6 @@ import { SelectTrigger, SelectValue, } from "@forge/ui/select"; -import { FORMS } from '@forge/consts'; interface Person { levelOfStudy?: (typeof FORMS.LEVELS_OF_STUDY)[number]; @@ -80,7 +80,10 @@ export default function SchoolYearPie({ people }: { people: Person[] }) { if (shortenedString && !baseConfig[shortenedString]) { baseConfig[shortenedString] = { label: shortenedString, - color: FORMS.ADMIN_PIE_CHART_COLORS[colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length], + color: + FORMS.ADMIN_PIE_CHART_COLORS[ + colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length + ], }; colorIdx++; } diff --git a/apps/blade/src/app/admin/club/data/_components/EventDemographics.tsx b/apps/blade/src/app/admin/club/data/_components/EventDemographics.tsx index 88ea85062..cbc9e7379 100644 --- a/apps/blade/src/app/admin/club/data/_components/EventDemographics.tsx +++ b/apps/blade/src/app/admin/club/data/_components/EventDemographics.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; +import { FORMS } from "@forge/consts"; import { Checkbox } from "@forge/ui/checkbox"; import { Select, @@ -17,7 +18,6 @@ import AttendancesMobile from "./event-data/AttendancesMobile"; import PopularityRanking from "./event-data/PopularityRanking"; import TypePie from "./event-data/TypePie"; import { WeekdayPopularityRadar } from "./event-data/WeekdayPopularityRadar"; -import { FORMS } from '@forge/consts'; export default function EventDemographics() { const { data: events } = api.event.getEvents.useQuery(); diff --git a/apps/blade/src/app/admin/club/data/_components/event-data/AttendancesBarChart.tsx b/apps/blade/src/app/admin/club/data/_components/event-data/AttendancesBarChart.tsx index 70bef30de..36416ce79 100644 --- a/apps/blade/src/app/admin/club/data/_components/event-data/AttendancesBarChart.tsx +++ b/apps/blade/src/app/admin/club/data/_components/event-data/AttendancesBarChart.tsx @@ -11,13 +11,13 @@ import { import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import type { ChartConfig } from "@forge/ui/chart"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, ChartTooltip, ChartTooltipContent, } from "@forge/ui/chart"; -import { FORMS } from '@forge/consts'; export default function AttendancesBarChart({ events, diff --git a/apps/blade/src/app/admin/club/data/_components/event-data/PopularityRanking.tsx b/apps/blade/src/app/admin/club/data/_components/event-data/PopularityRanking.tsx index 9a7d0f43a..40197c868 100644 --- a/apps/blade/src/app/admin/club/data/_components/event-data/PopularityRanking.tsx +++ b/apps/blade/src/app/admin/club/data/_components/event-data/PopularityRanking.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; +import { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; -import { FORMS } from '@forge/consts'; export default function PopularityRanking({ events, diff --git a/apps/blade/src/app/admin/club/data/_components/event-data/TypePie.tsx b/apps/blade/src/app/admin/club/data/_components/event-data/TypePie.tsx index 17849c9e0..ab7bf1a06 100644 --- a/apps/blade/src/app/admin/club/data/_components/event-data/TypePie.tsx +++ b/apps/blade/src/app/admin/club/data/_components/event-data/TypePie.tsx @@ -6,6 +6,7 @@ import { Cell, Label, Pie, PieChart, Sector } from "recharts"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import type { ChartConfig } from "@forge/ui/chart"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -20,7 +21,6 @@ import { SelectTrigger, SelectValue, } from "@forge/ui/select"; -import { FORMS } from '@forge/consts'; export default function TypePie({ events }: { events: ReturnEvent[] }) { const id = "pie-interactive"; @@ -66,7 +66,10 @@ export default function TypePie({ events }: { events: ReturnEvent[] }) { if (!baseConfig[tag]) { baseConfig[tag] = { label: tag, - color: FORMS.ADMIN_PIE_CHART_COLORS[colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length], + color: + FORMS.ADMIN_PIE_CHART_COLORS[ + colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length + ], }; colorIdx++; } diff --git a/apps/blade/src/app/admin/club/data/_components/event-data/WeekdayPopularityRadar.tsx b/apps/blade/src/app/admin/club/data/_components/event-data/WeekdayPopularityRadar.tsx index bbd4f53d2..e7caef5c1 100644 --- a/apps/blade/src/app/admin/club/data/_components/event-data/WeekdayPopularityRadar.tsx +++ b/apps/blade/src/app/admin/club/data/_components/event-data/WeekdayPopularityRadar.tsx @@ -10,13 +10,13 @@ import { import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import type { ChartConfig } from "@forge/ui/chart"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, ChartTooltip, ChartTooltipContent, } from "@forge/ui/chart"; -import { FORMS } from '@forge/consts'; export function WeekdayPopularityRadar({ events }: { events: ReturnEvent[] }) { const chartConfig = { @@ -107,7 +107,8 @@ export function WeekdayPopularityRadar({ events }: { events: ReturnEvent[] }) { })) .sort( (a, b) => - FORMS.WEEKDAY_ORDER.indexOf(a.weekday) - FORMS.WEEKDAY_ORDER.indexOf(b.weekday), + FORMS.WEEKDAY_ORDER.indexOf(a.weekday) - + FORMS.WEEKDAY_ORDER.indexOf(b.weekday), ); const maxAvgAttendees = Math.max( diff --git a/apps/blade/src/app/admin/club/data/_components/member-data/ShirtSizePie.tsx b/apps/blade/src/app/admin/club/data/_components/member-data/ShirtSizePie.tsx index b178dc716..dfbf82f53 100644 --- a/apps/blade/src/app/admin/club/data/_components/member-data/ShirtSizePie.tsx +++ b/apps/blade/src/app/admin/club/data/_components/member-data/ShirtSizePie.tsx @@ -4,8 +4,8 @@ import type { PieSectorDataItem } from "recharts/types/polar/Pie"; import { useEffect, useMemo, useState } from "react"; import { Cell, Label, Pie, PieChart, Sector } from "recharts"; -import { FORMS } from "@forge/consts"; import type { ChartConfig } from "@forge/ui/chart"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -65,7 +65,10 @@ export default function ShirtSizePie({ members }: { members: Member[] }) { if (shirtSize && !baseConfig[shirtSize]) { baseConfig[shirtSize] = { label: shirtSize, - color: FORMS.ADMIN_PIE_CHART_COLORS[colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length], + color: + FORMS.ADMIN_PIE_CHART_COLORS[ + colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length + ], }; colorIdx++; } diff --git a/apps/blade/src/app/admin/club/data/_components/member-data/YearOfStudyPie.tsx b/apps/blade/src/app/admin/club/data/_components/member-data/YearOfStudyPie.tsx index 5194ee8e4..cbfa47939 100644 --- a/apps/blade/src/app/admin/club/data/_components/member-data/YearOfStudyPie.tsx +++ b/apps/blade/src/app/admin/club/data/_components/member-data/YearOfStudyPie.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import { Cell, Label, Pie, PieChart, Sector } from "recharts"; import type { ChartConfig } from "@forge/ui/chart"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -19,7 +20,6 @@ import { SelectTrigger, SelectValue, } from "@forge/ui/select"; -import { FORMS } from '@forge/consts'; interface Member { gradDate: Date | string; @@ -129,7 +129,10 @@ export default function YearOfStudyPie({ members }: YearOfStudyPieProps) { if (!baseConfig[name]) { baseConfig[name] = { label: name, - color: FORMS.ADMIN_PIE_CHART_COLORS[colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length], + color: + FORMS.ADMIN_PIE_CHART_COLORS[ + colorIdx % FORMS.ADMIN_PIE_CHART_COLORS.length + ], }; colorIdx++; } diff --git a/apps/blade/src/app/admin/club/events/_components/create-event.tsx b/apps/blade/src/app/admin/club/events/_components/create-event.tsx index 19d74ed47..89ff35b70 100644 --- a/apps/blade/src/app/admin/club/events/_components/create-event.tsx +++ b/apps/blade/src/app/admin/club/events/_components/create-event.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { Loader2, Plus } from "lucide-react"; import { z } from "zod"; +import { EVENTS } from "@forge/consts"; import { InsertEventSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; @@ -38,7 +39,6 @@ import { Textarea } from "@forge/ui/textarea"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -import { EVENTS } from '@forge/consts'; // 12-hour-based hours (1–12), displayed as "01", "02", ..., "12" const hours = Array.from({ length: 12 }, (_, i) => diff --git a/apps/blade/src/app/admin/club/events/_components/update-event.tsx b/apps/blade/src/app/admin/club/events/_components/update-event.tsx index c52bc5bca..60f46eb5a 100644 --- a/apps/blade/src/app/admin/club/events/_components/update-event.tsx +++ b/apps/blade/src/app/admin/club/events/_components/update-event.tsx @@ -5,6 +5,7 @@ import { Loader2, Pencil } from "lucide-react"; import { z } from "zod"; import type { InsertEvent } from "@forge/db/schemas/knight-hacks"; +import { EVENTS } from "@forge/consts"; import { InsertEventSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; @@ -39,7 +40,6 @@ import { Textarea } from "@forge/ui/textarea"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -import { EVENTS } from '@forge/consts'; // 12-hour-based hours (1–12) const hours = Array.from({ length: 12 }, (_, i) => diff --git a/apps/blade/src/app/admin/club/members/_components/delete-member.tsx b/apps/blade/src/app/admin/club/members/_components/delete-member.tsx index a7f8d515a..1a9a2b727 100644 --- a/apps/blade/src/app/admin/club/members/_components/delete-member.tsx +++ b/apps/blade/src/app/admin/club/members/_components/delete-member.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { Loader2, Trash2 } from "lucide-react"; +import { USE_CAUTION } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -17,7 +18,6 @@ import { Input } from "@forge/ui/input"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -import { USE_CAUTION } from '@forge/consts'; export default function DeleteMemberButton({ member, diff --git a/apps/blade/src/app/admin/club/members/_components/final-dues-dialog.tsx b/apps/blade/src/app/admin/club/members/_components/final-dues-dialog.tsx index f89e1ac2d..2df2d9b8f 100644 --- a/apps/blade/src/app/admin/club/members/_components/final-dues-dialog.tsx +++ b/apps/blade/src/app/admin/club/members/_components/final-dues-dialog.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { Loader2 } from "lucide-react"; +import { CLEAR_DUES_MESSAGE, USE_CAUTION } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -17,7 +18,6 @@ import { Input } from "@forge/ui/input"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -import { CLEAR_DUES_MESSAGE, USE_CAUTION } from '@forge/consts'; export default function FinalDuesDialogButton({ disabled, diff --git a/apps/blade/src/app/admin/club/members/_components/member-profile.tsx b/apps/blade/src/app/admin/club/members/_components/member-profile.tsx index d51f6b5d3..5c6c40719 100644 --- a/apps/blade/src/app/admin/club/members/_components/member-profile.tsx +++ b/apps/blade/src/app/admin/club/members/_components/member-profile.tsx @@ -6,6 +6,7 @@ import { User } from "lucide-react"; import { FaGithub, FaGlobe, FaLinkedin } from "react-icons/fa"; import type { InsertMember } from "@forge/db/schemas/knight-hacks"; +import { FORMS, MEMBER_PROFILE_ICON_SIZE } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -16,7 +17,6 @@ import { } from "@forge/ui/dialog"; import { api } from "~/trpc/react"; -import { FORMS, MEMBER_PROFILE_ICON_SIZE } from '@forge/consts'; export default function MemberProfileButton({ member, diff --git a/apps/blade/src/app/admin/club/members/_components/update-member.tsx b/apps/blade/src/app/admin/club/members/_components/update-member.tsx index 34565b8e6..c995ac524 100644 --- a/apps/blade/src/app/admin/club/members/_components/update-member.tsx +++ b/apps/blade/src/app/admin/club/members/_components/update-member.tsx @@ -5,6 +5,7 @@ import { Loader2, Pencil } from "lucide-react"; import { z } from "zod"; import type { InsertMember } from "@forge/db/schemas/knight-hacks"; +import { FORMS } from "@forge/consts"; import { InsertMemberSchema } from "@forge/db/schemas/knight-hacks"; import { Button } from "@forge/ui/button"; import { @@ -37,7 +38,6 @@ import { import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -import { FORMS } from '@forge/consts'; export default function UpdateMemberButton({ member, diff --git a/apps/blade/src/app/admin/forms/[slug]/client.tsx b/apps/blade/src/app/admin/forms/[slug]/client.tsx index 1931a7635..992106a09 100644 --- a/apps/blade/src/app/admin/forms/[slug]/client.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/client.tsx @@ -256,18 +256,19 @@ export function EditorClient({ responseRoleIds, } as any); }, [ + isLoading, + isFetching, formTitle, + formData, + slug, + questions, + updateFormMutation, formDescription, formBanner, - questions, instructions, duesOnly, allowResubmission, - formData, - isLoading, - isFetching, - updateFormMutation, - slug, + responseRoleIds, ]); useEffect(() => { @@ -294,21 +295,19 @@ export function EditorClient({ setAllowResubmission(formData.allowResubmission); setResponseRoleIds((formData as any).responseRoleIds || []); - const loadedQuestions: UIQuestion[] = formData.formData.questions.map( - (q: FormQuestion & { order?: number }) => ({ - ...q, - id: crypto.randomUUID(), - }), - ); + const loadedQuestions: UIQuestion[] = ( + formData.formData as FORMS.FormType + ).questions.map((q: FormQuestion & { order?: number }) => ({ + ...q, + id: crypto.randomUUID(), + })); - /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ const loadedInstructions: UIInstruction[] = ( - (formData.formData as any).instructions || [] + (formData.formData as FORMS.FormType).instructions || [] ).map((inst: FormInstruction & { order?: number }) => ({ ...inst, id: crypto.randomUUID(), })); - /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ setQuestions(loadedQuestions); setInstructions(loadedInstructions); @@ -320,12 +319,12 @@ export function EditorClient({ // auto save trigger when toggle switches are changed useEffect(() => { if (!isLoading) handleSaveForm(); - }, [duesOnly, allowResubmission, responseRoleIds, isLoading]); // removed handleSaveForm to prevent save-on-every-render + }, [duesOnly, allowResubmission, responseRoleIds, isLoading, handleSaveForm]); // removed handleSaveForm to prevent save-on-every-render // auto save when finishing editing an item (changing active card) useEffect(() => { if (!isLoading) handleSaveForm(); - }, [activeItemId, isLoading]); // triggers when switching items or clicking off + }, [activeItemId, handleSaveForm, isLoading]); // triggers when switching items or clicking off // Periodic auto-save every 40 seconds useEffect(() => { diff --git a/apps/blade/src/app/admin/forms/[slug]/responses/_components/AllResponsesView.tsx b/apps/blade/src/app/admin/forms/[slug]/responses/_components/AllResponsesView.tsx index 5b30fc0ab..7496de371 100644 --- a/apps/blade/src/app/admin/forms/[slug]/responses/_components/AllResponsesView.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/responses/_components/AllResponsesView.tsx @@ -1,4 +1,5 @@ -import type { FORMS } from '@forge/consts'; +import type { FORMS } from "@forge/consts"; + import { FileUploadResponsesTable } from "./FileUploadResponsesTable"; import { ResponseBarChart } from "./ResponseBarChart"; import { ResponseHorizontalBarChart } from "./ResponseHorizontalBarChart"; diff --git a/apps/blade/src/app/admin/forms/[slug]/responses/_components/PerUserResponsesView.tsx b/apps/blade/src/app/admin/forms/[slug]/responses/_components/PerUserResponsesView.tsx index 7fde2dd3c..93bc94b65 100644 --- a/apps/blade/src/app/admin/forms/[slug]/responses/_components/PerUserResponsesView.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/responses/_components/PerUserResponsesView.tsx @@ -15,13 +15,13 @@ import { X, } from "lucide-react"; +import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { Separator } from "@forge/ui/separator"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -import type { FORMS } from '@forge/consts'; interface PerUserResponsesViewProps { formData: FORMS.FormType; diff --git a/apps/blade/src/app/admin/forms/[slug]/responses/_components/ResponsePieChart.tsx b/apps/blade/src/app/admin/forms/[slug]/responses/_components/ResponsePieChart.tsx index 92ff50b91..32dbd1d8c 100644 --- a/apps/blade/src/app/admin/forms/[slug]/responses/_components/ResponsePieChart.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/responses/_components/ResponsePieChart.tsx @@ -4,6 +4,7 @@ import { Cell, Pie, PieChart } from "recharts"; +import { FORMS } from "@forge/consts"; import { Card, CardContent, CardHeader, CardTitle } from "@forge/ui/card"; import { ChartContainer, @@ -11,7 +12,6 @@ import { ChartTooltip, ChartTooltipContent, } from "@forge/ui/chart"; -import { FORMS } from '@forge/consts'; // props - expects a question string and array of responses interface ResponsePieChartProps { @@ -78,8 +78,9 @@ export function ResponsePieChart({ chartConfig[item.name] = { label: item.name, color: - FORMS.ADMIN_PIE_CHART_COLORS[index % FORMS.ADMIN_PIE_CHART_COLORS.length] ?? - "#000000", + FORMS.ADMIN_PIE_CHART_COLORS[ + index % FORMS.ADMIN_PIE_CHART_COLORS.length + ] ?? "#000000", }; }); diff --git a/apps/blade/src/app/admin/hackathon/events/_components/update-event.tsx b/apps/blade/src/app/admin/hackathon/events/_components/update-event.tsx index 9b681457e..eb270b550 100644 --- a/apps/blade/src/app/admin/hackathon/events/_components/update-event.tsx +++ b/apps/blade/src/app/admin/hackathon/events/_components/update-event.tsx @@ -166,7 +166,7 @@ export function UpdateEventButton({ event }: { event: InsertEvent }) { const currentTag = form.getValues("tag"); const points = EVENTS.EVENT_POINTS[currentTag] || 0; form.setValue("points", points); - }, [form.watch("tag")]); + }, [form]); const onSubmit = form.handleSubmit((values) => { setIsLoading(true); diff --git a/apps/blade/src/app/admin/hackathon/hackers/_components/hacker-profile.tsx b/apps/blade/src/app/admin/hackathon/hackers/_components/hacker-profile.tsx index 6d192939c..587008ff5 100644 --- a/apps/blade/src/app/admin/hackathon/hackers/_components/hacker-profile.tsx +++ b/apps/blade/src/app/admin/hackathon/hackers/_components/hacker-profile.tsx @@ -6,6 +6,7 @@ import { User } from "lucide-react"; import { FaGithub, FaGlobe, FaLinkedin } from "react-icons/fa"; import type { InsertHacker } from "@forge/db/schemas/knight-hacks"; +import { FORMS, MEMBER_PROFILE_ICON_SIZE } from "@forge/consts"; import { Badge } from "@forge/ui/badge"; import { Button } from "@forge/ui/button"; import { @@ -19,7 +20,6 @@ import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; import FoodRestrictionsButton from "./food-restrictions"; -import { FORMS, MEMBER_PROFILE_ICON_SIZE } from '@forge/consts'; export default function HackerProfileButton({ hacker, diff --git a/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx b/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx index a55ba0e05..d64049499 100644 --- a/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx +++ b/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx @@ -2,7 +2,7 @@ import type { APIRole } from "discord-api-types/v10"; import type { ZodBoolean } from "zod"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Link, Loader2, Pencil, User, X } from "lucide-react"; import { z } from "zod"; @@ -79,9 +79,20 @@ export default function RoleEdit({ defaultValues: defaults, }); + const updateString = useCallback((values: z.infer) => { + const perms = Object.entries(values); + let newString = ""; + perms.forEach((v) => { + if (v[1]) newString += "1"; + else newString += "0"; + }); + + setPermString(newString); + }, []); + useEffect(() => { updateString(form.getValues()); - }, []); + }, [form, updateString]); useEffect(() => { if (roles) @@ -98,7 +109,7 @@ export default function RoleEdit({ } void doGetRole(); - }, [roleID]); + }, [oldRole, roleID, roleQ, roles]); useEffect(() => { if (roles) @@ -107,19 +118,7 @@ export default function RoleEdit({ ? false : roles.find((v) => v.name == name) != undefined, ); - }, [name]); - - function updateString(values: z.infer) { - const perms = Object.entries(values); - console.log(perms); - let newString = ""; - perms.forEach((v) => { - if (v[1]) newString += "1"; - else newString += "0"; - }); - - setPermString(newString); - } + }, [name, oldRole?.name, roles]); function sendRole(str: string) { try { diff --git a/apps/blade/src/app/admin/roles/configure/_components/roletable.tsx b/apps/blade/src/app/admin/roles/configure/_components/roletable.tsx index 5f5617f43..839fe5fe3 100644 --- a/apps/blade/src/app/admin/roles/configure/_components/roletable.tsx +++ b/apps/blade/src/app/admin/roles/configure/_components/roletable.tsx @@ -54,7 +54,7 @@ export default function RoleTable() { } if (roles) void fetchDiscordRoles(); - }, [roles]); + }, [discordRolesQ, roles]); function deleteRole(id: string) { try { diff --git a/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx b/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx index e85e1c7ed..38b86544e 100644 --- a/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx +++ b/apps/blade/src/app/dashboard/_components/hackathon-dashboard/hackathon-dashboard.tsx @@ -72,8 +72,8 @@ export default async function HackathonDashboard({ classPfp: string; } - const HACKER_CLASS_INFO_TYPED: Record = - DISCORD.KNIGHTHACKS_8.HACKER_CLASS_INFO as Record; + const HACKER_CLASS_INFO_TYPED: Record = DISCORD + .KNIGHTHACKS_8.HACKER_CLASS_INFO as Record; const classInfo = HACKER_CLASS_INFO_TYPED[hacker.class] ?? { teamColor: "#000000", diff --git a/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx b/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx index ca6fe7a21..f14d56539 100644 --- a/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx +++ b/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx @@ -4,8 +4,8 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import { CircleCheckBig, Loader2 } from "lucide-react"; -import { HACKATHON_TEMPLATE_IDS } from "@forge/email/client"; import { USE_CAUTION } from "@forge/consts"; +import { HACKATHON_TEMPLATE_IDS } from "@forge/email/client"; import { Button } from "@forge/ui/button"; import { Dialog, diff --git a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx index 530f798e2..3e98f5d58 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import { CheckCircle2, Loader2, XCircle } from "lucide-react"; import { z } from "zod"; +import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; @@ -109,7 +110,7 @@ export function FormResponderClient({ ? true : (duesQuery.data?.duesPaid ?? false); - const form = formQuery.data.formData; + const form = formQuery.data.formData as FORMS.FormType; const isDuesOnly = formQuery.data.duesOnly; const allowResubmission = formQuery.data.allowResubmission; const hasAlreadySubmitted = existingResponseQuery.data?.length !== 0; diff --git a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx index fc031d623..36257fc40 100644 --- a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx @@ -6,6 +6,7 @@ import { useRef, useState } from "react"; import Image from "next/image"; import { FileUp, Loader2, X } from "lucide-react"; +import { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; import { Checkbox } from "@forge/ui/checkbox"; @@ -27,7 +28,6 @@ import { TimePicker } from "@forge/ui/time-picker"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -import { FORMS } from '@forge/consts'; type FormQuestion = z.infer; diff --git a/apps/blade/src/app/member/application/_components/member-application-form.tsx b/apps/blade/src/app/member/application/_components/member-application-form.tsx index 45fbde29a..a7350efc2 100644 --- a/apps/blade/src/app/member/application/_components/member-application-form.tsx +++ b/apps/blade/src/app/member/application/_components/member-application-form.tsx @@ -6,8 +6,7 @@ import { useRouter } from "next/navigation"; import { Loader2 } from "lucide-react"; import { z } from "zod"; -import type { - GradTerm} from "@forge/consts"; +import type { GradTerm } from "@forge/consts"; import { ALLOWED_PROFILE_PICTURE_EXTENSIONS, ALLOWED_PROFILE_PICTURE_TYPES, diff --git a/apps/blade/src/app/settings/_components/delete-member-button.tsx b/apps/blade/src/app/settings/_components/delete-member-button.tsx index 3b2c611f0..00b12bf9c 100644 --- a/apps/blade/src/app/settings/_components/delete-member-button.tsx +++ b/apps/blade/src/app/settings/_components/delete-member-button.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { Loader2, Trash2 } from "lucide-react"; +import { USE_CAUTION } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -18,7 +19,6 @@ import { Input } from "@forge/ui/input"; import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; -import { USE_CAUTION } from '@forge/consts'; export default function DeleteMemberButton({ memberId, diff --git a/apps/blade/src/app/settings/hacker-profile/page.tsx b/apps/blade/src/app/settings/hacker-profile/page.tsx index 8d4fa37cf..4ce418518 100644 --- a/apps/blade/src/app/settings/hacker-profile/page.tsx +++ b/apps/blade/src/app/settings/hacker-profile/page.tsx @@ -3,11 +3,11 @@ import Link from "next/link"; import { redirect } from "next/navigation"; import { auth } from "@forge/auth"; +import { DISCORD } from "@forge/consts"; import { Separator } from "@forge/ui/separator"; import { api, HydrateClient } from "~/trpc/server"; import { HackerProfileForm } from "./hacker-profile-form"; -import { DISCORD } from '@forge/consts'; export default async function SettingsProfilePage() { const session = await auth(); diff --git a/apps/blade/src/components/forms/form-card.tsx b/apps/blade/src/components/forms/form-card.tsx index bfe2c5ef4..a654e4ce0 100644 --- a/apps/blade/src/components/forms/form-card.tsx +++ b/apps/blade/src/components/forms/form-card.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import Link from "next/link"; import { QrCode } from "lucide-react"; +import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card, @@ -139,7 +140,8 @@ export function FormCard({

    - {fullForm?.formData.description || "No description"} + {(fullForm?.formData as FORMS.FormType).description || + "No description"}

    diff --git a/apps/cron/src/crons/role-sync.ts b/apps/cron/src/crons/role-sync.ts index 9ab47d866..cc771e6de 100644 --- a/apps/cron/src/crons/role-sync.ts +++ b/apps/cron/src/crons/role-sync.ts @@ -1,7 +1,8 @@ import type { APIGuildMember } from "discord-api-types/v10"; import { Routes } from "discord-api-types/v10"; -import { discord, KNIGHTHACKS_GUILD_ID } from "@forge/api/utils"; +import { discord } from "@forge/api/utils"; +import { DISCORD } from "@forge/consts"; import { eq } from "@forge/db"; import { db } from "@forge/db/client"; import { Permissions, Roles, User } from "@forge/db/schemas/auth"; @@ -37,7 +38,7 @@ export const roleSync = new CronBuilder({ try { // Fetch the user's roles from Discord const guildMember = (await discord.get( - Routes.guildMember(KNIGHTHACKS_GUILD_ID, user.discordUserId), + Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, user.discordUserId), )) as APIGuildMember; const discordRoleIds = guildMember.roles; diff --git a/apps/gemiknights/src/app/_components/ui/background-gradient-animation.tsx b/apps/gemiknights/src/app/_components/ui/background-gradient-animation.tsx index cce610337..0fa14255a 100644 --- a/apps/gemiknights/src/app/_components/ui/background-gradient-animation.tsx +++ b/apps/gemiknights/src/app/_components/ui/background-gradient-animation.tsx @@ -58,7 +58,18 @@ export const BackgroundGradientAnimation = ({ document.body.style.setProperty("--pointer-color", pointerColor); document.body.style.setProperty("--size", size); document.body.style.setProperty("--blending-value", blendingValue); - }, []); + }, [ + blendingValue, + fifthColor, + firstColor, + fourthColor, + gradientBackgroundEnd, + gradientBackgroundStart, + pointerColor, + secondColor, + size, + thirdColor, + ]); useEffect(() => { function move() { @@ -73,7 +84,7 @@ export const BackgroundGradientAnimation = ({ } move(); - }, [tgX, tgY]); + }, [curX, curY, tgX, tgY]); const handleMouseMove = (event: React.MouseEvent) => { if (interactiveRef.current) { diff --git a/packages/api/src/routers/dues-payment.ts b/packages/api/src/routers/dues-payment.ts index 57cc3d325..98b67fa88 100644 --- a/packages/api/src/routers/dues-payment.ts +++ b/packages/api/src/routers/dues-payment.ts @@ -3,6 +3,7 @@ import { TRPCError } from "@trpc/server"; import Stripe from "stripe"; import { z } from "zod"; +import { KNIGHTHACKS_MEMBERSHIP_PRICE } from "@forge/consts"; import { eq } from "@forge/db"; import { db } from "@forge/db/client"; import { DuesPayment, Member } from "@forge/db/schemas/knight-hacks"; @@ -10,7 +11,6 @@ import { DuesPayment, Member } from "@forge/db/schemas/knight-hacks"; import { env } from "../env"; import { protectedProcedure } from "../trpc"; import { log, stripe } from "../utils"; -import { KNIGHTHACKS_MEMBERSHIP_PRICE } from '@forge/consts'; export const duesPaymentRouter = { createCheckout: protectedProcedure.mutation(async ({ ctx }) => { diff --git a/packages/api/src/routers/event-feedback.ts b/packages/api/src/routers/event-feedback.ts index 2b009ad2e..5fd4a4462 100644 --- a/packages/api/src/routers/event-feedback.ts +++ b/packages/api/src/routers/event-feedback.ts @@ -2,6 +2,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; +import { DISCORD, EVENTS } from "@forge/consts"; import { and, eq, sql } from "@forge/db"; import { db } from "@forge/db/client"; import { @@ -12,7 +13,6 @@ import { import { permProcedure } from "../trpc"; import { controlPerms, log } from "../utils"; -import { DISCORD, EVENTS } from '@forge/consts'; export const eventFeedbackRouter = { createEventFeedback: permProcedure diff --git a/packages/api/src/routers/hacker.ts b/packages/api/src/routers/hacker.ts index 05f4c077c..e085ea2b0 100644 --- a/packages/api/src/routers/hacker.ts +++ b/packages/api/src/routers/hacker.ts @@ -1181,7 +1181,7 @@ export const hackerRouter = { if (assignedClass) { await addRoleToMember( discordId, - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unnecessary-condition + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition DISCORD.KNIGHTHACKS_8.CLASS_ROLE_ID[ assignedClass as AssignableHackerClass ] ?? "", diff --git a/packages/consts/src/discord/project-launch-26.ts b/packages/consts/src/discord/project-launch-26.ts index b9ed9efbc..b9425b815 100644 --- a/packages/consts/src/discord/project-launch-26.ts +++ b/packages/consts/src/discord/project-launch-26.ts @@ -1 +1 @@ -export const MEMBER_ROLE = "1467281189088788489" \ No newline at end of file +export const MEMBER_ROLE = "1467281189088788489"; diff --git a/packages/consts/src/util.ts b/packages/consts/src/util.ts index 18e9729bc..d801f1fec 100644 --- a/packages/consts/src/util.ts +++ b/packages/consts/src/util.ts @@ -1 +1 @@ -export const IS_PROD = process.env.NODE_ENV === "production"; \ No newline at end of file +export const IS_PROD = process.env.NODE_ENV === "production"; diff --git a/packages/consts/tsconfig.json b/packages/consts/tsconfig.json index 595256422..ca8233d53 100644 --- a/packages/consts/tsconfig.json +++ b/packages/consts/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@forge/tsconfig/internal-package.json", - "include": ["src"], + "include": ["src"] } From 9d1646bc9709d560e1eaf478c3d41e639b5d3359 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:09:31 -0500 Subject: [PATCH 10/23] chore(pnpm): install packages --- pnpm-lock.yaml | 72 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bcc6432d9..2f3522796 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,6 @@ importers: '@forge/api': specifier: workspace:* version: link:../../packages/api - '@forge/consts': - specifier: workspace:* - version: link:../../packages/consts '@forge/ui': specifier: workspace:* version: link:../../packages/ui @@ -404,7 +401,7 @@ importers: version: 7.4.4 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' version: 3.8.1 @@ -477,7 +474,7 @@ importers: version: 0.37.120 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) prettier: specifier: 'catalog:' version: 3.8.1 @@ -499,9 +496,6 @@ importers: '@forge/auth': specifier: workspace:* version: link:../../packages/auth - '@forge/consts': - specifier: workspace:* - version: link:../../packages/consts '@forge/db': specifier: workspace:* version: link:../../packages/db @@ -856,7 +850,7 @@ importers: version: 0.11.1(typescript@5.7.3)(zod@3.25.76) better-auth: specifier: ^1.3.34 - version: 1.4.18(mongodb@6.20.0)(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(pg@8.18.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 1.4.18(mongodb@6.20.0(socks@2.8.7))(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(pg@8.18.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: specifier: ^14.2.26 version: 14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -891,9 +885,6 @@ importers: packages/consts: dependencies: - '@forge/db': - specifier: workspace:* - version: link:../db minimatch: specifier: ^10.1.2 version: 10.1.2 @@ -922,9 +913,6 @@ importers: packages/db: dependencies: - '@forge/api': - specifier: workspace:* - version: link:../api '@forge/consts': specifier: workspace:* version: link:../consts @@ -5963,7 +5951,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global@4.4.0: resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} @@ -10087,6 +10075,11 @@ snapshots: '@esbuild/win32-x64@0.19.12': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@1.21.7))': + dependencies: + eslint: 9.39.2(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': dependencies: eslint: 9.39.2(jiti@2.6.1) @@ -12578,7 +12571,7 @@ snapshots: basic-ftp@5.1.0: {} - better-auth@1.4.18(mongodb@6.20.0)(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(pg@8.18.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + better-auth@1.4.18(mongodb@6.20.0(socks@2.8.7))(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(pg@8.18.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0) '@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)) @@ -13433,7 +13426,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -13455,7 +13448,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13564,6 +13557,47 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint@9.39.2(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + eslint@9.39.2(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) From 63cc249e968c8d80be548365b5727e80ce9a9f40 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:54:53 -0500 Subject: [PATCH 11/23] Squashed commit of the following: commit 109dfb6196b5f34fb9cbd842bce0becbb080e3f7 Author: Aiden Letourneau <63489431+aidenletourneau@users.noreply.github.com> Date: Sat Feb 7 22:40:21 2026 -0500 [#342] Allow editing responses in Blade forms (#315) --- .../src/app/admin/forms/[slug]/client.tsx | 28 +- .../member-dashboard/forms/form-responses.tsx | 25 +- .../forms/[formName]/[responseId]/page.tsx | 12 +- .../[formName]/_components/form-not-found.tsx | 20 + .../_components/form-responder-client.tsx | 435 +++--------------- .../[formName]/_components/form-runner.tsx | 210 +++++++++ .../_components/form-submitted-success.tsx | 53 +++ .../_components/form-view-edit-client.tsx | 387 ++++------------ .../_components/question-response-card.tsx | 11 +- .../_components/response-not-found.tsx | 20 + .../app/forms/[formName]/_components/utils.ts | 88 ++++ .../[formName]/_hooks/useSubmissionSuccess.ts | 71 +++ apps/blade/src/app/forms/[formName]/page.tsx | 14 +- packages/api/src/routers/forms.ts | 126 ++++- packages/db/src/schemas/knight-hacks.ts | 2 + 15 files changed, 800 insertions(+), 702 deletions(-) create mode 100644 apps/blade/src/app/forms/[formName]/_components/form-not-found.tsx create mode 100644 apps/blade/src/app/forms/[formName]/_components/form-runner.tsx create mode 100644 apps/blade/src/app/forms/[formName]/_components/form-submitted-success.tsx create mode 100644 apps/blade/src/app/forms/[formName]/_components/response-not-found.tsx create mode 100644 apps/blade/src/app/forms/[formName]/_components/utils.ts create mode 100644 apps/blade/src/app/forms/[formName]/_hooks/useSubmissionSuccess.ts diff --git a/apps/blade/src/app/admin/forms/[slug]/client.tsx b/apps/blade/src/app/admin/forms/[slug]/client.tsx index 992106a09..c1d49938a 100644 --- a/apps/blade/src/app/admin/forms/[slug]/client.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/client.tsx @@ -184,6 +184,7 @@ export function EditorClient({ const [instructions, setInstructions] = useState([]); const [duesOnly, setDuesOnly] = useState(false); const [allowResubmission, setAllowResubmission] = useState(true); + const [allowEdit, setAllowEdit] = useState(true); const [responseRoleIds, setResponseRoleIds] = useState([]); const [responseRolesDialogOpen, setResponseRolesDialogOpen] = useState(false); const [activeItemId, setActiveItemId] = useState(null); @@ -253,6 +254,7 @@ export function EditorClient({ }, duesOnly, allowResubmission, + allowEdit, responseRoleIds, } as any); }, [ @@ -268,6 +270,7 @@ export function EditorClient({ instructions, duesOnly, allowResubmission, + allowEdit, responseRoleIds, ]); @@ -293,6 +296,7 @@ export function EditorClient({ setFormBanner(formData.formData.banner || ""); setDuesOnly(formData.duesOnly); setAllowResubmission(formData.allowResubmission); + setAllowEdit(formData.allowEdit); setResponseRoleIds((formData as any).responseRoleIds || []); const loadedQuestions: UIQuestion[] = ( @@ -319,7 +323,14 @@ export function EditorClient({ // auto save trigger when toggle switches are changed useEffect(() => { if (!isLoading) handleSaveForm(); - }, [duesOnly, allowResubmission, responseRoleIds, isLoading, handleSaveForm]); // removed handleSaveForm to prevent save-on-every-render + }, [ + duesOnly, + allowResubmission, + responseRoleIds, + isLoading, + allowEdit, + handleSaveForm, + ]); // removed handleSaveForm to prevent save-on-every-render // auto save when finishing editing an item (changing active card) useEffect(() => { @@ -541,7 +552,7 @@ export function EditorClient({
    -
    +
    +
    + + +
    - +
    ))} @@ -54,17 +57,3 @@ export async function FormResponses() { ); } - -function ViewFormResponseButton({ - responseIdSlug, - formNameSlug, -}: { - responseIdSlug: string; - formNameSlug: string; -}) { - return ( - - - - ); -} diff --git a/apps/blade/src/app/forms/[formName]/[responseId]/page.tsx b/apps/blade/src/app/forms/[formName]/[responseId]/page.tsx index 75294e6c8..bcff006e3 100644 --- a/apps/blade/src/app/forms/[formName]/[responseId]/page.tsx +++ b/apps/blade/src/app/forms/[formName]/[responseId]/page.tsx @@ -3,7 +3,9 @@ import { redirect } from "next/navigation"; import { auth } from "@forge/auth/server"; import { api, HydrateClient } from "~/trpc/server"; -import { FormReviewClient } from "../_components/form-view-edit-client"; +import FormNotFound from "../_components/form-not-found"; +import { FormReviewWrapper } from "../_components/form-view-edit-client"; +import ResponseNotFound from "../_components/response-not-found"; function serializeSearchParams( searchParams: Record, @@ -40,8 +42,12 @@ export default async function FormResponderPage({ redirect(`/?callbackURL=${encodeURIComponent(callbackURL)}`); } + if (!params.formName) { + return ; + } + if (!params.responseId) { - return
    Submission not found
    ; + return ; } // handle url encode form names to allow spacing and special characters @@ -61,7 +67,7 @@ export default async function FormResponderPage({ return ( - + + +

    Form Not Found

    +

    + This form doesn't exist or may have been removed. +

    +

    + Please let a team member know if you think this is an error. +

    +
    +
    + ); +} diff --git a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx index 3e98f5d58..5397bfb6a 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx @@ -1,55 +1,46 @@ "use client"; -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; +import { useState } from "react"; +import Link from "next/link"; import { CheckCircle2, Loader2, XCircle } from "lucide-react"; -import { z } from "zod"; import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; -import { InstructionResponseCard } from "~/app/forms/[formName]/_components/instruction-response-card"; -import { QuestionResponseCard } from "~/app/forms/[formName]/_components/question-response-card"; +import type { FormResponsePayload } from "./utils"; import { api } from "~/trpc/react"; +import { useSubmissionSuccess } from "../_hooks/useSubmissionSuccess"; +import FormNotFound from "./form-not-found"; +import { FormRunner } from "./form-runner"; +import { SubmissionSuccessCard } from "./form-submitted-success"; -const emailSchema = z.string().email("Invalid email address"); -const phoneSchema = z.string().regex(/^\+?\d{7,15}$/, "Invalid phone number"); -const linkSchema = z.string().url("Please enter a valid URL"); - -interface FormResponderClientProps { +interface FormResponderWrapperProps { formName: string; userName: string; handleCallbacks: (response: Record) => void; } -export function FormResponderClient({ +export function FormResponderWrapper({ formName, userName, handleCallbacks, -}: FormResponderClientProps) { - const router = useRouter(); - const [responses, setResponses] = useState< - Record - >({}); - const [touchedFields, setTouchedFields] = useState>(new Set()); +}: FormResponderWrapperProps) { const [isSubmitted, setIsSubmitted] = useState(false); - const [showCheckmark, setShowCheckmark] = useState(false); - const [showText, setShowText] = useState(false); const [submitError, setSubmitError] = useState(null); - const [redirectCountdown, setRedirectCountdown] = useState(5); - const formQuery = api.forms.getForm.useQuery({ - slug_name: formName, - }); + const { showCheckmark, showText, redirectCountdown } = + useSubmissionSuccess(isSubmitted); - // is bro a dues paying member? + const formQuery = api.forms.getForm.useQuery({ slug_name: formName }); const duesQuery = api.duesPayment.validatePaidDues.useQuery(); - // did bro submit alr? - const existingResponseQuery = api.forms.getUserResponse.useQuery({ - form: formQuery.data?.id ?? "", - }); + const formIdGate = formQuery.data?.id; + + const existingResponseQuery = api.forms.getUserResponse.useQuery( + { form: formIdGate }, + { enabled: !!formIdGate }, + ); const submitResponse = api.forms.createResponse.useMutation({ onSuccess: (_data, variables) => { @@ -64,58 +55,40 @@ export function FormResponderClient({ }, }); - // Staggered animation for success screen - useEffect(() => { - if (isSubmitted) { - const checkTimer = setTimeout(() => setShowCheckmark(true), 100); - const textTimer = setTimeout(() => setShowText(true), 400); - - // countdown - const countdownInterval = setInterval(() => { - setRedirectCountdown((prev) => prev - 1); - }, 1000); - - const redirectTimer = setTimeout(() => { - router.push("/"); - }, 5000); - - return () => { - clearTimeout(checkTimer); - clearTimeout(textTimer); - clearInterval(countdownInterval); - clearTimeout(redirectTimer); - }; - } - }, [isSubmitted, router]); - - // wait for all queries to load if ( formQuery.isLoading || duesQuery.isLoading || existingResponseQuery.isLoading - ) + ) { return (
    ); - - // if form fails to load show error - if (formQuery.error || !formQuery.data) { - return ; } + if (formQuery.error || !formQuery.data) return ; + + const formId = formQuery.data.id; + + // not found + if (existingResponseQuery.error) + return
    Error Loading existing response
    ; + + const form = formQuery.data.formData as FORMS.FormType; + const zodValidator = formQuery.data.zodValidator; + const isDuesOnly = formQuery.data.duesOnly; + const allowResubmission = formQuery.data.allowResubmission; + const allowEdit = formQuery.data.allowEdit; + const duesCheckFailed = !!duesQuery.error; const hasPaidDues = duesCheckFailed ? true : (duesQuery.data?.duesPaid ?? false); - const form = formQuery.data.formData as FORMS.FormType; - const isDuesOnly = formQuery.data.duesOnly; - const allowResubmission = formQuery.data.allowResubmission; - const hasAlreadySubmitted = existingResponseQuery.data?.length !== 0; + const hasAlreadySubmitted = (existingResponseQuery.data?.length ?? 0) !== 0; - // BRO DID NOT PAY DUES!!! + // dues gate if (isDuesOnly && !hasPaidDues) { return (
    @@ -130,8 +103,9 @@ export function FormResponderClient({ ); } - // dude they're trying to over throw the elections with multiple submissions + // already submitted gate if (hasAlreadySubmitted && !allowResubmission) { + const existing = existingResponseQuery.data?.[0]; return (
    @@ -140,323 +114,50 @@ export function FormResponderClient({

    You have already submitted a response to this form.

    + + {existing && ( + + + + )}
    ); } - // SUCESSSSS + // success if (isSubmitted) { return ( -
    - -
    - -
    -
    -

    Thanks, {userName}!

    -

    - Your response to "{form.name}" has been recorded. -

    -

    - Redirecting in {redirectCountdown}... -

    -
    -
    -
    + ); } - const handleResponseChange = ( - questionText: string, - value: string | string[] | number | Date | null, - ) => { - setResponses((prev) => ({ - ...prev, - [questionText]: value, - })); - }; - - const handleFieldBlur = (questionText: string) => { - setTouchedFields((prev) => new Set(prev).add(questionText)); - }; - - const handleSubmit = () => { - // Build response data object - const responseData: Record = {}; - - form.questions.forEach((question) => { - const response = responses[question.question]; - - // Only include non-empty responses - if (response !== null && response !== undefined && response !== "") { - if (Array.isArray(response) && response.length === 0) { - return; // Skip empty arrays - } - // Convert Date objects to ISO strings - if (response instanceof Date) { - if (question.type === "DATE") { - responseData[question.question] = response - .toISOString() - .split("T")[0]; - } else if (question.type === "TIME") { - responseData[question.question] = response - .toTimeString() - .slice(0, 5); - } - } else { - // Convert boolean strings to actual booleans for BOOLEAN question type - if (question.type === "BOOLEAN" && typeof response === "string") { - responseData[question.question] = response === "true"; - } else { - responseData[question.question] = response; - } - } - } - }); - + const onSubmit = (payload: FormResponsePayload) => { submitResponse.mutate({ - form: formQuery.data.id, - responseData, - }); - }; - - const getValidationError = (question: (typeof form.questions)[number]) => { - if (!touchedFields.has(question.question)) { - return null; - } - - const response = responses[question.question]; - - if (question.optional) { - if ( - !response || - response === "" || - (Array.isArray(response) && response.length === 0) - ) { - return null; - } - } else { - if (response === null || response === undefined || response === "") { - return "This field is required."; - } - if (Array.isArray(response) && response.length === 0) { - return "This field is required."; - } - // For required BOOLEAN questions, must be checked (true) - if (question.type === "BOOLEAN") { - const isChecked = - (typeof response === "string" && response === "true") || - (typeof response === "boolean" && response === true); - if (!isChecked) { - return "You must accept this to continue."; - } - } - } - - if (question.type === "EMAIL" && typeof response === "string") { - const result = emailSchema.safeParse(response); - if (!result.success) { - return "Please enter a valid email address"; - } - } - if (question.type === "PHONE" && typeof response === "string") { - const result = phoneSchema.safeParse(response); - if (!result.success) { - return "Please enter a valid phone number (7-15 digits, optional + prefix)"; - } - } - if (question.type === "LINK" && typeof response === "string") { - const result = linkSchema.safeParse(response); - if (!result.success) { - return "Please enter a valid URL"; - } - } - - return null; - }; - - const isFormValid = () => { - // Check if all required questions have responses - return form.questions.every((question) => { - if (question.optional) { - const response = responses[question.question]; - if ( - !response || - response === "" || - (Array.isArray(response) && response.length === 0) - ) { - return true; - } - - if (question.type === "EMAIL" && typeof response === "string") { - return emailSchema.safeParse(response).success; - } - if (question.type === "PHONE" && typeof response === "string") { - return phoneSchema.safeParse(response).success; - } - if (question.type === "LINK" && typeof response === "string") { - return linkSchema.safeParse(response).success; - } - return true; - } - - const response = responses[question.question]; - if (response === null || response === undefined || response === "") - return false; - if (Array.isArray(response) && response.length === 0) return false; - - // For required BOOLEAN questions, must be checked (true), not false - if (question.type === "BOOLEAN") { - if (typeof response === "string") { - return response === "true"; // Must be "true" string - } - if (typeof response === "boolean") { - return response === true; // Must be true boolean - } - return false; // Missing or invalid - } - - if (question.type === "EMAIL" && typeof response === "string") { - return emailSchema.safeParse(response).success; - } - if (question.type === "PHONE" && typeof response === "string") { - return phoneSchema.safeParse(response).success; - } - if (question.type === "LINK" && typeof response === "string") { - return linkSchema.safeParse(response).success; - } - - return true; + form: formId, + responseData: payload, }); }; return ( -
    -
    - {/* Banner */} - {form.banner &&
    } - - {/* Header */} - -
    -

    {form.name}

    - - {form.description && ( -

    {form.description}

    - )} -
    -
    - - {/* Questions and Instructions */} -
    - {(() => { - /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return */ - // Combine questions and instructions, sort by order - type QuestionWithOrder = (typeof form.questions)[number] & { - itemType: "question"; - }; - interface InstructionWithOrder { - itemType: "instruction"; - title: string; - content?: string; - imageUrl?: string; - videoUrl?: string; - order?: number; - } - - const questionsWithType: QuestionWithOrder[] = form.questions.map( - (q) => ({ - ...q, - itemType: "question" as const, - }), - ); - - const instructionsWithType: InstructionWithOrder[] = ( - (form as any).instructions || [] - ).map((inst: any) => ({ - ...inst, - itemType: "instruction" as const, - })); - - const allItems = [ - ...questionsWithType, - ...instructionsWithType, - ].sort((a, b) => (a.order ?? 999) - (b.order ?? 999)); - - return allItems.map((item, index) => { - const isInstruction = item.itemType === "instruction"; - - return ( -
    - {isInstruction ? ( - - ) : ( - { - handleResponseChange(item.question, value); - }} - onBlur={() => handleFieldBlur(item.question)} - formId={formQuery.data.id} - error={getValidationError(item)} - /> - )} -
    - ); - }); - /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return */ - })()} -
    - - {submitError && ( -
    - {submitError} -
    - )} - - {/* Action Buttons */} -
    - -
    -
    -
    - ); -} - -function FormNotFound() { - return ( -
    - - -

    Form Not Found

    -

    - This form doesn't exist or may have been removed. -

    -

    - Please let a team member know if you think this is an error. -

    -
    -
    + ); } diff --git a/apps/blade/src/app/forms/[formName]/_components/form-runner.tsx b/apps/blade/src/app/forms/[formName]/_components/form-runner.tsx new file mode 100644 index 000000000..589ff1165 --- /dev/null +++ b/apps/blade/src/app/forms/[formName]/_components/form-runner.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import type { + FormType, + InstructionValidatorType, +} from "@forge/consts/knight-hacks"; +import { Button } from "@forge/ui/button"; +import { Card } from "@forge/ui/card"; + +import type { FormResponsePayload, FormResponseUI } from "./utils"; +import { InstructionResponseCard } from "~/app/forms/[formName]/_components/instruction-response-card"; +import { QuestionResponseCard } from "~/app/forms/[formName]/_components/question-response-card"; +import { getValidationError, isFormValid, normalizeResponses } from "./utils"; + +/** + * Shared renderer for "fill out form" and "review/edit response". + * - Renders header + questions + instructions + * - Manages responses state (UI shape) + * - Calls `onSubmit` with normalized payload + */ +export function FormRunner({ + isReview = false, + form, + formId, + zodValidator, + initialResponses, + allowEdit = true, + isSubmitting = false, + submitError, + onSubmit, +}: { + isReview?: boolean; + form: FormType; + + formId: string; + + userName?: string; + + zodValidator: string; + + /** For edit/review mode: prefill UI responses */ + initialResponses?: FormResponseUI; + + /** Disable editing inputs (view-only) */ + allowEdit?: boolean; + + isSubmitting?: boolean; + + submitError?: string | null; + + /** Parent provides submit handler, FormRunner will normalize first */ + onSubmit: (payload: FormResponsePayload) => void; +}) { + const [responses, setResponses] = useState( + initialResponses ?? {}, + ); + + // If initialResponses, hydrate once it changes. + useEffect(() => { + if (!initialResponses) return; + setResponses(initialResponses); + }, [initialResponses]); + + const handleResponseChange = ( + questionText: string, + value: string | string[] | number | Date | boolean | null, + ) => { + // If view-only, ignore changes + if (!allowEdit) return; + + setResponses((prev) => { + if ( + value == null || + (typeof value === "string" && value.trim() === "") || + (Array.isArray(value) && value.length === 0) + ) { + const { [questionText]: _, ...rest } = prev; + return rest; + } + + return { ...prev, [questionText]: value }; + }); + }; + + const canSubmit = + allowEdit && !isSubmitting && isFormValid(zodValidator, responses, form); + + const handleSubmit = () => { + const payload = normalizeResponses(responses, form); + onSubmit(payload); + }; + + type QuestionWithOrder = (typeof form.questions)[number] & { + itemType: "question"; + }; + interface InstructionWithOrder { + itemType: "instruction"; + title: string; + content?: string; + imageUrl?: string; + videoUrl?: string; + order?: number; + } + + const questionsWithType: QuestionWithOrder[] = form.questions.map((q) => ({ + ...q, + itemType: "question" as const, + })); + + const instructionsWithType: InstructionWithOrder[] = ( + form.instructions || [] + ).map((inst: InstructionValidatorType) => ({ + ...inst, + itemType: "instruction" as const, + })); + + const allItems = [...questionsWithType, ...instructionsWithType].sort( + (a, b) => (a.order ?? 999) - (b.order ?? 999), + ); + + return ( +
    +
    + {/* Banner */} + {form.banner &&
    } + + {/* Header */} + +
    +

    + {isReview && `${allowEdit ? "Edit" : "View"} - `} + {form.name} +

    + + {form.description && ( +

    {form.description}

    + )} +
    +
    + + {/* Questions + Instructions */} +
    + {allItems.map((item, index) => { + const isInstruction = item.itemType === "instruction"; + + return ( +
    + {isInstruction ? ( + <> + + + ) : ( + { + handleResponseChange(item.question, value); + }} + formId={formId} + error={getValidationError( + item, + zodValidator, + responses, + form, + )} + disabled={!allowEdit} + /> + )} +
    + ); + })} +
    + + {submitError && ( +
    + {submitError} +
    + )} + + {/* Actions */} +
    + {!isReview ? ( + + ) : ( + allowEdit && ( + + ) + )} +
    +
    +
    + ); +} diff --git a/apps/blade/src/app/forms/[formName]/_components/form-submitted-success.tsx b/apps/blade/src/app/forms/[formName]/_components/form-submitted-success.tsx new file mode 100644 index 000000000..c8d6eb036 --- /dev/null +++ b/apps/blade/src/app/forms/[formName]/_components/form-submitted-success.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { CheckCircle2 } from "lucide-react"; + +import { Card } from "@forge/ui/card"; + +interface SubmissionSuccessCardProps { + userName: string; + formName: string; + showCheckmark: boolean; + showText: boolean; + redirectCountdown: number; +} + +export function SubmissionSuccessCard({ + userName, + formName, + showCheckmark, + showText, + redirectCountdown, +}: SubmissionSuccessCardProps) { + return ( +
    + + {/* Checkmark */} +
    + +
    + + {/* Text */} +
    +

    Thanks, {userName}!

    + +

    + Your response to "{formName}" has been recorded. +

    + +

    + Redirecting in {redirectCountdown}... +

    +
    +
    +
    + ); +} diff --git a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx index 3ed74fa1c..05130f4ac 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx @@ -1,341 +1,156 @@ "use client"; -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { CheckCircle2, Loader2, XCircle } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Loader2 } from "lucide-react"; -import { Button } from "@forge/ui/button"; -import { Card } from "@forge/ui/card"; +import type { FormType } from "@forge/consts/knight-hacks"; -import { QuestionResponseCard } from "~/app/forms/[formName]/_components/question-response-card"; +import type { FormResponsePayload, FormResponseUI } from "./utils"; import { api } from "~/trpc/react"; +import { useSubmissionSuccess } from "../_hooks/useSubmissionSuccess"; +import FormNotFound from "./form-not-found"; +import { FormRunner } from "./form-runner"; +import { SubmissionSuccessCard } from "./form-submitted-success"; +import ResponseNotFound from "./response-not-found"; -interface FormReviewClientProps { +interface FormReviewWrapperProps { formName: string; userName: string; - responseId?: string; + responseId: string; } -export function FormReviewClient({ +export function FormReviewWrapper({ formName, userName, responseId, -}: FormReviewClientProps) { - const router = useRouter(); - const [responses, setResponses] = useState< - Record - >({}); +}: FormReviewWrapperProps) { const [isSubmitted, setIsSubmitted] = useState(false); - const [showCheckmark, setShowCheckmark] = useState(false); - const [showText, setShowText] = useState(false); const [submitError, setSubmitError] = useState(null); - const [redirectCountdown, setRedirectCountdown] = useState(5); - const formQuery = api.forms.getForm.useQuery({ - slug_name: formName, - }); + const { showCheckmark, showText, redirectCountdown } = + useSubmissionSuccess(isSubmitted); - // use responseId to query singular response to view - const responseQuery = api.forms.getUserResponse.useQuery({ - responseId, - }); + const formQuery = api.forms.getForm.useQuery({ slug_name: formName }); + + const responseQuery = api.forms.getUserResponse.useQuery( + { responseId }, + { enabled: !!responseId, staleTime: 0 }, + ); - // TODO: WILL USE FOR EDIT - const submitResponse = api.forms.createResponse.useMutation({ + const editResponse = api.forms.editResponse.useMutation({ onSuccess: () => { setSubmitError(null); setIsSubmitted(true); }, onError: (error) => { setSubmitError( - error.message || "Failed to submit response. Please try again.", + error.message || "Failed to submit response edit. Please try again.", ); }, }); - useEffect(() => { - const data = responseQuery.data ? responseQuery.data[0] : null; - if (!data?.responseData) return; - - const hydrated: Record = - {}; + const form = formQuery.data; + const formData = form?.formData; - for (const [questionText, raw] of Object.entries(data.responseData)) { - if (raw === null) hydrated[questionText] = null; - else if (typeof raw === "string") { - if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) - hydrated[questionText] = new Date(raw); - else if (/^\d{2}:\d{2}$/.test(raw)) - hydrated[questionText] = new Date(`1970-01-01T${raw}`); - else hydrated[questionText] = raw; - } else if (typeof raw === "number") hydrated[questionText] = raw; - else if (Array.isArray(raw) && raw.every((v) => typeof v === "string")) - hydrated[questionText] = raw; - else hydrated[questionText] = null; - } + const stored = (responseQuery.data?.[0]?.responseData ?? + {}) as FormResponsePayload; - setResponses(hydrated); - }, [responseId, responseQuery.data]); + const initialResponses = useMemo(() => { + if (!formData) return {}; + return payloadToUI(stored, formData); + }, [stored, form]); - useEffect(() => { - setResponses({}); - }, [responseId]); - - // Staggered animation for success screen - useEffect(() => { - if (isSubmitted) { - const checkTimer = setTimeout(() => setShowCheckmark(true), 100); - const textTimer = setTimeout(() => setShowText(true), 400); - - // countdown - const countdownInterval = setInterval(() => { - setRedirectCountdown((prev) => prev - 1); - }, 1000); - - const redirectTimer = setTimeout(() => { - router.push("/"); - }, 5000); - - return () => { - clearTimeout(checkTimer); - clearTimeout(textTimer); - clearInterval(countdownInterval); - clearTimeout(redirectTimer); - }; - } - }, [isSubmitted, router]); - - // wait for all queries to load - if ( - formQuery.isLoading || - responseQuery.isLoading || - Object.keys(responses).length === 0 - ) + if (formQuery.isLoading || responseQuery.isLoading) { return (
    ); - - // if form fails to load show error - if (formQuery.error || !formQuery.data) { - return ; } - if (responseQuery.error || !responseQuery.data) { - return ; - } + if (formQuery.error || !formData) return ; + if (responseQuery.error || !responseQuery.data) return ; - const form = formQuery.data.formData; + const zodValidator = form.zodValidator; - // TODO: Implement editing - const allowEdit = false; + const allowEdit = form.allowEdit; - const formDisabled = !allowEdit; - - // SUCESSSSS + // success if (isSubmitted) { return ( -
    - -
    - -
    -
    -

    Thanks, {userName}!

    -

    - Your response to "{form.name}" has been recorded. -

    -

    - Redirecting in {redirectCountdown}... -

    -
    -
    -
    + ); } - const handleResponseChange = ( - questionText: string, - value: string | string[] | number | Date | null, - ) => { - setResponses((prev) => ({ - ...prev, - [questionText]: value, - })); - }; - - // TODO: WILL USE FOR EDIT - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const handleSubmit = () => { - // Build response data object - const responseData: Record = {}; - - form.questions.forEach((question) => { - const response = responses[question.question]; - - // Only include non-empty responses - if (response !== null && response !== undefined && response !== "") { - if (Array.isArray(response) && response.length === 0) { - return; // Skip empty arrays - } - // Convert Date objects to ISO strings - if (response instanceof Date) { - if (question.type === "DATE") { - responseData[question.question] = response - .toISOString() - .split("T")[0]; - } else if (question.type === "TIME") { - responseData[question.question] = response - .toTimeString() - .slice(0, 5); - } - } else { - responseData[question.question] = response; - } - } - }); - - submitResponse.mutate({ - form: formQuery.data.id, - responseData, - }); - }; - - // TODO: WILL USE FOR EDIT - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const isFormValid = () => { - // Check if all required questions have responses - return form.questions.every((question) => { - if (question.optional) return true; // Optional questions don't need validation - - const response = responses[question.question]; - if (response === null || response === undefined || response === "") - return false; - if (Array.isArray(response) && response.length === 0) return false; - return true; + const onSubmit = (payload: FormResponsePayload) => { + editResponse.mutate({ + id: responseId, + responseData: payload, }); }; return ( -
    -
    - {/* Banner */} - {form.banner &&
    } - - {/* Header */} - -
    - {/* Implement View/Edit Title */} - {/*

    {`${allowEdit ? "Edit" : "View"} - ${form.name}`}

    */} -

    {`${"View"} - ${form.name}`}

    - - {form.description && ( -

    {form.description}

    - )} -
    -
    - - {/* Questions */} -
    - {form.questions.map((q, index) => { - const questionText = q.question; - const responseValue: - | string - | string[] - | number - | Date - | null - | undefined = responses[questionText]; - return ( -
    - { - handleResponseChange(questionText, value); - }} - disabled={formDisabled} - /> -
    - ); - })} -
    - - {submitError && ( -
    - {submitError} -
    - )} - - {/* Action Buttons */} -
    - {/* Implement disabling form */} - {/* {!formDisabled && ( - - )} */} - -
    -
    -
    + ); } -function FormNotFound() { - return ( -
    - - -

    Form Not Found

    -

    - This form doesn't exist or may have been removed. -

    -

    - Please let a team member know if you think this is an error. -

    -
    -
    - ); -} +// Form response payload -> UI conversion +function payloadToUI( + payload: FormResponsePayload, + form: FormType, +): FormResponseUI { + const out: FormResponseUI = {}; + + for (const q of form.questions) { + const key = q.question; + const raw = payload[key]; + + if (raw === undefined) continue; + if (raw === null) { + out[key] = null; + continue; + } -function ResponseNotFound() { - return ( -
    - - -

    Response Not Found

    -

    - This response doesn't exist or might not match the current user. -

    -

    - Please let a team member know if you think this is an error. -

    -
    -
    - ); + switch (q.type) { + case "DATE": + out[key] = typeof raw === "string" ? new Date(`${raw}T00:00:00`) : null; + break; + case "TIME": + out[key] = + typeof raw === "string" ? new Date(`1970-01-01T${raw}:00`) : null; + break; + case "BOOLEAN": + out[key] = + typeof raw === "boolean" + ? raw + : raw === "true" + ? true + : raw === "false" + ? false + : null; + break; + default: + out[key] = raw; + break; + } + } + + return out; } diff --git a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx index 36257fc40..5d2a9bd9a 100644 --- a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx @@ -33,8 +33,8 @@ type FormQuestion = z.infer; interface QuestionResponseCardProps { question: FormQuestion; - value?: string | string[] | number | Date | null; - onChange: (value: string | string[] | number | Date | null) => void; + value?: string | string[] | number | Date | boolean | null; + onChange: (value: string | string[] | number | Date | boolean | null) => void; onBlur?: () => void; disabled?: boolean; formId?: string; @@ -101,7 +101,7 @@ function QuestionBody({ formId, }: { question: FormQuestion; - value?: string | string[] | number | Date | null; + value?: string | string[] | number | Date | boolean | null; onChange: (value: string | string[] | number | Date | null) => void; onBlur?: () => void; disabled?: boolean; @@ -752,6 +752,11 @@ function FileUploadInput({ const getUploadUrlMutation = api.forms.getUploadUrl.useMutation(); + // used to sync with responseData for view/edit, otherwise value will be null + React.useEffect(() => { + setFileName(value ? (value.split("/").pop() ?? null) : null); + }, [value]); + const handleFileUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; diff --git a/apps/blade/src/app/forms/[formName]/_components/response-not-found.tsx b/apps/blade/src/app/forms/[formName]/_components/response-not-found.tsx new file mode 100644 index 000000000..8a2b97173 --- /dev/null +++ b/apps/blade/src/app/forms/[formName]/_components/response-not-found.tsx @@ -0,0 +1,20 @@ +import { XCircle } from "lucide-react"; + +import { Card } from "@forge/ui/card"; + +export default function ResponseNotFound() { + return ( +
    + + +

    Response Not Found

    +

    + This response doesn't exist or might not match the current user. +

    +

    + Please let a team member know if you think this is an error. +

    +
    +
    + ); +} diff --git a/apps/blade/src/app/forms/[formName]/_components/utils.ts b/apps/blade/src/app/forms/[formName]/_components/utils.ts new file mode 100644 index 000000000..dfb776742 --- /dev/null +++ b/apps/blade/src/app/forms/[formName]/_components/utils.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; + +import { FormType, QuestionValidatorType } from "@forge/consts/knight-hacks"; + +/** UI state in the client */ +export type FormResponseUI = Partial< + Record +>; + +/** JSON-safe payload what zodValidator will validate */ +export type FormResponsePayload = Partial< + Record +>; + +export const getValidatorResponse = ( + zodValidator: string, + responses: FormResponseUI, + form: FormType, +) => { + // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call + const zodSchema = new Function("z", `return ${zodValidator}`)( + z, + ) as z.ZodSchema; + + const payload = normalizeResponses(responses, form); + + return zodSchema.safeParse(payload); +}; + +// normalized responses needed for zod validation and therefore proc responseData +export const normalizeResponses = ( + responses: FormResponseUI, + form: FormType, +): FormResponsePayload => { + const out: FormResponsePayload = {}; + + for (const q of form.questions) { + const key = q.question; + const v = responses[key]; + + // drop missing/empty values + if (v == null || v === "" || (Array.isArray(v) && v.length === 0)) continue; + + // dates -> strings based on question type + if (v instanceof Date) { + if (q.type === "DATE") out[key] = v.toISOString().slice(0, 10); + else if (q.type === "TIME") out[key] = v.toTimeString().slice(0, 5); + else continue; // unexpected Date for non-date/time question + continue; + } + + // if your UI sometimes passes "true"/"false" strings, normalize them here + if (q.type === "BOOLEAN" && typeof v === "string") { + if (v === "true") out[key] = true; + else if (v === "false") out[key] = false; + else continue; + continue; + } + + out[key] = v; + } + + return out; +}; + +// get specific validator error for question +export const getValidationError = ( + question: QuestionValidatorType, + zodValidator: string, + responses: FormResponseUI, + form: FormType, +) => { + const validatorResponse = getValidatorResponse(zodValidator, responses, form); + if (validatorResponse.success) return null; + + const issue = validatorResponse.error.issues.find((i) => { + const k = i.path[0]; + return typeof k === "string" && k === question.question; + }); + + return issue?.message ?? null; +}; + +export const isFormValid = ( + zodValidator: string, + responses: FormResponseUI, + form: FormType, +) => getValidatorResponse(zodValidator, responses, form).success; diff --git a/apps/blade/src/app/forms/[formName]/_hooks/useSubmissionSuccess.ts b/apps/blade/src/app/forms/[formName]/_hooks/useSubmissionSuccess.ts new file mode 100644 index 000000000..46193c1b8 --- /dev/null +++ b/apps/blade/src/app/forms/[formName]/_hooks/useSubmissionSuccess.ts @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; + +interface UseSubmissionSuccessOptions { + redirectTo?: string; + redirectDelayMs?: number; + checkmarkDelayMs?: number; + textDelayMs?: number; +} + +export function useSubmissionSuccess( + isSubmitted: boolean, + { + redirectTo = "/", + redirectDelayMs = 5000, + checkmarkDelayMs = 100, + textDelayMs = 400, + }: UseSubmissionSuccessOptions = {}, +) { + const router = useRouter(); + + const [showCheckmark, setShowCheckmark] = useState(false); + const [showText, setShowText] = useState(false); + const [redirectCountdown, setRedirectCountdown] = useState( + redirectDelayMs / 1000, + ); + + useEffect(() => { + if (!isSubmitted) { + setShowCheckmark(false); + setShowText(false); + setRedirectCountdown(Math.ceil(redirectDelayMs / 1000)); + return; + } + + const checkTimer = setTimeout( + () => setShowCheckmark(true), + checkmarkDelayMs, + ); + + const textTimer = setTimeout(() => setShowText(true), textDelayMs); + + const countdownInterval = setInterval(() => { + setRedirectCountdown((prev) => Math.max(prev - 1, 0)); + }, 1000); + + const redirectTimer = setTimeout(() => { + router.push(redirectTo); + }, redirectDelayMs); + + return () => { + clearTimeout(checkTimer); + clearTimeout(textTimer); + clearInterval(countdownInterval); + clearTimeout(redirectTimer); + }; + }, [ + isSubmitted, + router, + redirectTo, + redirectDelayMs, + checkmarkDelayMs, + textDelayMs, + ]); + + return { + showCheckmark, + showText, + redirectCountdown, + }; +} diff --git a/apps/blade/src/app/forms/[formName]/page.tsx b/apps/blade/src/app/forms/[formName]/page.tsx index 7c6402848..1964ded62 100644 --- a/apps/blade/src/app/forms/[formName]/page.tsx +++ b/apps/blade/src/app/forms/[formName]/page.tsx @@ -9,7 +9,8 @@ import { Card } from "@forge/ui/card"; import { extractProcedures } from "~/lib/utils"; import { api, HydrateClient } from "~/trpc/server"; -import { FormResponderClient } from "./_components/form-responder-client"; +import FormNotFound from "./_components/form-not-found"; +import { FormResponderWrapper } from "./_components/form-responder-client"; function serializeSearchParams( searchParams: Record, @@ -47,14 +48,7 @@ export default async function FormResponderPage({ } if (!params.formName) { - return ( -
    - - -

    Form not found

    -
    -
    - ); + return ; } // handle url encode form names to allow spacing and special characters @@ -142,7 +136,7 @@ export default async function FormResponderPage({ return ( - { controlPerms.or(["EDIT_FORMS"], ctx); const jsonSchema = generateJsonSchema(input.formData); - console.log(input); const slug_name = input.formData.name.toLowerCase().replaceAll(" ", "-"); @@ -133,6 +132,20 @@ export const formsRouter = { const formId = existingForm.id; + // prevent toggling edit on a form with trpc connections + if (input.allowEdit === true) { + const connection = await db.query.TrpcFormConnection.findFirst({ + where: (t, { eq }) => eq(t.form, formId), + }); + + if (connection) { + throw new TRPCError({ + message: "Cannot add edit for a form with trpc connections", + code: "FORBIDDEN", + }); + } + } + await db .insert(FormsSchemas) .values({ @@ -325,6 +338,18 @@ export const formsRouter = { ) .mutation(async ({ input, ctx }) => { controlPerms.or(["EDIT_FORMS"], ctx); + + const form = await db.query.FormsSchemas.findFirst({ + where: (t, { eq }) => eq(t.id, input.form), + }); + + if (form?.allowEdit) { + throw new TRPCError({ + message: "Cannot add connection to form with allowEdit", + code: "BAD_REQUEST", + }); + } + try { await db.insert(TrpcFormConnection).values({ ...input }); } catch { @@ -476,6 +501,78 @@ export const formsRouter = { }); }), + editResponse: protectedProcedure + .input( + InsertFormResponseSchema.omit({ userId: true, form: true }).extend({ + id: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const userId = ctx.session.user.id; + + // Fetch the existing response to get the form ID + const existingResponse = await db.query.FormResponse.findFirst({ + where: (t, { eq, and }) => + and(eq(t.id, input.id), eq(t.userId, userId)), + }); + + if (!existingResponse) { + throw new TRPCError({ + message: "Response not found or not owned by user", + code: "NOT_FOUND", + }); + } + + // Verify the form allows editing + const form = await db.query.FormsSchemas.findFirst({ + where: (t, { eq }) => eq(t.id, existingResponse.form), + }); + + if (!form?.allowEdit) { + throw new TRPCError({ + message: "This form does not allow editing responses", + code: "FORBIDDEN", + }); + } + + // Validate responseData against form schema + const formData = form.formData as FormType; + const jsonSchema = generateJsonSchema(formData); + + if (!jsonSchema.success) { + throw new TRPCError({ + message: jsonSchema.msg, + code: "BAD_REQUEST", + }); + } + + const zodSchemaString = jsonSchemaToZod(jsonSchema.schema); + // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call + const zodSchema = new Function("z", `return ${zodSchemaString}`)( + z, + ) as z.ZodSchema; + + const validationResult = zodSchema.safeParse(input.responseData); + if (!validationResult.success) { + const errorMessages = validationResult.error.errors.map((err) => { + const path = err.path.join("."); + return path ? `${path}: ${err.message}` : err.message; + }); + throw new TRPCError({ + message: `Form response failed validation: ${errorMessages.join("; ")}`, + code: "BAD_REQUEST", + }); + } + + const updated = await db + .update(FormResponse) + .set({ responseData: input.responseData, editedAt: new Date() }) + .where(eq(FormResponse.id, input.id)) + .returning({ id: FormResponse.id, editedAt: FormResponse.editedAt }); + + return updated[0]; + }), + getResponses: permProcedure .input(z.object({ form: z.string() })) .query(async ({ input, ctx }) => { @@ -533,12 +630,13 @@ export const formsRouter = { if (responseId) { return await db .select({ - submittedAt: FormResponse.createdAt, + submittedAt: FormResponse.editedAt, responseData: FormResponse.responseData, formName: FormsSchemas.name, formSlug: FormsSchemas.slugName, id: FormResponse.id, hasSubmitted: sql`true`, + allowEdit: FormsSchemas.allowEdit, }) .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) @@ -550,40 +648,42 @@ export const formsRouter = { ); } - // return all responses all forms + // return all responses of form const form = input.form; - if (!form) { + if (form) { return await db .select({ - submittedAt: FormResponse.createdAt, + submittedAt: FormResponse.editedAt, responseData: FormResponse.responseData, formName: FormsSchemas.name, formSlug: FormsSchemas.slugName, id: FormResponse.id, hasSubmitted: sql`true`, + allowEdit: FormsSchemas.allowEdit, }) .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) - .where(eq(FormResponse.userId, userId)) - .orderBy(desc(FormResponse.createdAt)); + .where( + and(eq(FormResponse.userId, userId), eq(FormsSchemas.id, form)), + ) + .orderBy(desc(FormResponse.editedAt)); } - // return all responses of form + // return all responses all forms return await db .select({ - submittedAt: FormResponse.createdAt, + submittedAt: FormResponse.editedAt, responseData: FormResponse.responseData, formName: FormsSchemas.name, formSlug: FormsSchemas.slugName, id: FormResponse.id, hasSubmitted: sql`true`, + allowEdit: FormsSchemas.allowEdit, }) .from(FormResponse) .leftJoin(FormsSchemas, eq(FormResponse.form, FormsSchemas.id)) - .where( - and(eq(FormResponse.userId, userId), eq(FormsSchemas.name, form)), - ) - .orderBy(desc(FormResponse.createdAt)); + .where(eq(FormResponse.userId, userId)) + .orderBy(desc(FormResponse.editedAt)); }), // Generate presigned upload URL for direct MinIO upload diff --git a/packages/db/src/schemas/knight-hacks.ts b/packages/db/src/schemas/knight-hacks.ts index 96d07b13e..0d7acf5f6 100644 --- a/packages/db/src/schemas/knight-hacks.ts +++ b/packages/db/src/schemas/knight-hacks.ts @@ -488,6 +488,7 @@ export const FormsSchemas = createTable("form_schemas", (t) => ({ createdAt: t.timestamp().notNull().defaultNow(), duesOnly: t.boolean().notNull().default(false), allowResubmission: t.boolean().notNull().default(false), + allowEdit: t.boolean().notNull().default(false), formData: t.jsonb().notNull(), formValidatorJson: t.jsonb().notNull(), section: t.varchar({ length: 255 }).notNull().default("General"), @@ -528,6 +529,7 @@ export const FormResponse = createTable("form_response", (t) => ({ .references(() => User.id, { onDelete: "cascade" }), responseData: t.jsonb().notNull(), createdAt: t.timestamp().notNull().defaultNow(), + editedAt: t.timestamp().notNull().defaultNow(), })); export const InsertFormResponseSchema = createInsertSchema(FormResponse); From 5c8b8e926be0670bc9b2e69cfbf076d56dded121 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sun, 8 Feb 2026 00:10:32 -0500 Subject: [PATCH 12/23] bump --- apps/blade/src/app/admin/forms/[slug]/client.tsx | 5 ++--- .../[formName]/_components/form-responder-client.tsx | 2 +- .../app/forms/[formName]/_components/form-runner.tsx | 9 +++------ .../[formName]/_components/form-view-edit-client.tsx | 10 +++++----- .../src/app/forms/[formName]/_components/utils.ts | 12 ++++++------ packages/api/src/routers/forms.ts | 5 +++-- packages/consts/src/forms/index.ts | 2 +- 7 files changed, 21 insertions(+), 24 deletions(-) diff --git a/apps/blade/src/app/admin/forms/[slug]/client.tsx b/apps/blade/src/app/admin/forms/[slug]/client.tsx index c1d49938a..1988a4a4b 100644 --- a/apps/blade/src/app/admin/forms/[slug]/client.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/client.tsx @@ -248,15 +248,14 @@ export function EditorClient({ banner: formBanner || undefined, questions: questions.map(({ id: _, ...rest }) => rest), instructions: instructions.map( - ({ id: _, imageUrl: _imageUrl, videoUrl: _videoUrl, ...rest }) => - rest, + ({ id: _, imageUrl: _imageUrl, videoUrl: _videoUrl, ...rest }) => rest ), }, duesOnly, allowResubmission, allowEdit, responseRoleIds, - } as any); + } as unknown as FORMS.FormType); }, [ isLoading, isFetching, diff --git a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx index 5397bfb6a..28a58da66 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx @@ -150,7 +150,7 @@ export function FormResponderWrapper({ return ( ({ + ).map((inst: FORMS.InstructionValidatorType) => ({ ...inst, itemType: "instruction" as const, })); diff --git a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx index 05130f4ac..994e6d138 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx @@ -3,7 +3,7 @@ import { useMemo, useState } from "react"; import { Loader2 } from "lucide-react"; -import type { FormType } from "@forge/consts/knight-hacks"; +import type { FORMS } from "@forge/consts"; import type { FormResponsePayload, FormResponseUI } from "./utils"; import { api } from "~/trpc/react"; @@ -50,7 +50,7 @@ export function FormReviewWrapper({ }); const form = formQuery.data; - const formData = form?.formData; + const formData = form?.formData as FORMS.FormType; const stored = (responseQuery.data?.[0]?.responseData ?? {}) as FormResponsePayload; @@ -58,7 +58,7 @@ export function FormReviewWrapper({ const initialResponses = useMemo(() => { if (!formData) return {}; return payloadToUI(stored, formData); - }, [stored, form]); + }, [formData, stored]); if (formQuery.isLoading || responseQuery.isLoading) { return ( @@ -98,7 +98,7 @@ export function FormReviewWrapper({ return ( UI conversion function payloadToUI( payload: FormResponsePayload, - form: FormType, + form: FORMS.FormType, ): FormResponseUI { const out: FormResponseUI = {}; diff --git a/apps/blade/src/app/forms/[formName]/_components/utils.ts b/apps/blade/src/app/forms/[formName]/_components/utils.ts index dfb776742..72733f928 100644 --- a/apps/blade/src/app/forms/[formName]/_components/utils.ts +++ b/apps/blade/src/app/forms/[formName]/_components/utils.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { FormType, QuestionValidatorType } from "@forge/consts/knight-hacks"; +import type { FORMS } from "@forge/consts"; /** UI state in the client */ export type FormResponseUI = Partial< @@ -15,7 +15,7 @@ export type FormResponsePayload = Partial< export const getValidatorResponse = ( zodValidator: string, responses: FormResponseUI, - form: FormType, + form: FORMS.FormType, ) => { // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call const zodSchema = new Function("z", `return ${zodValidator}`)( @@ -30,7 +30,7 @@ export const getValidatorResponse = ( // normalized responses needed for zod validation and therefore proc responseData export const normalizeResponses = ( responses: FormResponseUI, - form: FormType, + form: FORMS.FormType, ): FormResponsePayload => { const out: FormResponsePayload = {}; @@ -65,10 +65,10 @@ export const normalizeResponses = ( // get specific validator error for question export const getValidationError = ( - question: QuestionValidatorType, + question: FORMS.QuestionValidatorType, zodValidator: string, responses: FormResponseUI, - form: FormType, + form: FORMS.FormType, ) => { const validatorResponse = getValidatorResponse(zodValidator, responses, form); if (validatorResponse.success) return null; @@ -84,5 +84,5 @@ export const getValidationError = ( export const isFormValid = ( zodValidator: string, responses: FormResponseUI, - form: FormType, + form: FORMS.FormType, ) => getValidatorResponse(zodValidator, responses, form).success; diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 4522aa069..9b3012259 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -1,3 +1,4 @@ +import type { TRPCRouterRecord } from "@trpc/server"; import type { JSONSchema7 } from "json-schema"; import { TRPCError } from "@trpc/server"; import { and, count, desc, eq, inArray, lt, sql } from "drizzle-orm"; @@ -536,7 +537,7 @@ export const formsRouter = { } // Validate responseData against form schema - const formData = form.formData as FormType; + const formData = form.formData as FORMS.FormType; const jsonSchema = generateJsonSchema(formData); if (!jsonSchema.success) { @@ -1357,4 +1358,4 @@ export const formsRouter = { return { canEdit: hasSectionRole }; }), -}; +} satisfies TRPCRouterRecord; diff --git a/packages/consts/src/forms/index.ts b/packages/consts/src/forms/index.ts index 7b59f7a31..17ec2e0e0 100644 --- a/packages/consts/src/forms/index.ts +++ b/packages/consts/src/forms/index.ts @@ -187,7 +187,7 @@ export const FormSchemaValidator = z.object({ export type FormType = z.infer; export type InstructionValidatorType = z.infer; -type QuestionValidatorType = z.infer; +export type QuestionValidatorType = z.infer; export type ValidatorOptions = Omit; export type QuestionsType = z.infer["type"]; From e138cf9f20d6fe69b45ff751a17687d1336ea236 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sun, 8 Feb 2026 00:52:34 -0500 Subject: [PATCH 13/23] Revert "Merge remote-tracking branch 'origin/main' into refactor/consts" This reverts commit e94cdb9e01fdf4c1d6b42d80b51095c3449e3662, reversing changes made to 5c8b8e926be0670bc9b2e69cfbf076d56dded121. --- .../src/app/admin/forms/[slug]/client.tsx | 1 - .../_components/form-responder-client.tsx | 46 ------------------- .../_components/form-view-edit-client.tsx | 2 - 3 files changed, 49 deletions(-) diff --git a/apps/blade/src/app/admin/forms/[slug]/client.tsx b/apps/blade/src/app/admin/forms/[slug]/client.tsx index f4dfffec1..1988a4a4b 100644 --- a/apps/blade/src/app/admin/forms/[slug]/client.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/client.tsx @@ -185,7 +185,6 @@ export function EditorClient({ const [duesOnly, setDuesOnly] = useState(false); const [allowResubmission, setAllowResubmission] = useState(true); const [allowEdit, setAllowEdit] = useState(true); - const [allowEdit, setAllowEdit] = useState(true); const [responseRoleIds, setResponseRoleIds] = useState([]); const [responseRolesDialogOpen, setResponseRolesDialogOpen] = useState(false); const [activeItemId, setActiveItemId] = useState(null); diff --git a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx index 44dfc5460..28a58da66 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx @@ -1,7 +1,5 @@ "use client"; -import { useState } from "react"; -import Link from "next/link"; import { useState } from "react"; import Link from "next/link"; import { CheckCircle2, Loader2, XCircle } from "lucide-react"; @@ -10,52 +8,35 @@ import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; -import type { FormResponsePayload } from "./utils"; import type { FormResponsePayload } from "./utils"; import { api } from "~/trpc/react"; import { useSubmissionSuccess } from "../_hooks/useSubmissionSuccess"; import FormNotFound from "./form-not-found"; import { FormRunner } from "./form-runner"; import { SubmissionSuccessCard } from "./form-submitted-success"; -import { useSubmissionSuccess } from "../_hooks/useSubmissionSuccess"; -import FormNotFound from "./form-not-found"; -import { FormRunner } from "./form-runner"; -import { SubmissionSuccessCard } from "./form-submitted-success"; -interface FormResponderWrapperProps { interface FormResponderWrapperProps { formName: string; userName: string; handleCallbacks: (response: Record) => void; } -export function FormResponderWrapper({ export function FormResponderWrapper({ formName, userName, handleCallbacks, -}: FormResponderWrapperProps) { }: FormResponderWrapperProps) { const [isSubmitted, setIsSubmitted] = useState(false); const [submitError, setSubmitError] = useState(null); - const { showCheckmark, showText, redirectCountdown } = - useSubmissionSuccess(isSubmitted); const { showCheckmark, showText, redirectCountdown } = useSubmissionSuccess(isSubmitted); - const formQuery = api.forms.getForm.useQuery({ slug_name: formName }); const formQuery = api.forms.getForm.useQuery({ slug_name: formName }); const duesQuery = api.duesPayment.validatePaidDues.useQuery(); const formIdGate = formQuery.data?.id; - const existingResponseQuery = api.forms.getUserResponse.useQuery( - { form: formIdGate }, - { enabled: !!formIdGate }, - ); - const formIdGate = formQuery.data?.id; - const existingResponseQuery = api.forms.getUserResponse.useQuery( { form: formIdGate }, { enabled: !!formIdGate }, @@ -78,7 +59,6 @@ export function FormResponderWrapper({ formQuery.isLoading || duesQuery.isLoading || existingResponseQuery.isLoading - ) { ) { return (
    @@ -107,9 +87,7 @@ export function FormResponderWrapper({ : (duesQuery.data?.duesPaid ?? false); const hasAlreadySubmitted = (existingResponseQuery.data?.length ?? 0) !== 0; - const hasAlreadySubmitted = (existingResponseQuery.data?.length ?? 0) !== 0; - // dues gate // dues gate if (isDuesOnly && !hasPaidDues) { return ( @@ -125,10 +103,8 @@ export function FormResponderWrapper({ ); } - // already submitted gate // already submitted gate if (hasAlreadySubmitted && !allowResubmission) { - const existing = existingResponseQuery.data?.[0]; const existing = existingResponseQuery.data?.[0]; return (
    @@ -146,20 +122,11 @@ export function FormResponderWrapper({ )} - - {existing && ( - - - - )}
    ); } - // success // success if (isSubmitted) { return ( @@ -173,23 +140,10 @@ export function FormResponderWrapper({ ); } - const onSubmit = (payload: FormResponsePayload) => { - - ); - } - const onSubmit = (payload: FormResponsePayload) => { submitResponse.mutate({ form: formId, responseData: payload, - form: formId, - responseData: payload, }); }; diff --git a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx index 7cf18b541..994e6d138 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx @@ -1,7 +1,5 @@ "use client"; -import { useMemo, useState } from "react"; -import { Loader2 } from "lucide-react"; import { useMemo, useState } from "react"; import { Loader2 } from "lucide-react"; From 4ae61312566d98983abbb916f9e3d44f40365b63 Mon Sep 17 00:00:00 2001 From: DGoel1602 Date: Sun, 8 Feb 2026 12:24:24 -0500 Subject: [PATCH 14/23] fix: lint + typecheck --- .../src/app/admin/forms/[slug]/client.tsx | 21 +++++++------------ .../[formName]/_components/form-runner.tsx | 2 +- .../_components/form-view-edit-client.tsx | 3 +-- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/apps/blade/src/app/admin/forms/[slug]/client.tsx b/apps/blade/src/app/admin/forms/[slug]/client.tsx index 1988a4a4b..1fb57da71 100644 --- a/apps/blade/src/app/admin/forms/[slug]/client.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/client.tsx @@ -197,8 +197,7 @@ export function EditorClient({ error: fetchError, isLoading: isFetching, } = api.forms.getForm.useQuery( - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument - { slug_name: slug } as any, + { slug_name: slug }, { retry: false, refetchOnWindowFocus: false }, ); @@ -212,10 +211,7 @@ export function EditorClient({ }); const handleSaveForm = React.useCallback(() => { - if (isLoading || isFetching || !formTitle) return; - - // Allow mock mode to proceed without real formData - if (!formData && slug !== "test-form") return; + if (isLoading || isFetching || !formTitle || !formData) return; // Check for duplicate question names const questionNames = new Set(); @@ -236,26 +232,23 @@ export function EditorClient({ return; } - /* eslint-disable @typescript-eslint/no-explicit-any */ - /* eslint-disable @typescript-eslint/no-unsafe-argument */ - /* eslint-disable @typescript-eslint/no-unsafe-assignment */ - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ updateFormMutation.mutate({ - id: (formData as any).id, + id: formData.id, formData: { name: formTitle, description: formDescription, banner: formBanner || undefined, questions: questions.map(({ id: _, ...rest }) => rest), instructions: instructions.map( - ({ id: _, imageUrl: _imageUrl, videoUrl: _videoUrl, ...rest }) => rest + ({ id: _, imageUrl: _imageUrl, videoUrl: _videoUrl, ...rest }) => + rest, ), }, duesOnly, allowResubmission, allowEdit, responseRoleIds, - } as unknown as FORMS.FormType); + }); }, [ isLoading, isFetching, @@ -296,7 +289,7 @@ export function EditorClient({ setDuesOnly(formData.duesOnly); setAllowResubmission(formData.allowResubmission); setAllowEdit(formData.allowEdit); - setResponseRoleIds((formData as any).responseRoleIds || []); + setResponseRoleIds(formData.responseRoleIds); const loadedQuestions: UIQuestion[] = ( formData.formData as FORMS.FormType diff --git a/apps/blade/src/app/forms/[formName]/_components/form-runner.tsx b/apps/blade/src/app/forms/[formName]/_components/form-runner.tsx index 3f2f35999..4b5e19fd6 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-runner.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-runner.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; +import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; @@ -9,7 +10,6 @@ import type { FormResponsePayload, FormResponseUI } from "./utils"; import { InstructionResponseCard } from "~/app/forms/[formName]/_components/instruction-response-card"; import { QuestionResponseCard } from "~/app/forms/[formName]/_components/question-response-card"; import { getValidationError, isFormValid, normalizeResponses } from "./utils"; -import type { FORMS } from '@forge/consts'; /** * Shared renderer for "fill out form" and "review/edit response". diff --git a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx index 994e6d138..cf08d34c8 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx @@ -56,7 +56,6 @@ export function FormReviewWrapper({ {}) as FormResponsePayload; const initialResponses = useMemo(() => { - if (!formData) return {}; return payloadToUI(stored, formData); }, [formData, stored]); @@ -68,7 +67,7 @@ export function FormReviewWrapper({ ); } - if (formQuery.error || !formData) return ; + if (formQuery.error || !form) return ; if (responseQuery.error || !responseQuery.data) return ; const zodValidator = form.zodValidator; From 35c224fadaef71067fddf22f948b0bc62810d446 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:09:51 -0500 Subject: [PATCH 15/23] chore(typecheck): ignore issue Ignoring an issue. This should be fine because dhruv is saying it works on a later tsc version. We can upgrade but for now I'm just pushing. --- .../forms/[formName]/_components/form-responder-client.tsx | 3 ++- .../forms/[formName]/_components/form-view-edit-client.tsx | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx index 28a58da66..8bbeec2ee 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx @@ -143,7 +143,8 @@ export function FormResponderWrapper({ const onSubmit = (payload: FormResponsePayload) => { submitResponse.mutate({ form: formId, - responseData: payload, + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + responseData: payload as any, }); }; diff --git a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx index cf08d34c8..4dda3f78f 100644 --- a/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx +++ b/apps/blade/src/app/forms/[formName]/_components/form-view-edit-client.tsx @@ -52,6 +52,7 @@ export function FormReviewWrapper({ const form = formQuery.data; const formData = form?.formData as FORMS.FormType; + // eslint-disable-next-line react-hooks/exhaustive-deps const stored = (responseQuery.data?.[0]?.responseData ?? {}) as FormResponsePayload; @@ -90,7 +91,8 @@ export function FormReviewWrapper({ const onSubmit = (payload: FormResponsePayload) => { editResponse.mutate({ id: responseId, - responseData: payload, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + responseData: payload as any, }); }; From 3b354bb376bb11eb31bc61bd7d065703e0ae3180 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:33:48 -0500 Subject: [PATCH 16/23] feat(consts): move constants to correct spots --- apps/blade/src/app/sponsor/page.tsx | 3 - packages/consts/README.md | 83 ++++++++++++ packages/consts/package.json | 3 +- packages/consts/src/club.ts | 8 ++ .../src/{discord/index.ts => discord.ts} | 54 ++------ packages/consts/src/events.ts | 14 ++ packages/consts/src/forms/index.ts | 122 ++++++++++-------- packages/consts/src/guild.ts | 4 + packages/consts/src/hackathons/index.ts | 6 + .../knight-hacks-8.ts => hackathons/kh8.ts} | 0 packages/consts/src/index.ts | 13 +- packages/consts/src/minio.ts | 30 +++++ .../consts/src/{misc.ts => permissions.ts} | 69 ---------- packages/consts/src/project-launch/index.ts | 1 + .../spring-26.ts} | 0 packages/consts/src/{officers.ts => team.ts} | 37 +++++- pnpm-lock.yaml | 46 ++----- 17 files changed, 279 insertions(+), 214 deletions(-) create mode 100644 packages/consts/src/club.ts rename packages/consts/src/{discord/index.ts => discord.ts} (61%) create mode 100644 packages/consts/src/guild.ts create mode 100644 packages/consts/src/hackathons/index.ts rename packages/consts/src/{discord/knight-hacks-8.ts => hackathons/kh8.ts} (100%) create mode 100644 packages/consts/src/minio.ts rename packages/consts/src/{misc.ts => permissions.ts} (59%) create mode 100644 packages/consts/src/project-launch/index.ts rename packages/consts/src/{discord/project-launch-26.ts => project-launch/spring-26.ts} (100%) rename packages/consts/src/{officers.ts => team.ts} (63%) diff --git a/apps/blade/src/app/sponsor/page.tsx b/apps/blade/src/app/sponsor/page.tsx index ed48e4c50..b6b4f1392 100644 --- a/apps/blade/src/app/sponsor/page.tsx +++ b/apps/blade/src/app/sponsor/page.tsx @@ -9,9 +9,6 @@ export const metadata: Metadata = { description: "Help us make dreams!", }; -// TODO: move to consts -const SPONSOR_VIDEO_LINK_2 = - "https://www.youtube.com/embed/OzW_4QeCfM0?si=G8SUf8UbEo2W5MnL"; const STATS = [ { value: "1,000+", label: "Students" }, diff --git a/packages/consts/README.md b/packages/consts/README.md index d842930e4..1b68a9208 100644 --- a/packages/consts/README.md +++ b/packages/consts/README.md @@ -5,3 +5,86 @@ The `consts` package is, obviously, where we store constant variables. Before yo Some types of data kind of go against this would be, for example, a named Discord channel that we need to remember. Storing it at the top of the file that it is used in makes no sense. Just some food for thought. Note: please do not use `misc.ts`. This is a placeholder until we can migrate the rest. Thanks for understanding. + + +Got it 👍 Here’s a clean, explicit list of **everything Dylan asked MILLION to change**, distilled from the chat and mapped to where things *should* live. + +--- + +## ✅ Requested Changes (by target area) + +### **Minio** + +Move the following into **minio**: + +* KH bucket +* PFP extensions +* Minio endpoint → QR path + +*(Dylan said these should be guild or minio, but explicitly “probably minio”)* + +--- + +### **Forms** + +Move into **forms**: + +* `term_to_date` + *(Reason: only used with graduation date, which is forms-only)* + +--- + +### **Guild** + +Move into **guild**: + +* Guild tag +* Member profile icon + +--- + +### **Club** + +Move into a **club** file: + +* Membership price +* Dues payment +* `clear_dues_message` +* Caution + +--- + +### **Permissions** + +Move into a **permissions** file: + +* Permission object +* Permission index + +--- + +### **Events** + +Move into **events**: + +* Time zone +* Calendar ID +* Personify email +* Weekday order *(this was the only forms/events correction Dylan called out later)* + +--- + +## ❌ Explicitly Not Requested + +* No review of other ~80 files +* Everything else assumed mapped correctly +* Forms/events mostly fine aside from weekday order +* Validation likely moving to `@forge/utils` later, not part of this task + +--- + +If you want, I can: + +* Turn this into a **task checklist** +* Propose an **ideal folder tree** +* Or map each item to **exact filenames** based on your current `consts` layout diff --git a/packages/consts/package.json b/packages/consts/package.json index d702c6ada..a082309ff 100644 --- a/packages/consts/package.json +++ b/packages/consts/package.json @@ -29,6 +29,7 @@ }, "prettier": "@forge/prettier-config", "dependencies": { + "discord-api-types": "^0.37.113", "minimatch": "^10.1.2" } -} +} \ No newline at end of file diff --git a/packages/consts/src/club.ts b/packages/consts/src/club.ts new file mode 100644 index 000000000..b1c707d9d --- /dev/null +++ b/packages/consts/src/club.ts @@ -0,0 +1,8 @@ +export const MEMBERSHIP_PRICE = 2500; + +export const DUES_PAYMENT = 25; + +export const CLEAR_DUES_MESSAGE = + "I am aware of the consequences regarding this action if it were by mistake. I am absolutely sure that I want to clear all dues."; + +export const USE_CAUTION = true; diff --git a/packages/consts/src/discord/index.ts b/packages/consts/src/discord.ts similarity index 61% rename from packages/consts/src/discord/index.ts rename to packages/consts/src/discord.ts index c2a607096..149c15fe5 100644 --- a/packages/consts/src/discord/index.ts +++ b/packages/consts/src/discord.ts @@ -1,10 +1,11 @@ -// TODO: JSDOC all all of the non PROD_ or DEV_ exports -import { IS_PROD } from "../util"; -import * as KNIGHTHACKS_8 from "./knight-hacks-8"; -import * as PROJECT_LAUNCH_26 from "./project-launch-26"; +import { + GuildScheduledEventEntityType, + GuildScheduledEventPrivacyLevel, +} from "discord-api-types/v10"; + +import { IS_PROD } from "./util"; -export { KNIGHTHACKS_8 }; -export { PROJECT_LAUNCH_26 }; +// TODO: JSDOC all all of the non PROD_ or DEV_ exports export const PROD_OFFICER_ROLE = "486629374758748180"; export const DEV_OFFICER_ROLE = "1246637685011906560"; @@ -45,42 +46,5 @@ export const RECRUITING_CHANNEL = IS_PROD export const PERMANENT_INVITE = "https://discord.com/invite/Kv5g9vf"; -export const DISCORD_EVENT_TYPE = 3; -export const DISCORD_EVENT_PRIVACY_LEVEL = 2; - -export const TEAMS = [ - { - team: "Outreach", - color: "#88fea1", - director_role: "779845137822908436", - }, - { - team: "Design", - color: "#eaacff", - director_role: "874028482089349172", - }, - { - team: "Development", - color: "#93ceff", - director_role: "1082124530077683772", - }, - { - team: "Sponsorship", - color: "#f5f4af", - director_role: "626815399442513920", - }, - { - team: "Workshops", - color: "#206694", - director_role: "757002949603098837", - }, - { - team: "Projects/Mentorship", - color: "#3498db", - director_role: "1244790444626280550", - }, -]; - -export const ALLOWED_FORM_ASSIGNABLE_DISC_ROLES = [ - PROJECT_LAUNCH_26.MEMBER_ROLE, -]; +export const EVENT_TYPE = GuildScheduledEventEntityType.External; +export const EVENT_PRIVACY_LEVEL = GuildScheduledEventPrivacyLevel.GuildOnly; diff --git a/packages/consts/src/events.ts b/packages/consts/src/events.ts index ae8ce158c..170e4f8a9 100644 --- a/packages/consts/src/events.ts +++ b/packages/consts/src/events.ts @@ -1,3 +1,5 @@ +import { IS_PROD } from "./util"; + export const EVENT_TAGS = [ "GBM", "Social", @@ -68,3 +70,15 @@ export const EVENT_FEEDBACK_SLIDER_STEP = 1; export const EVENT_FEEDBACK_SLIDER_VALUE = 5; export const EVENT_FEEDBACK_TEXT_ROWS = 4; export const EVENT_FEEDBACK_POINTS_INCREMENT = 10; + +export const CALENDAR_TIME_ZONE = "America/New_York"; + +export const PROD_GOOGLE_CALENDAR_ID = + "c_0b9df2b0062a5d711fc16060ff3286ef404b174bfafc4cbdd4e3009e91536e94@group.calendar.google.com"; +export const DEV_GOOGLE_CALENDAR_ID = + "c_178118a9a25d9f278014b123b79b7e9caf9b29ac94bba3934237db00979e0f75@group.calendar.google.com"; +export const GOOGLE_CALENDAR_ID = IS_PROD + ? PROD_GOOGLE_CALENDAR_ID + : DEV_GOOGLE_CALENDAR_ID; + +export const GOOGLE_PERSONIFY_EMAIL = "dylan@knighthacks.org"; diff --git a/packages/consts/src/forms/index.ts b/packages/consts/src/forms/index.ts index 17ec2e0e0..f97017b00 100644 --- a/packages/consts/src/forms/index.ts +++ b/packages/consts/src/forms/index.ts @@ -1,5 +1,7 @@ import * as z from "zod"; +import { PROJECT_LAUNCH_26 } from "../project-launch"; +// These are elsewhere to reduce file length... import { COMPANIES } from "./companies"; import { COUNTRIES } from "./countries"; import { SCHOOLS } from "./schools"; @@ -138,60 +140,6 @@ export const SHORT_RACES_AND_ETHNICITIES = [ "Native American/Alaskan Native", ] as const; -export const QuestionValidator = z.object({ - question: z.string(), - image: z.string().url().optional(), - type: z.enum([ - "SHORT_ANSWER", - "PARAGRAPH", - "MULTIPLE_CHOICE", - "CHECKBOXES", - "DROPDOWN", - "LINEAR_SCALE", - "DATE", - "TIME", - "EMAIL", - "NUMBER", - "PHONE", - "FILE_UPLOAD", - "BOOLEAN", - "LINK", - ]), - options: z.array(z.string()).optional(), - optionsConst: z.string().optional(), - optional: z.boolean().optional(), - allowOther: z.boolean().optional(), - min: z.number().optional(), - max: z.number().optional(), - order: z.number().optional(), -}); - -export const InstructionValidator = z.object({ - title: z.string().max(200), - content: z.string().max(2000).optional(), - imageUrl: z.string().url().optional(), - videoUrl: z.string().url().optional(), - imageObjectName: z.string().optional(), - videoObjectName: z.string().optional(), - order: z.number().optional(), -}); - -export const FormSchemaValidator = z.object({ - banner: z.string().url().optional(), - name: z.string().max(200), - description: z.string().max(500), - questions: z.array(QuestionValidator), - instructions: z.array(InstructionValidator).optional(), -}); - -export type FormType = z.infer; -export type InstructionValidatorType = z.infer; - -export type QuestionValidatorType = z.infer; -export type ValidatorOptions = Omit; - -export type QuestionsType = z.infer["type"]; - export const AVAILABLE_DROPDOWN_CONSTANTS = { LEVELS_OF_STUDY: "Levels of Study", ALLERGIES: "Allergies", @@ -223,6 +171,14 @@ export const EVENT_FEEDBACK_HEARD = [ "From Another Club", ] as const; +export const TERM_TO_DATE = { + Spring: { month: 4, day: 2 }, // May 2 + Summer: { month: 7, day: 6 }, // Aug 6 + Fall: { month: 11, day: 10 }, // Dec 10 +} as const; + +export type GradTerm = keyof typeof TERM_TO_DATE; + export function getDropdownOptionsFromConst( constName: string, ): readonly string[] { @@ -341,4 +297,62 @@ export interface Semester { endDate: Date; } +export const ALLOWED_ASSIGNABLE_DISCORD_ROLES = [PROJECT_LAUNCH_26.MEMBER_ROLE]; + +// TODO: decide where these go + export const DEVPOST_TEAM_MEMBER_EMAIL_OFFSET = 3; + +export const QuestionValidator = z.object({ + question: z.string(), + image: z.string().url().optional(), + type: z.enum([ + "SHORT_ANSWER", + "PARAGRAPH", + "MULTIPLE_CHOICE", + "CHECKBOXES", + "DROPDOWN", + "LINEAR_SCALE", + "DATE", + "TIME", + "EMAIL", + "NUMBER", + "PHONE", + "FILE_UPLOAD", + "BOOLEAN", + "LINK", + ]), + options: z.array(z.string()).optional(), + optionsConst: z.string().optional(), + optional: z.boolean().optional(), + allowOther: z.boolean().optional(), + min: z.number().optional(), + max: z.number().optional(), + order: z.number().optional(), +}); + +export const InstructionValidator = z.object({ + title: z.string().max(200), + content: z.string().max(2000).optional(), + imageUrl: z.string().url().optional(), + videoUrl: z.string().url().optional(), + imageObjectName: z.string().optional(), + videoObjectName: z.string().optional(), + order: z.number().optional(), +}); + +export const FormSchemaValidator = z.object({ + banner: z.string().url().optional(), + name: z.string().max(200), + description: z.string().max(500), + questions: z.array(QuestionValidator), + instructions: z.array(InstructionValidator).optional(), +}); + +export type FormType = z.infer; +export type InstructionValidatorType = z.infer; + +export type QuestionValidatorType = z.infer; +export type ValidatorOptions = Omit; + +export type QuestionsType = z.infer["type"]; diff --git a/packages/consts/src/guild.ts b/packages/consts/src/guild.ts new file mode 100644 index 000000000..0c95f29c9 --- /dev/null +++ b/packages/consts/src/guild.ts @@ -0,0 +1,4 @@ +export const GUILD_TAG_OPTIONS = ["alumni", "current"] as const; +export type GuildTag = (typeof GUILD_TAG_OPTIONS)[number]; + +export const MEMBER_PROFILE_ICON_SIZE = 24; diff --git a/packages/consts/src/hackathons/index.ts b/packages/consts/src/hackathons/index.ts new file mode 100644 index 000000000..74a3a24e6 --- /dev/null +++ b/packages/consts/src/hackathons/index.ts @@ -0,0 +1,6 @@ +export * as KNIGHT_HACKS_8 from "./kh8"; + +export const SPONSOR_VIDEO_LINK = + "https://www.youtube.com/embed/OU1q02v1Vrw?si=dyHSQCmxzcau7-mF"; +export const SPONSOR_VIDEO_LINK_2 = + "https://www.youtube.com/embed/OzW_4QeCfM0?si=G8SUf8UbEo2W5MnL"; diff --git a/packages/consts/src/discord/knight-hacks-8.ts b/packages/consts/src/hackathons/kh8.ts similarity index 100% rename from packages/consts/src/discord/knight-hacks-8.ts rename to packages/consts/src/hackathons/kh8.ts diff --git a/packages/consts/src/index.ts b/packages/consts/src/index.ts index c058a01b6..8b20ceec0 100644 --- a/packages/consts/src/index.ts +++ b/packages/consts/src/index.ts @@ -1,7 +1,10 @@ -export * as DISCORD from "./discord"; export * as FORMS from "./forms"; +export * as HACKATHONS from "./hackathons"; +export * as PROJECT_LAUNCH from "./project-launch"; +export * as CLUB from "./club"; +export * as DISCORD from "./discord"; export * as EVENTS from "./events"; -export * as OFFICERS from "./officers"; - -// TODO: get rid of misc -export * from "./misc"; +export * as GUILD from "./guild"; +export * as MINIO from "./minio"; +export * as PERMISSIONS from "./permissions"; +export * as TEAM from "./team"; diff --git a/packages/consts/src/minio.ts b/packages/consts/src/minio.ts new file mode 100644 index 000000000..16c99846a --- /dev/null +++ b/packages/consts/src/minio.ts @@ -0,0 +1,30 @@ +export const ENDPOINT = "minio-g0soogg4gs8gwcggw4ococok.knighthacks.org"; + +export const QR_CONTENT_TYPE = "image/png"; +export const QR_PATHNAME = "/knight-hacks-qr/**"; + +export const BUCKET_NAME = "knight-hacks-qr"; +export const BUCKET_REGION = "us-east-1"; +// TODO: check if this should be MB or MiB +export const MAX_RESUME_SIZE = 5 * 1000000; // 5MB + +export const FORM_ASSETS_BUCKET = "form-assets"; + +export const PRESIGNED_URL_EXPIRY = 7 * 24 * 60 * 60; // 7 days + +// TODO: see above +export const KNIGHTHACKS_MAX_PROFILE_PICTURE_SIZE = 2 * 1024 * 1024; // 2MB +export const ALLOWED_PROFILE_PICTURE_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", +]; + +export const ALLOWED_PROFILE_PICTURE_EXTENSIONS = [ + "jpg", + "jpeg", + "png", + "gif", + "webp", +]; diff --git a/packages/consts/src/misc.ts b/packages/consts/src/permissions.ts similarity index 59% rename from packages/consts/src/misc.ts rename to packages/consts/src/permissions.ts index f728f46f1..747dc7f0c 100644 --- a/packages/consts/src/misc.ts +++ b/packages/consts/src/permissions.ts @@ -1,48 +1,3 @@ -// This file should not be added to. I have no idea where these belong, hence -// the miscellaneous category. Please DO NOT add any more constants to this -// file. - -import { IS_PROD } from "./util"; - -export const KNIGHTHACKS_S3_BUCKET_REGION = "us-east-1"; -export const KNIGHTHACKS_MAX_RESUME_SIZE = 5 * 1000000; // 5MB -export const FORM_ASSETS_BUCKET = "form-assets"; - -export const PRESIGNED_URL_EXPIRY = 7 * 24 * 60 * 60; // 7 days - -export const KNIGHTHACKS_MAX_PROFILE_PICTURE_SIZE = 2 * 1024 * 1024; // 2MB -export const ALLOWED_PROFILE_PICTURE_TYPES = [ - "image/jpeg", - "image/png", - "image/gif", - "image/webp", -]; - -export const ALLOWED_PROFILE_PICTURE_EXTENSIONS = [ - "jpg", - "jpeg", - "png", - "gif", - "webp", -]; - -export const TERM_TO_DATE = { - Spring: { month: 4, day: 2 }, // May 2 - Summer: { month: 7, day: 6 }, // Aug 6 - Fall: { month: 11, day: 10 }, // Dec 10 -} as const; -export type GradTerm = keyof typeof TERM_TO_DATE; - -export const GUILD_TAG_OPTIONS = ["alumni", "current"] as const; -export type GuildTag = (typeof GUILD_TAG_OPTIONS)[number]; - -export const MINIO_ENDPOINT = "minio-g0soogg4gs8gwcggw4ococok.knighthacks.org"; -export const BUCKET_NAME = "knight-hacks-qr"; -export const QR_CONTENT_TYPE = "image/png"; -export const QR_PATHNAME = "/knight-hacks-qr/**"; - -export const KNIGHTHACKS_MEMBERSHIP_PRICE = 2500; - export interface PermissionDataObj { idx: number; name: string; @@ -160,27 +115,3 @@ export const PERMISSIONS = Object.fromEntries( export type PermissionKey = keyof typeof PERMISSION_DATA; export type PermissionIndex = (typeof PERMISSION_DATA)[PermissionKey]["idx"]; - -export const MEMBER_PROFILE_ICON_SIZE = 24; - -export const SPONSOR_VIDEO_LINK = - "https://www.youtube.com/embed/OU1q02v1Vrw?si=dyHSQCmxzcau7-mF"; - -export const CALENDAR_TIME_ZONE = "America/New_York"; - -export const DUES_PAYMENT = 25; - -export const CLEAR_DUES_MESSAGE = - "I am aware of the consequences regarding this action if it were by mistake. I am absolutely sure that I want to clear all dues."; - -export const PROD_GOOGLE_CALENDAR_ID = - "c_0b9df2b0062a5d711fc16060ff3286ef404b174bfafc4cbdd4e3009e91536e94@group.calendar.google.com"; -export const DEV_GOOGLE_CALENDAR_ID = - "c_178118a9a25d9f278014b123b79b7e9caf9b29ac94bba3934237db00979e0f75@group.calendar.google.com"; -export const GOOGLE_CALENDAR_ID = IS_PROD - ? PROD_GOOGLE_CALENDAR_ID - : DEV_GOOGLE_CALENDAR_ID; - -export const GOOGLE_PERSONIFY_EMAIL = "dylan@knighthacks.org"; - -export const USE_CAUTION = true; diff --git a/packages/consts/src/project-launch/index.ts b/packages/consts/src/project-launch/index.ts new file mode 100644 index 000000000..c29dc5187 --- /dev/null +++ b/packages/consts/src/project-launch/index.ts @@ -0,0 +1 @@ +export * as PROJECT_LAUNCH_26 from "./spring-26"; diff --git a/packages/consts/src/discord/project-launch-26.ts b/packages/consts/src/project-launch/spring-26.ts similarity index 100% rename from packages/consts/src/discord/project-launch-26.ts rename to packages/consts/src/project-launch/spring-26.ts diff --git a/packages/consts/src/officers.ts b/packages/consts/src/team.ts similarity index 63% rename from packages/consts/src/officers.ts rename to packages/consts/src/team.ts index 072d426a5..ccd13c295 100644 --- a/packages/consts/src/officers.ts +++ b/packages/consts/src/team.ts @@ -1,4 +1,4 @@ -export const LIST = [ +export const OFFICERS = [ { name: "Dylan Vidal", position: "President", @@ -43,4 +43,37 @@ export const LIST = [ }, ] as const; -export type Officer = (typeof LIST)[number]; +export type Officer = (typeof OFFICERS)[number]; + +export const TEAMS = [ + { + team: "Outreach", + color: "#88fea1", + director_role: "779845137822908436", + }, + { + team: "Design", + color: "#eaacff", + director_role: "874028482089349172", + }, + { + team: "Development", + color: "#93ceff", + director_role: "1082124530077683772", + }, + { + team: "Sponsorship", + color: "#f5f4af", + director_role: "626815399442513920", + }, + { + team: "Workshops", + color: "#206694", + director_role: "757002949603098837", + }, + { + team: "Projects/Mentorship", + color: "#3498db", + director_role: "1244790444626280550", + }, +]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f3522796..931baa784 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,7 +240,7 @@ importers: version: 6.6.0 geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) google-auth-library: specifier: ^9.15.0 version: 9.15.1 @@ -355,7 +355,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -401,7 +401,7 @@ importers: version: 7.4.4 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) prettier: specifier: 'catalog:' version: 3.8.1 @@ -528,7 +528,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -619,7 +619,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -885,6 +885,9 @@ importers: packages/consts: dependencies: + discord-api-types: + specifier: ^0.37.113 + version: 0.37.120 minimatch: specifier: ^10.1.2 version: 10.1.2 @@ -1189,7 +1192,7 @@ importers: version: 14.2.35 eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: specifier: ^6.10.0 version: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -1269,7 +1272,7 @@ importers: version: link:../typescript eslint: specifier: 'catalog:' - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' version: 3.8.1 @@ -13466,33 +13469,6 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.39.2(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -13834,7 +13810,7 @@ snapshots: - encoding - supports-color - geist@1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + geist@1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: next: 14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) From 19a8db68c8924bcf9b9acbe1577183ba2fb3170c Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:06:28 -0500 Subject: [PATCH 17/23] chore(consts): add bucket names to minio.ts --- packages/consts/src/minio.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/consts/src/minio.ts b/packages/consts/src/minio.ts index 16c99846a..8a906cab0 100644 --- a/packages/consts/src/minio.ts +++ b/packages/consts/src/minio.ts @@ -1,14 +1,17 @@ export const ENDPOINT = "minio-g0soogg4gs8gwcggw4ococok.knighthacks.org"; +export const BUCKET_REGION = "us-east-1"; + export const QR_CONTENT_TYPE = "image/png"; export const QR_PATHNAME = "/knight-hacks-qr/**"; +export const QR_BUCKET_NAME = "knight-hacks-qr"; + +export const PROFILE_PICTURES_BUCKET_NAME = "guild-profile-pictures"; +export const FORM_ASSETS_BUCKET_NAME = "form-assets"; -export const BUCKET_NAME = "knight-hacks-qr"; -export const BUCKET_REGION = "us-east-1"; // TODO: check if this should be MB or MiB export const MAX_RESUME_SIZE = 5 * 1000000; // 5MB -export const FORM_ASSETS_BUCKET = "form-assets"; export const PRESIGNED_URL_EXPIRY = 7 * 24 * 60 * 60; // 7 days From 5c7e1995b832eba522e12a219c5fb20bb79119b5 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:07:01 -0500 Subject: [PATCH 18/23] chore(db): refactor to consts changes --- packages/db/scripts/seed_devdb.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/db/scripts/seed_devdb.ts b/packages/db/scripts/seed_devdb.ts index 08ccbd909..ae8126bac 100644 --- a/packages/db/scripts/seed_devdb.ts +++ b/packages/db/scripts/seed_devdb.ts @@ -15,7 +15,7 @@ import { drizzle } from "drizzle-orm/node-postgres"; import Pool from "pg-pool"; import { stringify } from "superjson"; -import { DISCORD, KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts"; +import { DISCORD, MINIO } from "@forge/consts"; import { minioClient } from "../../api/src/minio/minio-client"; import { discord, log } from "../../api/src/utils"; @@ -339,7 +339,7 @@ async function minio() { try { const bucketExists = await minioClient.bucketExists(BUCKET_NAME); if (!bucketExists) { - await minioClient.makeBucket(BUCKET_NAME, KNIGHTHACKS_S3_BUCKET_REGION); + await minioClient.makeBucket(BUCKET_NAME, MINIO.BUCKET_REGION); } await minioClient.fPutObject(BUCKET_NAME, filePath, filePath, { From f0649d95305eece5c01cb43b490f2eeca76405f1 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:07:34 -0500 Subject: [PATCH 19/23] chore(api): refactor to consts changes --- packages/api/src/routers/dues-payment.ts | 4 +-- packages/api/src/routers/event.ts | 31 +++++++++------------ packages/api/src/routers/forms.ts | 30 ++++++++++---------- packages/api/src/routers/guild.ts | 18 ++++++------ packages/api/src/routers/hacker.ts | 26 ++++++++---------- packages/api/src/routers/member.ts | 19 ++++++------- packages/api/src/routers/misc.ts | 6 ++-- packages/api/src/routers/qr.ts | 16 ++++++----- packages/api/src/routers/resume.ts | 4 +-- packages/api/src/routers/roles.ts | 5 ++-- packages/api/src/trpc.ts | 5 ++-- packages/api/src/utils.ts | 35 +++++++++--------------- 12 files changed, 88 insertions(+), 111 deletions(-) diff --git a/packages/api/src/routers/dues-payment.ts b/packages/api/src/routers/dues-payment.ts index 98b67fa88..081c692bd 100644 --- a/packages/api/src/routers/dues-payment.ts +++ b/packages/api/src/routers/dues-payment.ts @@ -3,7 +3,7 @@ import { TRPCError } from "@trpc/server"; import Stripe from "stripe"; import { z } from "zod"; -import { KNIGHTHACKS_MEMBERSHIP_PRICE } from "@forge/consts"; +import { CLUB } from "@forge/consts"; import { eq } from "@forge/db"; import { db } from "@forge/db/client"; import { DuesPayment, Member } from "@forge/db/schemas/knight-hacks"; @@ -19,7 +19,7 @@ export const duesPaymentRouter = { ? "http://localhost:3000" : "https://blade.knighthacks.org"; - const price = KNIGHTHACKS_MEMBERSHIP_PRICE as number; + const price = CLUB.MEMBERSHIP_PRICE as number; const member = await db .select() diff --git a/packages/api/src/routers/event.ts b/packages/api/src/routers/event.ts index c273e9ec5..f852716bd 100644 --- a/packages/api/src/routers/event.ts +++ b/packages/api/src/routers/event.ts @@ -5,12 +5,7 @@ import { TRPCError } from "@trpc/server"; import { Routes } from "discord-api-types/v10"; import { z } from "zod"; -import { - CALENDAR_TIME_ZONE, - DISCORD, - EVENTS, - GOOGLE_CALENDAR_ID, -} from "@forge/consts"; +import { DISCORD, EVENTS } from "@forge/consts"; import { count, desc, eq, getTableColumns } from "@forge/db"; import { db } from "@forge/db/client"; import { @@ -129,10 +124,10 @@ export const eventRouter = { body: { description: hackDesc + input.description + pointDesc, name: formattedName, - privacy_level: DISCORD.DISCORD_EVENT_PRIVACY_LEVEL, + privacy_level: DISCORD.EVENT_PRIVACY_LEVEL, scheduled_start_time: startLocalIso, // Use ISO for Discord scheduled_end_time: endLocalIso, // Use ISO for Discord - entity_type: DISCORD.DISCORD_EVENT_TYPE, + entity_type: DISCORD.EVENT_TYPE, entity_metadata: { location: input.location, }, @@ -152,15 +147,15 @@ export const eventRouter = { let googleEventId: string | undefined; try { const response = await calendar.events.insert({ - calendarId: GOOGLE_CALENDAR_ID, + calendarId: EVENTS.GOOGLE_CALENDAR_ID, requestBody: { end: { dateTime: endLocalIso, // ISO for Google Calendar - timeZone: CALENDAR_TIME_ZONE, + timeZone: EVENTS.CALENDAR_TIME_ZONE, }, start: { dateTime: startLocalIso, // ISO for Google Calendar - timeZone: CALENDAR_TIME_ZONE, + timeZone: EVENTS.CALENDAR_TIME_ZONE, }, description: input.description, summary: formattedName, @@ -238,7 +233,7 @@ export const eventRouter = { // Clean up the event in Google Calendar if the database insert fails try { await calendar.events.delete({ - calendarId: GOOGLE_CALENDAR_ID, + calendarId: EVENTS.GOOGLE_CALENDAR_ID, eventId: googleEventId, }); } catch (cleanupErr) { @@ -326,10 +321,10 @@ export const eventRouter = { body: { description: hackDesc + input.description + pointDesc, name: formattedName, - privacy_level: DISCORD.DISCORD_EVENT_PRIVACY_LEVEL, + privacy_level: DISCORD.EVENT_PRIVACY_LEVEL, scheduled_start_time: startLocalIso, scheduled_end_time: endLocalIso, - entity_type: DISCORD.DISCORD_EVENT_TYPE, + entity_type: DISCORD.EVENT_TYPE, entity_metadata: { location: input.location, }, @@ -347,16 +342,16 @@ export const eventRouter = { // Step 2: Update the event in Google Calendar try { await calendar.events.update({ - calendarId: GOOGLE_CALENDAR_ID, + calendarId: EVENTS.GOOGLE_CALENDAR_ID, eventId: input.googleId, requestBody: { end: { dateTime: endLocalIso, - timeZone: CALENDAR_TIME_ZONE, + timeZone: EVENTS.CALENDAR_TIME_ZONE, }, start: { dateTime: startLocalIso, - timeZone: CALENDAR_TIME_ZONE, + timeZone: EVENTS.CALENDAR_TIME_ZONE, }, description: input.description, summary: formattedName, @@ -498,7 +493,7 @@ export const eventRouter = { // Step 2: Delete the event in the Google Calendar try { await calendar.events.delete({ - calendarId: GOOGLE_CALENDAR_ID, + calendarId: EVENTS.GOOGLE_CALENDAR_ID, eventId: input.googleId, } as calendar_v3.Params$Resource$Events$Delete); } catch (error) { diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 9b3012259..1278d8f1d 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -5,12 +5,7 @@ import { and, count, desc, eq, inArray, lt, sql } from "drizzle-orm"; import jsonSchemaToZod from "json-schema-to-zod"; import * as z from "zod"; -import { - FORM_ASSETS_BUCKET, - FORMS, - KNIGHTHACKS_S3_BUCKET_REGION, - PRESIGNED_URL_EXPIRY, -} from "@forge/consts"; +import { FORMS, MINIO } from "@forge/consts"; import { db } from "@forge/db/client"; import { Permissions, Roles } from "@forge/db/schemas/auth"; import { @@ -721,26 +716,28 @@ export const formsRouter = { try { // Ensure bucket exists - const bucketExists = await minioClient.bucketExists(FORM_ASSETS_BUCKET); + const bucketExists = await minioClient.bucketExists( + MINIO.FORM_ASSETS_BUCKET_NAME, + ); if (!bucketExists) { await minioClient.makeBucket( - FORM_ASSETS_BUCKET, - KNIGHTHACKS_S3_BUCKET_REGION, + MINIO.FORM_ASSETS_BUCKET_NAME, + MINIO.BUCKET_REGION, ); } // Generate presigned PUT URL for upload (15 minutes to complete upload) const uploadUrl = await minioClient.presignedPutObject( - FORM_ASSETS_BUCKET, + MINIO.FORM_ASSETS_BUCKET_NAME, objectName, 15 * 60, // 15 minutes ); // Generate presigned GET URL for immediate preview const viewUrl = await minioClient.presignedGetObject( - FORM_ASSETS_BUCKET, + MINIO.FORM_ASSETS_BUCKET_NAME, objectName, - PRESIGNED_URL_EXPIRY, + MINIO.PRESIGNED_URL_EXPIRY, ); return { uploadUrl, objectName, viewUrl }; @@ -764,7 +761,10 @@ export const formsRouter = { const { objectName } = input; try { - await minioClient.removeObject(FORM_ASSETS_BUCKET, objectName); + await minioClient.removeObject( + MINIO.FORM_ASSETS_BUCKET_NAME, + objectName, + ); return { success: true }; } catch (e) { console.error("deleteMedia error:", e); @@ -787,9 +787,9 @@ export const formsRouter = { try { const viewUrl = await minioClient.presignedGetObject( - FORM_ASSETS_BUCKET, + MINIO.FORM_ASSETS_BUCKET_NAME, objectName, - PRESIGNED_URL_EXPIRY, + MINIO.PRESIGNED_URL_EXPIRY, ); return { viewUrl }; } catch (e) { diff --git a/packages/api/src/routers/guild.ts b/packages/api/src/routers/guild.ts index 23eb56968..d41a10d30 100644 --- a/packages/api/src/routers/guild.ts +++ b/packages/api/src/routers/guild.ts @@ -4,7 +4,7 @@ import { TRPCError } from "@trpc/server"; import { Client } from "minio"; import { z } from "zod"; -import { KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts"; +import { MINIO } from "@forge/consts"; import { and, count, sql } from "@forge/db"; import { db } from "@forge/db/client"; import { Member } from "@forge/db/schemas/knight-hacks"; @@ -13,8 +13,6 @@ import { env } from "../env"; import { minioClient } from "../minio/minio-client"; import { protectedProcedure, publicProcedure } from "../trpc"; -const GUILD_PROFILE_PICTURES_BUCKET_NAME = "guild-profile-pictures"; - const s3Client = new Client({ endPoint: env.MINIO_ENDPOINT, useSSL: true, @@ -58,12 +56,12 @@ export const guildRouter = { try { const bucketExists = await minioClient.bucketExists( - GUILD_PROFILE_PICTURES_BUCKET_NAME, + MINIO.PROFILE_PICTURES_BUCKET_NAME, ); if (!bucketExists) { await minioClient.makeBucket( - GUILD_PROFILE_PICTURES_BUCKET_NAME, - KNIGHTHACKS_S3_BUCKET_REGION, + MINIO.PROFILE_PICTURES_BUCKET_NAME, + MINIO.BUCKET_REGION, ); } } catch (e) { @@ -80,7 +78,7 @@ export const guildRouter = { const existingObjects: string[] = []; try { const stream = minioClient.listObjects( - GUILD_PROFILE_PICTURES_BUCKET_NAME, + MINIO.PROFILE_PICTURES_BUCKET_NAME, userDirectory, true, ) as AsyncIterable; @@ -99,7 +97,7 @@ export const guildRouter = { if (existingObjects.length > 0) { try { await minioClient.removeObjects( - GUILD_PROFILE_PICTURES_BUCKET_NAME, + MINIO.PROFILE_PICTURES_BUCKET_NAME, existingObjects, ); } catch (e) { @@ -116,7 +114,7 @@ export const guildRouter = { try { await minioClient.putObject( - GUILD_PROFILE_PICTURES_BUCKET_NAME, + MINIO.PROFILE_PICTURES_BUCKET_NAME, objectName, fileBuffer, fileBuffer.length, @@ -133,7 +131,7 @@ export const guildRouter = { }); } - const publicUrl = `https://${env.MINIO_ENDPOINT}/${GUILD_PROFILE_PICTURES_BUCKET_NAME}/${objectName}`; + const publicUrl = `https://${env.MINIO_ENDPOINT}/${MINIO.PROFILE_PICTURES_BUCKET_NAME}/${objectName}`; return { profilePictureUrl: publicUrl }; }), diff --git a/packages/api/src/routers/hacker.ts b/packages/api/src/routers/hacker.ts index e085ea2b0..a51f14b7c 100644 --- a/packages/api/src/routers/hacker.ts +++ b/packages/api/src/routers/hacker.ts @@ -4,12 +4,7 @@ import QRCode from "qrcode"; import { z } from "zod"; import type { HackerClass } from "@forge/db/schemas/knight-hacks"; -import { - BUCKET_NAME, - DISCORD, - FORMS, - KNIGHTHACKS_S3_BUCKET_REGION, -} from "@forge/consts"; +import { FORMS, HACKATHONS, MINIO } from "@forge/consts"; import { and, count, desc, eq, gt, or, sql, sum } from "@forge/db"; import { db } from "@forge/db/client"; import { Session } from "@forge/db/schemas/auth"; @@ -23,7 +18,6 @@ import { InsertHackerSchema, } from "@forge/db/schemas/knight-hacks"; -import type { AssignableHackerClass } from "../../../consts/src/discord/knight-hacks-8"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; import { @@ -465,17 +459,19 @@ export const hackerRouter = { if (existingHackerProfile.length === 0) { const objectName = `qr-code-${userId}.png`; - const bucketExists = await minioClient.bucketExists(BUCKET_NAME); + const bucketExists = await minioClient.bucketExists( + MINIO.QR_BUCKET_NAME, + ); if (!bucketExists) { await minioClient.makeBucket( - BUCKET_NAME, - KNIGHTHACKS_S3_BUCKET_REGION, + MINIO.QR_BUCKET_NAME, + MINIO.BUCKET_REGION, ); } const qrData = `user:${userId}`; const qrBuffer = await QRCode.toBuffer(qrData, { type: "png" }); await minioClient.putObject( - BUCKET_NAME, + MINIO.QR_BUCKET_NAME, objectName, qrBuffer, qrBuffer.length, @@ -1172,18 +1168,18 @@ export const hackerRouter = { try { await addRoleToMember( discordId, - DISCORD.KNIGHTHACKS_8.KH_EVENT_ROLE_ID, + HACKATHONS.KNIGHT_HACKS_8.KH_EVENT_ROLE_ID, ); console.log( - `Assigned role ${DISCORD.KNIGHTHACKS_8.KH_EVENT_ROLE_ID} to user ${discordId}`, + `Assigned role ${HACKATHONS.KNIGHT_HACKS_8.KH_EVENT_ROLE_ID} to user ${discordId}`, ); // VIP will already be given the discord role ahead of time, so no need to assign again if (assignedClass) { await addRoleToMember( discordId, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - DISCORD.KNIGHTHACKS_8.CLASS_ROLE_ID[ - assignedClass as AssignableHackerClass + HACKATHONS.KNIGHT_HACKS_8.CLASS_ROLE_ID[ + assignedClass as HACKATHONS.KNIGHT_HACKS_8.AssignableHackerClass ] ?? "", ); } diff --git a/packages/api/src/routers/member.ts b/packages/api/src/routers/member.ts index dcdb5582b..c33127972 100644 --- a/packages/api/src/routers/member.ts +++ b/packages/api/src/routers/member.ts @@ -3,12 +3,7 @@ import { TRPCError } from "@trpc/server"; import QRCode from "qrcode"; import { z } from "zod"; -import { - BUCKET_NAME, - DUES_PAYMENT, - FORMS, - KNIGHTHACKS_S3_BUCKET_REGION, -} from "@forge/consts"; +import { CLUB, FORMS, MINIO } from "@forge/consts"; import { and, count, @@ -57,17 +52,19 @@ export const memberRouter = { if (existingMember.length === 0) { const objectName = `qr-code-${userId}.png`; - const bucketExists = await minioClient.bucketExists(BUCKET_NAME); + const bucketExists = await minioClient.bucketExists( + MINIO.QR_BUCKET_NAME, + ); if (!bucketExists) { await minioClient.makeBucket( - BUCKET_NAME, - KNIGHTHACKS_S3_BUCKET_REGION, + MINIO.QR_BUCKET_NAME, + MINIO.BUCKET_REGION, ); } const qrData = `user:${userId}`; const qrBuffer = await QRCode.toBuffer(qrData, { type: "png" }); await minioClient.putObject( - BUCKET_NAME, + MINIO.BUCKET_REGION, objectName, qrBuffer, qrBuffer.length, @@ -426,7 +423,7 @@ export const memberRouter = { }); await db.insert(DuesPayment).values({ memberId: input.id, - amount: DUES_PAYMENT as number, + amount: CLUB.DUES_PAYMENT as number, paymentDate: new Date(), year: new Date().getFullYear(), }); diff --git a/packages/api/src/routers/misc.ts b/packages/api/src/routers/misc.ts index 931901e5c..40d5528d8 100644 --- a/packages/api/src/routers/misc.ts +++ b/packages/api/src/routers/misc.ts @@ -2,7 +2,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { Routes } from "discord-api-types/v10"; import { z } from "zod"; -import { DISCORD } from "@forge/consts"; +import { DISCORD, FORMS, TEAM } from "@forge/consts"; import { protectedProcedure } from "../trpc"; import { discord } from "../utils"; @@ -84,7 +84,7 @@ export const miscRouter = { }), ) .mutation(async ({ input, ctx }) => { - if (!DISCORD.ALLOWED_FORM_ASSIGNABLE_DISC_ROLES.includes(input.roleId)) { + if (FORMS.ALLOWED_ASSIGNABLE_DISCORD_ROLES.includes(input.roleId)) { throw new Error( `Roleid: ${input.roleId} is not assignable through forms for security purposes. Add to consts and make a PR if this is a mistake.`, ); @@ -129,7 +129,7 @@ export const miscRouter = { }), ) .mutation(async ({ input }) => { - const team = DISCORD.TEAMS.find((team) => team.team === input.team); + const team = TEAM.TEAMS.find((team) => team.team === input.team); if (!team) { throw new Error("Team not found"); } diff --git a/packages/api/src/routers/qr.ts b/packages/api/src/routers/qr.ts index b787355ab..105da0d13 100644 --- a/packages/api/src/routers/qr.ts +++ b/packages/api/src/routers/qr.ts @@ -1,7 +1,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; import QRCode from "qrcode"; -import { BUCKET_NAME, KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts"; +import { MINIO } from "@forge/consts"; import { minioClient } from "../minio/minio-client"; import { protectedProcedure } from "../trpc"; @@ -13,19 +13,21 @@ export const qrRouter = { try { try { - await minioClient.statObject(BUCKET_NAME, objectName); + await minioClient.statObject(MINIO.QR_BUCKET_NAME, objectName); } catch { - const bucketExists = await minioClient.bucketExists(BUCKET_NAME); + const bucketExists = await minioClient.bucketExists( + MINIO.QR_BUCKET_NAME, + ); if (!bucketExists) { await minioClient.makeBucket( - BUCKET_NAME, - KNIGHTHACKS_S3_BUCKET_REGION, + MINIO.QR_BUCKET_NAME, + MINIO.BUCKET_REGION, ); } const qrData = `user:${userId}`; const qrBuffer = await QRCode.toBuffer(qrData, { type: "png" }); await minioClient.putObject( - BUCKET_NAME, + MINIO.QR_BUCKET_NAME, objectName, qrBuffer, qrBuffer.length, @@ -34,7 +36,7 @@ export const qrRouter = { } const qrCodeUrl = await minioClient.presignedGetObject( - BUCKET_NAME, + MINIO.QR_BUCKET_NAME, objectName, 60 * 60 * 24, ); diff --git a/packages/api/src/routers/resume.ts b/packages/api/src/routers/resume.ts index 1b9797eb7..af6598088 100644 --- a/packages/api/src/routers/resume.ts +++ b/packages/api/src/routers/resume.ts @@ -3,7 +3,7 @@ import { TRPCError } from "@trpc/server"; import { Client } from "minio"; import { z } from "zod"; -import { KNIGHTHACKS_S3_BUCKET_REGION } from "@forge/consts"; +import { MINIO } from "@forge/consts"; import { db } from "@forge/db/client"; import { env } from "../env"; @@ -39,7 +39,7 @@ export const resumeRouter = { // Ensure bucket exists const bucketExists = await s3Client.bucketExists(bucketName); if (!bucketExists) { - await s3Client.makeBucket(bucketName, KNIGHTHACKS_S3_BUCKET_REGION); + await s3Client.makeBucket(bucketName, MINIO.BUCKET_REGION); } // Overwrite any existing resume associated with the user diff --git a/packages/api/src/routers/roles.ts b/packages/api/src/routers/roles.ts index de64aa28f..392158a5d 100644 --- a/packages/api/src/routers/roles.ts +++ b/packages/api/src/routers/roles.ts @@ -4,7 +4,6 @@ import { TRPCError } from "@trpc/server"; import { Routes } from "discord-api-types/v10"; import { z } from "zod"; -import type { PermissionKey } from "@forge/consts"; import { DISCORD, PERMISSIONS } from "@forge/consts"; import { eq, inArray, sql } from "@forge/db"; import { db } from "@forge/db/client"; @@ -280,13 +279,13 @@ export const rolesRouter = { const permissionsMap = Object.keys(PERMISSIONS).reduce( (accumulator, key) => { - const index = PERMISSIONS[key]; + const index = PERMISSIONS.PERMISSIONS[key]; if (index === undefined) return accumulator; accumulator[key] = permissionsBits[index] ?? false; return accumulator; }, - {} as Record, + {} as Record, ); return permissionsMap; diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 621518eca..3b467411a 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -11,7 +11,6 @@ import superjson from "superjson"; import { ZodError } from "zod"; import type { Session } from "@forge/auth/server"; -import type { PermissionKey } from "@forge/consts"; import { validateToken } from "@forge/auth/server"; import { PERMISSIONS } from "@forge/consts"; import { eq, sql } from "@forge/db"; @@ -164,13 +163,13 @@ export const permProcedure = protectedProcedure.use(async ({ ctx, next }) => { const permissionsMap = Object.keys(PERMISSIONS).reduce( (accumulator, key) => { - const index = PERMISSIONS[key]; + const index = PERMISSIONS.PERMISSIONS[key]; if (index === undefined) return accumulator; accumulator[key] = permissionsBits[index] ?? false; return accumulator; }, - {} as Record, + {} as Record, ); return next({ diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts index 06331bccd..befb2c118 100644 --- a/packages/api/src/utils.ts +++ b/packages/api/src/utils.ts @@ -9,16 +9,7 @@ import { google } from "googleapis"; import Stripe from "stripe"; import type { Session } from "@forge/auth/server"; -import type { PermissionIndex, PermissionKey } from "@forge/consts"; -import { - DISCORD, - FORM_ASSETS_BUCKET, - FORMS, - GOOGLE_PERSONIFY_EMAIL, - PERMISSION_DATA, - PERMISSIONS, - PRESIGNED_URL_EXPIRY, -} from "@forge/consts"; +import { DISCORD, EVENTS, FORMS, MINIO, PERMISSIONS } from "@forge/consts"; import { db } from "@forge/db/client"; import { JudgeSession, Roles } from "@forge/db/schemas/auth"; import { client } from "@forge/email"; @@ -71,7 +62,7 @@ export const isDiscordAdmin = async (user: Session["user"]) => { export const hasPermission = ( userPermissions: string, - permission: PermissionIndex, + permission: PERMISSIONS.PermissionIndex, ): boolean => { const permissionBit = userPermissions[permission]; return permissionBit === "1"; @@ -113,14 +104,14 @@ export const parsePermissions = async (discordUserId: string) => { // creates the map of permissions to their boolean values const permissionsMap = Object.keys(PERMISSIONS).reduce( (accumulator, key) => { - const index = PERMISSIONS[key]; + const index = PERMISSIONS.PERMISSIONS[key]; if (index === undefined) return accumulator; accumulator[key] = permissionsBits[index] ?? false; return accumulator; }, - {} as Record, + {} as Record, ); return permissionsMap; @@ -129,13 +120,13 @@ export const parsePermissions = async (discordUserId: string) => { // Mock tRPC context for type-safety interface Context { session: { - permissions: Record; + permissions: Record; }; } export const controlPerms = { // Returns true if the user has any required permission OR has isOfficer role - or: (perms: PermissionKey[], ctx: Context) => { + or: (perms: PERMISSIONS.PermissionKey[], ctx: Context) => { // first check if user has IS_OFFICER if (ctx.session.permissions.IS_OFFICER) return true; @@ -146,7 +137,7 @@ export const controlPerms = { }, // Returns true only if the user has ALL required permissions - and: (perms: PermissionKey[], ctx: Context) => { + and: (perms: PERMISSIONS.PermissionKey[], ctx: Context) => { // first check if user has IS_OFFICER if (ctx.session.permissions.IS_OFFICER) return true; @@ -300,7 +291,7 @@ const auth = new google.auth.JWT( undefined, GOOGLE_PRIVATE_KEY, [gapiCalendar, gapiGmailSend, gapiGmailSettingsSharing], - GOOGLE_PERSONIFY_EMAIL as string, + EVENTS.GOOGLE_PERSONIFY_EMAIL as string, ); export const gmail = google.gmail({ @@ -459,9 +450,9 @@ export async function regenerateMediaUrls( if ("imageObjectName" in i && i.imageObjectName) { try { updated.imageUrl = await minioClient.presignedGetObject( - FORM_ASSETS_BUCKET, + MINIO.FORM_ASSETS_BUCKET_NAME, i.imageObjectName, - PRESIGNED_URL_EXPIRY, + MINIO.PRESIGNED_URL_EXPIRY, ); } catch (e) { console.error("Failed to regenerate image URL:", e); @@ -472,9 +463,9 @@ export async function regenerateMediaUrls( if ("videoObjectName" in i && i.videoObjectName) { try { updated.videoUrl = await minioClient.presignedGetObject( - FORM_ASSETS_BUCKET, + MINIO.FORM_ASSETS_BUCKET_NAME, i.videoObjectName, - PRESIGNED_URL_EXPIRY, + MINIO.PRESIGNED_URL_EXPIRY, ); } catch (e) { console.error("Failed to regenerate video URL:", e); @@ -494,7 +485,7 @@ export function getPermsAsList(perms: string) { for (let i = 0; i < perms.length; i++) { const permKey = permKeys.at(i); if (perms[i] == "1" && permKey) { - const permissionData = PERMISSION_DATA[permKey]; + const permissionData = PERMISSIONS.PERMISSION_DATA[permKey]; if (permissionData) list.push(permissionData.name); } } From 2599a8eea53af0b419c034fc1478cf57faa490a1 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:08:00 -0500 Subject: [PATCH 20/23] chore(guild): refactor to consts changes --- apps/club/src/app/officers/_components/officers.tsx | 4 ++-- apps/guild/src/app/_components/dock.tsx | 7 +++---- apps/guild/src/app/page.tsx | 7 +++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/club/src/app/officers/_components/officers.tsx b/apps/club/src/app/officers/_components/officers.tsx index c48e12bca..ef30c0807 100644 --- a/apps/club/src/app/officers/_components/officers.tsx +++ b/apps/club/src/app/officers/_components/officers.tsx @@ -5,7 +5,7 @@ import { useGSAP } from "@gsap/react"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; -import { OFFICERS } from "@forge/consts"; +import { TEAM } from "@forge/consts"; import OfficerCard from "./assets/officer-card"; @@ -45,7 +45,7 @@ export default function Officers() { return (
    - {OFFICERS.LIST.map((officer, index) => ( + {TEAM.OFFICERS.map((officer, index) => (
    { diff --git a/apps/guild/src/app/_components/dock.tsx b/apps/guild/src/app/_components/dock.tsx index 79823fdfe..f69b88b2c 100644 --- a/apps/guild/src/app/_components/dock.tsx +++ b/apps/guild/src/app/_components/dock.tsx @@ -4,8 +4,7 @@ import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Search } from "lucide-react"; -import type { GuildTag } from "@forge/consts"; -import { GUILD_TAG_OPTIONS } from "@forge/consts"; +import { GUILD } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Input } from "@forge/ui/input"; import { @@ -20,7 +19,7 @@ const PAGE_SIZE_OPTIONS = [20, 40, 60, 80, 100] as const; type PageSize = (typeof PAGE_SIZE_OPTIONS)[number]; const DEFAULT_PAGE_SIZE: PageSize = 20; -type Tag = GuildTag | "none"; +type Tag = GUILD.GuildTag | "none"; interface DockProps { initialQuery: string; @@ -104,7 +103,7 @@ export default function Dock({ All - {GUILD_TAG_OPTIONS.map((t) => ( + {GUILD.GUILD_TAG_OPTIONS.map((t) => ( t === searchParams.tag) ?? undefined; + const selectedTag: GUILD.GuildTag | undefined = + GUILD.GUILD_TAG_OPTIONS.find((t) => t === searchParams.tag) ?? undefined; const { members, total } = await api.guild.getGuildMembers({ page: currentPage, From a54f922ce2e72d4989d22c21f74fe79d81475683 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:28:45 -0500 Subject: [PATCH 21/23] chore(blade): refactor to consts changes --- apps/blade/src/app/_components/bad-perms.tsx | 7 ++-- .../navigation/reusable-user-dropdown.tsx | 6 +-- .../_components/navigation/user-dropdown.tsx | 6 +-- .../club/events/_components/delete-event.tsx | 4 +- .../members/_components/delete-member.tsx | 4 +- .../members/_components/final-dues-dialog.tsx | 6 +-- .../members/_components/member-profile.tsx | 20 +++++---- .../events/_components/delete-event.tsx | 4 +- .../hackers/_components/delete-hacker.tsx | 4 +- .../hackers/_components/hacker-profile.tsx | 14 +++---- .../roles/configure/_components/roleedit.tsx | 8 ++-- .../hackathon-dashboard.tsx | 8 ++-- .../hacker-dashboard/hacker-data.tsx | 4 +- .../_components/hacker-application-form.tsx | 6 +-- .../_components/member-application-form.tsx | 42 ++++++++----------- .../_components/delete-hacker-button.tsx | 4 +- .../_components/delete-member-button.tsx | 4 +- .../hacker-profile/hacker-profile-form.tsx | 6 +-- .../src/app/settings/member-profile-form.tsx | 42 ++++++++----------- apps/blade/src/app/sponsor/page.tsx | 5 +-- apps/blade/src/lib/utils.ts | 6 +-- packages/api/src/routers/roles.ts | 4 +- packages/api/src/routers/user.ts | 2 +- packages/api/src/trpc.ts | 4 +- packages/api/src/utils.ts | 6 +-- packages/db/scripts/bootstrap-superadmin.ts | 4 +- 26 files changed, 111 insertions(+), 119 deletions(-) diff --git a/apps/blade/src/app/_components/bad-perms.tsx b/apps/blade/src/app/_components/bad-perms.tsx index 64973c766..4324c9713 100644 --- a/apps/blade/src/app/_components/bad-perms.tsx +++ b/apps/blade/src/app/_components/bad-perms.tsx @@ -1,12 +1,11 @@ import { ShieldX } from "lucide-react"; -import type { PermissionKey } from "@forge/consts"; -import { PERMISSION_DATA } from "@forge/consts"; +import { PERMISSIONS } from "@forge/consts"; -export function BadPerms({ perms }: { perms: PermissionKey[] }) { +export function BadPerms({ perms }: { perms: PERMISSIONS.PermissionKey[] }) { const permNames: string[] = []; perms.forEach((v) => { - const permissionData = PERMISSION_DATA[v]; + const permissionData = PERMISSIONS.PERMISSION_DATA[v]; if (permissionData) permNames.push(permissionData.name); }); diff --git a/apps/blade/src/app/_components/navigation/reusable-user-dropdown.tsx b/apps/blade/src/app/_components/navigation/reusable-user-dropdown.tsx index 89336f3a3..a01b7dd1f 100644 --- a/apps/blade/src/app/_components/navigation/reusable-user-dropdown.tsx +++ b/apps/blade/src/app/_components/navigation/reusable-user-dropdown.tsx @@ -12,7 +12,7 @@ import { Users, } from "lucide-react"; -import type { PermissionKey } from "@forge/consts"; +import type { PERMISSIONS } from "@forge/consts"; import { USER_DROPDOWN_ICON_COLOR, USER_DROPDOWN_ICON_SIZE } from "~/consts"; @@ -29,8 +29,8 @@ export interface roleItems { component: React.JSX.Element; route: string; requiredPermissions?: { - or?: PermissionKey[]; - and?: PermissionKey[]; + or?: PERMISSIONS.PermissionKey[]; + and?: PERMISSIONS.PermissionKey[]; }; } diff --git a/apps/blade/src/app/_components/navigation/user-dropdown.tsx b/apps/blade/src/app/_components/navigation/user-dropdown.tsx index 991de7c80..17719c495 100644 --- a/apps/blade/src/app/_components/navigation/user-dropdown.tsx +++ b/apps/blade/src/app/_components/navigation/user-dropdown.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { LayoutDashboard } from "lucide-react"; -import type { PermissionKey } from "@forge/consts"; +import type { PERMISSIONS } from "@forge/consts"; import { signOut } from "@forge/auth"; import { Avatar, AvatarFallback, AvatarImage } from "@forge/ui/avatar"; import { Button } from "@forge/ui/button"; @@ -29,7 +29,7 @@ import { } from "./reusable-user-dropdown"; interface UserDropdownProps { - permissions: Record; + permissions: Record; } /** @@ -37,7 +37,7 @@ interface UserDropdownProps { */ function filterItemsByPermissions( items: roleItems[], - permissions: Record, + permissions: Record, ): roleItems[] { return items.filter((item) => { // If no permissions required, show the item diff --git a/apps/blade/src/app/admin/club/events/_components/delete-event.tsx b/apps/blade/src/app/admin/club/events/_components/delete-event.tsx index f59e57a4c..fac4780a0 100644 --- a/apps/blade/src/app/admin/club/events/_components/delete-event.tsx +++ b/apps/blade/src/app/admin/club/events/_components/delete-event.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Loader2, Trash2 } from "lucide-react"; import type { EVENTS } from "@forge/consts"; -import { USE_CAUTION } from "@forge/consts"; +import { CLUB } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -112,7 +112,7 @@ export function DeleteEventButton({ event }: DeleteEventButtonProps) {
    {member.linkedinProfileUrl ? ( - + ) : ( - + )}
    {member.websiteUrl ? ( - + ) : ( - + )}
    diff --git a/apps/blade/src/app/admin/hackathon/events/_components/delete-event.tsx b/apps/blade/src/app/admin/hackathon/events/_components/delete-event.tsx index f59e57a4c..fac4780a0 100644 --- a/apps/blade/src/app/admin/hackathon/events/_components/delete-event.tsx +++ b/apps/blade/src/app/admin/hackathon/events/_components/delete-event.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Loader2, Trash2 } from "lucide-react"; import type { EVENTS } from "@forge/consts"; -import { USE_CAUTION } from "@forge/consts"; +import { CLUB } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Dialog, @@ -112,7 +112,7 @@ export function DeleteEventButton({ event }: DeleteEventButtonProps) {
    {hacker.linkedinProfileUrl ? ( - + ) : ( - + )}
    {hacker.websiteUrl ? ( - + ) : ( - + )}
    diff --git a/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx b/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx index d64049499..ca26209fc 100644 --- a/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx +++ b/apps/blade/src/app/admin/roles/configure/_components/roleedit.tsx @@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from "react"; import { Link, Loader2, Pencil, User, X } from "lucide-react"; import { z } from "zod"; -import { PERMISSION_DATA, PERMISSIONS } from "@forge/consts"; +import { PERMISSIONS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; import { @@ -45,7 +45,7 @@ export default function RoleEdit({ const [isUpdating, setIsUpdating] = useState(false); const [permString, setPermString] = useState( - "0".repeat(Object.keys(PERMISSIONS).length), + "0".repeat(Object.keys(PERMISSIONS.PERMISSIONS).length), ); const roleQ = api.roles.getDiscordRole.useQuery( @@ -63,7 +63,7 @@ export default function RoleEdit({ // Create base form schema dynamically from consts const roleObj: Record = {}; const defaults: Record = {}; - Object.keys(PERMISSIONS).map((v, i) => { + Object.keys(PERMISSIONS.PERMISSIONS).map((v, i) => { roleObj[v] = z.boolean(); if (oldRole) { defaults[v] = oldRole.permissions?.at(i) == "1"; @@ -269,7 +269,7 @@ export default function RoleEdit({ onSubmit={form.handleSubmit(updateString)} className="flex max-h-[40vh] flex-col overflow-y-scroll rounded-lg border" > - {Object.entries(PERMISSION_DATA).map((v) => ( + {Object.entries(PERMISSIONS.PERMISSION_DATA).map((v) => ( @@ -72,8 +72,8 @@ export default async function HackathonDashboard({ classPfp: string; } - const HACKER_CLASS_INFO_TYPED: Record = DISCORD - .KNIGHTHACKS_8.HACKER_CLASS_INFO as Record; + const HACKER_CLASS_INFO_TYPED: Record = HACKATHONS + .KNIGHT_HACKS_8.HACKER_CLASS_INFO as Record; const classInfo = HACKER_CLASS_INFO_TYPED[hacker.class] ?? { teamColor: "#000000", diff --git a/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx b/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx index f14d56539..bdf80dcf1 100644 --- a/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx +++ b/apps/blade/src/app/dashboard/_components/hacker-dashboard/hacker-data.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import { CircleCheckBig, Loader2 } from "lucide-react"; -import { USE_CAUTION } from "@forge/consts"; +import { CLUB } from "@forge/consts"; import { HACKATHON_TEMPLATE_IDS } from "@forge/email/client"; import { Button } from "@forge/ui/button"; import { @@ -294,7 +294,7 @@ export function HackerData({
    @@ -172,7 +175,10 @@ export default function HackerProfileButton({ ) : ( - + )}
    diff --git a/packages/api/src/routers/roles.ts b/packages/api/src/routers/roles.ts index 8a79e882c..c669d4e2b 100644 --- a/packages/api/src/routers/roles.ts +++ b/packages/api/src/routers/roles.ts @@ -267,9 +267,9 @@ export const rolesRouter = { sql`cast(${Permissions.userId} as text) = ${input ? input.userId : ctx.session.user.id}`, ); - const permissionsBits = new Array(Object.keys(PERMISSIONS.PERMISSIONS).length).fill( - false, - ) as boolean[]; + const permissionsBits = new Array( + Object.keys(PERMISSIONS.PERMISSIONS).length, + ).fill(false) as boolean[]; permRows.forEach((v) => { for (let i = 0; i < v.permissions.length; i++) { diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 2850c94ac..a82242edc 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -151,9 +151,9 @@ export const permProcedure = protectedProcedure.use(async ({ ctx, next }) => { .innerJoin(Permissions, eq(Roles.id, Permissions.roleId)) .where(sql`cast(${Permissions.userId} as text) = ${ctx.session.user.id}`); - const permissionsBits = new Array(Object.keys(PERMISSIONS.PERMISSIONS).length).fill( - false, - ) as boolean[]; + const permissionsBits = new Array( + Object.keys(PERMISSIONS.PERMISSIONS).length, + ).fill(false) as boolean[]; permRows.forEach((v) => { for (let i = 0; i < v.permissions.length; i++) { diff --git a/packages/consts/README.md b/packages/consts/README.md index 1b68a9208..1c02568e3 100644 --- a/packages/consts/README.md +++ b/packages/consts/README.md @@ -3,88 +3,3 @@ The `consts` package is, obviously, where we store constant variables. Before you add to this, you must ask yourself: "does this need to be used between two separate apps". If the answer is yes, find a good place to put it. If the answer is no, then we probably don't want to put it here. Some types of data kind of go against this would be, for example, a named Discord channel that we need to remember. Storing it at the top of the file that it is used in makes no sense. Just some food for thought. - -Note: please do not use `misc.ts`. This is a placeholder until we can migrate the rest. Thanks for understanding. - - -Got it 👍 Here’s a clean, explicit list of **everything Dylan asked MILLION to change**, distilled from the chat and mapped to where things *should* live. - ---- - -## ✅ Requested Changes (by target area) - -### **Minio** - -Move the following into **minio**: - -* KH bucket -* PFP extensions -* Minio endpoint → QR path - -*(Dylan said these should be guild or minio, but explicitly “probably minio”)* - ---- - -### **Forms** - -Move into **forms**: - -* `term_to_date` - *(Reason: only used with graduation date, which is forms-only)* - ---- - -### **Guild** - -Move into **guild**: - -* Guild tag -* Member profile icon - ---- - -### **Club** - -Move into a **club** file: - -* Membership price -* Dues payment -* `clear_dues_message` -* Caution - ---- - -### **Permissions** - -Move into a **permissions** file: - -* Permission object -* Permission index - ---- - -### **Events** - -Move into **events**: - -* Time zone -* Calendar ID -* Personify email -* Weekday order *(this was the only forms/events correction Dylan called out later)* - ---- - -## ❌ Explicitly Not Requested - -* No review of other ~80 files -* Everything else assumed mapped correctly -* Forms/events mostly fine aside from weekday order -* Validation likely moving to `@forge/utils` later, not part of this task - ---- - -If you want, I can: - -* Turn this into a **task checklist** -* Propose an **ideal folder tree** -* Or map each item to **exact filenames** based on your current `consts` layout diff --git a/packages/consts/package.json b/packages/consts/package.json index a082309ff..a77c5f775 100644 --- a/packages/consts/package.json +++ b/packages/consts/package.json @@ -32,4 +32,4 @@ "discord-api-types": "^0.37.113", "minimatch": "^10.1.2" } -} \ No newline at end of file +} diff --git a/packages/consts/src/minio.ts b/packages/consts/src/minio.ts index 8a906cab0..7dd0ebc48 100644 --- a/packages/consts/src/minio.ts +++ b/packages/consts/src/minio.ts @@ -12,7 +12,6 @@ export const FORM_ASSETS_BUCKET_NAME = "form-assets"; // TODO: check if this should be MB or MiB export const MAX_RESUME_SIZE = 5 * 1000000; // 5MB - export const PRESIGNED_URL_EXPIRY = 7 * 24 * 60 * 60; // 7 days // TODO: see above From 2adc4d7b92405d8e44200693624c12b954de4994 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:39:45 -0500 Subject: [PATCH 23/23] fix(consts): out of scope out of scope Co-Authored-By: Carlos Catala <119781636+cataladev@users.noreply.github.com> --- apps/blade/src/components/forms/form-card.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/blade/src/components/forms/form-card.tsx b/apps/blade/src/components/forms/form-card.tsx index a654e4ce0..ad6991318 100644 --- a/apps/blade/src/components/forms/form-card.tsx +++ b/apps/blade/src/components/forms/form-card.tsx @@ -140,8 +140,10 @@ export function FormCard({

    - {(fullForm?.formData as FORMS.FormType).description || - "No description"} + {fullForm?.formData + ? (fullForm.formData as FORMS.FormType).description || + "No description" + : "No description"}