diff --git a/.gitignore b/.gitignore
index 5a6f8656..d6aeb5e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,4 @@ yarn-error.log*
# vscode
.vscode
.env*.local
+.env.*
\ No newline at end of file
diff --git a/apps/infrastructure-migrator/driver.ts b/apps/infrastructure-migrator/driver.ts
deleted file mode 100644
index 60eca43d..00000000
--- a/apps/infrastructure-migrator/driver.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-import { sql } from "@vercel/postgres";
-import { drizzle as pgDrizzle } from "drizzle-orm/vercel-postgres";
-import { drizzle } from "drizzle-orm/libsql";
-import * as pgSchema from "./schema";
-import { createClient } from "@libsql/client";
-export * from "drizzle-orm";
-import dotenv from "dotenv";
-import * as schema from "db/schema";
-import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
-import c, { staticUploads } from "config";
-import { eq } from "drizzle-orm";
-
-dotenv.config({
- path: "../../.env",
-});
-
-export const S3 = new S3Client({
- region: "auto",
- endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID!}.r2.cloudflarestorage.com`,
- credentials: {
- accessKeyId: process.env.R2_ACCESS_KEY_ID!,
- secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
- },
-});
-
-const dbPostgres = pgDrizzle(sql, { schema: pgSchema });
-const turso = createClient({
- url: process.env.TURSO_DATABASE_URL!,
- authToken: process.env.TURSO_AUTH_TOKEN,
-});
-
-const db = drizzle(turso, { schema });
-
-const allUsersPromise = dbPostgres.query.users.findMany();
-const allDataPromise = dbPostgres.query.data.findMany();
-const allEventsPromise = dbPostgres.query.events.findMany();
-const allCheckinsPromise = dbPostgres.query.checkins.findMany();
-const allEventCategoriesPromise = dbPostgres.query.eventCategories.findMany();
-const allSemestersPromise = dbPostgres.query.semesters.findMany();
-const allEventsToCategoriesPromise =
- dbPostgres.query.eventsToCategories.findMany();
-
-async function migratePostgresSqLite(runDB = false) {
- console.log("Starting Migration 🚀");
- console.log("Fetching Postgres Data 🐘");
- const [
- allUsers,
- allData,
- allEvents,
- allCheckins,
- allEventCategories,
- allSemesters,
- allEventsToCategories,
- ] = await Promise.all([
- allUsersPromise,
- allDataPromise,
- allEventsPromise,
- allCheckinsPromise,
- allEventCategoriesPromise,
- allSemestersPromise,
- allEventsToCategoriesPromise,
- ]);
- // console.log("Postgres data fetched 📦");
-
- // console.log("Migrating Users 👥");
- // await db.insert(schema.users).values(allUsers);
- // console.log("Migrated Users ✅");
-
- // console.log("Migrating User Data 📊");
- // await db.insert(schema.data).values(allData);
- // console.log("Migrated User Data ✅");
-
- // console.log("Migrating Event Categories 💬");
- // await db.insert(schema.eventCategories).values(allEventCategories);
- // console.log("Migrated Event Categories ✅");
-
- // console.log("Migrating Semesters 📚");
- // await db.insert(schema.semesters).values(allSemesters);
- // console.log("Migrated Semesters ✅");
-
- // console.log("Migrating Events 📝");
- // await db.insert(schema.events).values(
- // allEvents.map((event) => ({
- // ...event,
- // updatedAt: event.updatedAt.getTime(),
- // })),
- // );
- // console.log("Migrated Events ✅");
-
- // console.log("Migrating Events to Categories 📝");
- // await db.insert(schema.eventsToCategories).values(allEventsToCategories);
- // console.log("Migrated Events to Categories ✅");
-
- // console.log("Migrating Checkins 📝");
- // await db.insert(schema.checkins).values(allCheckins);
- // console.log("Migrated Checkins ✅");
-
- console.log("Migrating Vercel Blob Files To R2");
-
- const resumeData = await db.query.data.findMany({
- columns: { resume: true, userID: true },
- });
-
- for (let resumeEntry of resumeData) {
- const { resume: resumeUrlAsString, userID } = resumeEntry;
- if (
- resumeUrlAsString === null ||
- !resumeUrlAsString.length ||
- resumeUrlAsString.startsWith("/api")
- )
- continue;
- console.log("Migrating resume for user", userID, resumeUrlAsString);
- const resumeUrl = new URL(resumeUrlAsString);
- const resumeFetchResponse = await fetch(resumeUrl);
-
- if (!resumeFetchResponse.ok) {
- console.log("resume fetch failed");
- continue;
- }
- const resumeBlob = await resumeFetchResponse.blob();
-
- let key = decodeURIComponent(resumeUrl.pathname);
- // if the first character is a slash, remove it
- if (key.charAt(0) === "/") {
- key = key.slice(1);
- }
-
- const buffer = await resumeBlob.arrayBuffer();
-
- const cmd = new PutObjectCommand({
- Key: key,
- Bucket: staticUploads.bucketName,
- ContentType: "application/pdf",
- ///@ts-expect-error
- Body: buffer,
- });
-
- await S3.send(cmd);
-
- // New url to correspond to an api route
- const newResumeUrl = `/api/upload/resume/view?key=${key}`;
-
- await db
- .update(schema.data)
- .set({ resume: newResumeUrl.toString() })
- .where(eq(schema.data.userID, userID));
- }
-
- console.log("Migrated Vercel Blob Files To R2");
-
- return process.exit(0);
-}
-
-migratePostgresSqLite().catch((e) => {
- console.error(e);
- process.exit(1);
-});
diff --git a/apps/infrastructure-migrator/package.json b/apps/infrastructure-migrator/package.json
deleted file mode 100644
index 9ddd440d..00000000
--- a/apps/infrastructure-migrator/package.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "infrastructure-migrator",
- "version": "1.0.0",
- "description": "Helper to migrate from postgres to sqlite",
- "main": "driver.ts",
- "scripts": {
- "migrate": "node -r esbuild-register driver.ts"
- },
- "author": "",
- "license": "ISC",
- "dependencies": {
- "@aws-sdk/client-s3": "^3.758.0",
- "@libsql/client": "^0.14.0",
- "@vercel/postgres": "^0.10.0",
- "config": "workspace:*",
- "db": "workspace:*",
- "dotenv": "^16.3.1",
- "drizzle-orm": "^0.38.4",
- "esbuild-register": "^3.4.2",
- "web": "workspace:*"
- },
- "devDependencies": {
- "drizzle-kit": "^0.30.5"
- }
-}
diff --git a/apps/infrastructure-migrator/schema.ts b/apps/infrastructure-migrator/schema.ts
deleted file mode 100644
index 239a54fb..00000000
--- a/apps/infrastructure-migrator/schema.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-import {
- text,
- varchar,
- boolean,
- timestamp,
- integer,
- pgEnum,
- primaryKey,
- pgTable,
- serial,
- date,
-} from "drizzle-orm/pg-core";
-import { relations } from "drizzle-orm";
-import c from "config";
-
-// pieces of this schema need to be revamped as a lot of them are lazily set as text instead of varchar with a hard limit
-/* USERS */
-
-export const userRoles = pgEnum("user_roles", c.memberRoles);
-
-export const users = pgTable("users", {
- userID: serial("user_id").primaryKey(),
- clerkID: varchar("clerk_id", { length: 255 }).unique(),
- firstName: varchar("first_name", { length: 255 }).notNull(),
- lastName: varchar("last_name", { length: 255 }).notNull(),
- email: varchar({ length: 255 }).notNull().unique(),
- role: userRoles().default("member").notNull(),
- joinDate: timestamp("join_date").defaultNow().notNull(),
- universityID: varchar("university_id", { length: 255 }).notNull().unique(),
-});
-
-export const usersRelations = relations(users, ({ one, many }) => ({
- data: one(data, { fields: [users.userID], references: [data.userID] }),
- checkins: many(checkins),
-}));
-
-export const data = pgTable("data", {
- userID: integer("user_id")
- .primaryKey()
- .references(() => users.userID, { onDelete: "cascade" }),
- major: varchar({ length: 255 }).notNull(),
- classification: varchar({ length: 255 }).notNull(),
- graduationMonth: integer("graduation_month").notNull(),
- graduationYear: integer("graduation_year").notNull(),
- birthday: timestamp("birthday"),
- gender: varchar({ length: 255 }).array().notNull(),
- ethnicity: varchar({ length: 255 }).array().notNull(),
- resume: varchar({ length: 255 }),
- shirtType: varchar("shirt_type", { length: 255 }).notNull(),
- shirtSize: varchar("shirt_size", { length: 255 }).notNull(),
- interestedEventTypes: varchar("interested_event_types", { length: 255 })
- .array()
- .notNull(),
-});
-
-/* EVENTS */
-export const eventCategories = pgTable("event_categories", {
- id: varchar("id", { length: 8 }).primaryKey(),
- name: varchar({ length: 255 }).notNull().unique(),
- color: varchar({ length: 255 }).notNull(),
-});
-
-export const eventCategoriesRelations = relations(
- eventCategories,
- ({ many }) => ({
- eventsToCategories: many(eventsToCategories),
- }),
-);
-
-export const events = pgTable("events", {
- id: varchar({ length: 100 }).primaryKey(),
- name: varchar({ length: 100 }).notNull(),
- description: text("description").notNull(),
- thumbnailUrl: varchar("thumbnail_url", { length: 255 })
- .default(c.thumbnails.default)
- .notNull(),
- start: timestamp("start").notNull(),
- end: timestamp("end").notNull(),
- checkinStart: timestamp("checkin_start").notNull(),
- checkinEnd: timestamp("checkin_end").notNull(),
- location: varchar({ length: 255 }).notNull(),
- isUserCheckinable: boolean("is_user_checkinable").notNull().default(true),
- isHidden: boolean("is_hidden").notNull().default(false),
- points: integer("points").notNull().default(1),
- createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
- semesterID: integer("semester_id").references(() => semesters.semesterID, {
- onDelete: "set null",
- }),
-});
-
-export const eventsRelations = relations(events, ({ many, one }) => ({
- eventsToCategories: many(eventsToCategories),
- checkins: many(checkins),
- semester: one(semesters, {
- fields: [events.semesterID],
- references: [semesters.semesterID],
- }),
-}));
-
-export const eventsToCategories = pgTable("events_to_categories", {
- eventID: varchar("event_id", { length: 100 })
- .notNull()
- .references(() => events.id, { onDelete: "cascade" }),
- categoryID: varchar("category_id", { length: 100 })
- .notNull()
- .references(() => eventCategories.id, { onDelete: "cascade" }),
-});
-
-export const eventsToCategoriesRelations = relations(
- eventsToCategories,
- ({ one }) => ({
- category: one(eventCategories, {
- fields: [eventsToCategories.categoryID],
- references: [eventCategories.id],
- }),
- event: one(events, {
- fields: [eventsToCategories.eventID],
- references: [events.id],
- }),
- }),
-);
-
-export const checkins = pgTable(
- "checkins",
- {
- eventID: varchar("event_id", { length: 100 })
- .references(() => events.id, { onDelete: "cascade" })
- .notNull(),
- userID: integer("user_id")
- .references(() => users.userID, { onDelete: "cascade" })
- .notNull(),
- time: timestamp("time").defaultNow().notNull(),
- rating: integer("rating"),
- adminID: integer("admin_id"),
- feedback: varchar({ length: 2000 }),
- },
- (table) => {
- return [
- {
- id: primaryKey({ columns: [table.eventID, table.userID] }),
- },
- ];
- },
-);
-
-export const checkinRelations = relations(checkins, ({ one }) => ({
- author: one(users, {
- fields: [checkins.userID],
- references: [users.userID],
- }),
- event: one(events, {
- fields: [checkins.eventID],
- references: [events.id],
- }),
-}));
-
-export const semesters = pgTable("semesters", {
- semesterID: serial("semester_id").primaryKey(),
- name: varchar("name", { length: 255 }).notNull().unique(),
- startDate: timestamp("start_date").notNull(),
- endDate: timestamp("end_date").notNull(),
- pointsRequired: integer("points_required").notNull(),
- isCurrent: boolean("is_current").notNull().default(false),
-});
-
-export const semestersRelations = relations(semesters, ({ many }) => ({
- events: many(events),
-}));
diff --git a/apps/mover/package.json b/apps/mover/package.json
deleted file mode 100644
index 18ef7551..00000000
--- a/apps/mover/package.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "mover",
- "version": "1.0.0",
- "description": "",
- "main": "index.ts",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
- "move": "bun run --env-file=../../.env ./script.ts"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "dependencies": {
- "bun-promptx": "^0.2.0",
- "db": "workspace:*",
- "nanoid": "^5.0.6",
- "web": "workspace:^",
- "zod": "^3.23.8"
- }
-}
diff --git a/apps/mover/script.ts b/apps/mover/script.ts
deleted file mode 100644
index c7f84bae..00000000
--- a/apps/mover/script.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import { createPrompt } from "bun-promptx";
-import { z } from "zod";
-import { db } from "db";
-import { data, users } from "db/schema";
-
-async function move() {
- const url = createPrompt("Enter Get All Users URL: ");
- if (url.error || !url.value) {
- console.error(url.error);
- return process.exit(1);
- }
-
- const username = createPrompt("Enter admin username: ");
- if (username.error || !username.value) {
- console.error(username.error);
- return process.exit(1);
- }
- const password = createPrompt("Enter password: ", { echoMode: "password" });
- if (password.error || !password.value) {
- console.error(password.error);
- return process.exit(1);
- }
-
- const req = await fetch(url.value, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- apikey: username.value + password.value,
- }),
- });
-
- const res = await req.json();
-
- const allMembers = res.allMembers;
-
- const memberValidator = z.object({
- id: z.string(),
- email: z.string().email(),
- name: z.string(),
- joinDate: z.string().pipe(z.coerce.date()),
- data: z.object({
- memberID: z.string(),
- major: z.string(),
- classification: z.string(),
- graduationDate: z.string(),
- shirtIsUnisex: z.boolean(),
- shirtSize: z.string(),
- Birthday: z.string().optional().nullable(),
- isInACM: z.boolean(),
- isInACMW: z.boolean(),
- isInRC: z.boolean(),
- isInICPC: z.boolean(),
- isInCIC: z.boolean(),
- isBlackorAA: z.boolean(),
- isAsian: z.boolean(),
- isNAorAN: z.boolean(),
- isNHorPI: z.boolean(),
- isHispanicorLatinx: z.boolean(),
- isWhite: z.boolean(),
- isMale: z.boolean(),
- isFemale: z.boolean(),
- isNonBinary: z.boolean(),
- isTransgender: z.boolean(),
- isIntersex: z.boolean(),
- doesNotIdentify: z.boolean(),
- otherIdentity: z.string().optional().nullable(),
- address: z.string().optional().nullable(),
- }),
- extendedMemberData: z.string(),
- lastSeen: z.string().optional().nullable(),
- });
-
- for (const member of allMembers) {
- const validatedMember = memberValidator.safeParse(member);
- if (validatedMember.success) {
- console.log(
- "Processing member: ",
- validatedMember.data.email + " | " + validatedMember.data.name,
- );
- const m = validatedMember.data;
- await db.transaction(async (tx) => {
- const newUserRecordResult = await tx
- .insert(users)
- .values({
- email: m.email,
- joinDate: m.joinDate,
- universityID: m.id,
- firstName: m.name
- .split(" ")
- .slice(0, -1)
- .join(" ")
- .trim(),
- lastName: m.name
- .split(" ")
- [m.name.split(" ").length - 1].trim(),
- })
- .returning({ id: users.userID });
- if (
- newUserRecordResult.length !== 1 ||
- !newUserRecordResult[0]
- ) {
- await tx.rollback();
- console.error(
- `User creation failed for user (${m.id} / ${m.email})`,
- );
- return;
- }
- const newUserRecord = newUserRecordResult[0];
- const gender = [
- m.data.isMale ? "Male" : null,
- m.data.isFemale ? "Female" : null,
- m.data.isNonBinary ? "Non-binary" : null,
- m.data.isTransgender ? "Transgender" : null,
- m.data.isIntersex ? "Intersex" : null,
- m.data.doesNotIdentify ? "Prefer not to say" : null,
- m.data.otherIdentity ? "Other" : null,
- ].filter((x) => x !== null) as string[];
-
- const ethnicity = [
- m.data.isBlackorAA ? "Black or African American" : null,
- m.data.isAsian ? "Asian" : null,
- m.data.isNAorAN ? "American Indian or Alaska Native" : null,
- m.data.isNHorPI
- ? "Native Hawaiian or Other Pacific Islander"
- : null,
- m.data.isHispanicorLatinx ? "Hispanic or Latino" : null,
- m.data.isWhite ? "White" : null,
- ].filter((x) => x !== null) as string[];
-
- let gradYear = 0;
- let gradMonth = 0;
-
- if (m.data.graduationDate.includes("-")) {
- gradYear = parseInt(m.data.graduationDate.split("-")[0]);
- gradMonth = parseInt(m.data.graduationDate.split("-")[1]);
- } else if (m.data.graduationDate.includes("/")) {
- gradYear = parseInt(m.data.graduationDate.split("/")[0]);
- gradMonth = parseInt(m.data.graduationDate.split("/")[1]);
- }
-
- await tx.insert(data).values({
- userID: newUserRecord.id,
- major: m.data.major,
-
- classification: m.data.classification,
- graduationMonth: gradMonth,
- graduationYear: gradYear,
- birthday: m.data.Birthday
- ? new Date(m.data.Birthday)
- : null,
- gender: gender,
- ethnicity: ethnicity,
- resume: null,
- shirtType: m.data.shirtIsUnisex ? "Unisex" : "Women's",
- shirtSize: m.data.shirtSize,
- interestedEventTypes: [],
- });
- });
- } else {
- console.error("Member is invalid: ", member.id);
- console.log("due to error ", validatedMember.error);
- }
- }
-
- return process.exit(0);
-}
-
-move();
diff --git a/apps/web/components.json b/apps/web/components.json
index ce76ad3f..d249920f 100644
--- a/apps/web/components.json
+++ b/apps/web/components.json
@@ -1,6 +1,6 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
- "style": "default",
+ "style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
index 40c3d680..1b3be084 100644
--- a/apps/web/next-env.d.ts
+++ b/apps/web/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/web/package.json b/apps/web/package.json
index 7790ced1..c903e2fb 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "pnpm with-env next dev",
+ "dev": "pnpm with-env next dev --turbopack",
"build": "pnpm with-env next build",
"start": "pnpm with-env next start",
"lint": "next lint",
@@ -21,18 +21,31 @@
"@internationalized/date": "^3.5.4",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
+ "@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
+ "@radix-ui/react-collapsible": "^1.1.3",
+ "@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.0.5",
- "@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
+ "@radix-ui/react-hover-card": "^1.1.6",
+ "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.0.2",
- "@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-menubar": "^1.1.6",
+ "@radix-ui/react-navigation-menu": "^1.2.5",
+ "@radix-ui/react-popover": "^1.1.6",
+ "@radix-ui/react-progress": "^1.1.2",
+ "@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.0.5",
- "@radix-ui/react-select": "^2.0.0",
+ "@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.0",
- "@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-slider": "^1.2.3",
+ "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.1.0",
+ "@radix-ui/react-toggle": "^1.1.2",
+ "@radix-ui/react-toggle-group": "^1.1.2",
+ "@radix-ui/react-tooltip": "^1.1.8",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/match-sorter-utils": "^8.15.1",
"@tanstack/react-table": "^8.17.3",
@@ -40,31 +53,41 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
+ "colorthief": "^2.6.0",
"config": "workspace:*",
"date-fns": "^3.6.0",
"date-fns-tz": "^3.1.3",
- "db": "workspace:*",
"dotenv": "^16.3.1",
"dotenv-cli": "^7.4.1",
+ "embla-carousel-react": "^8.5.2",
+ "fast-fuzzy": "^1.12.0",
+ "input-otp": "^1.4.2",
"jiti": "^1.21.0",
"lucide-react": "^0.377.0",
+ "mapbox-gl": "^3.11.0",
+ "motion": "^12.6.2",
"nanoid": "^5.0.6",
- "next": "14.2.25",
+ "next": "15.2.4",
"next-safe-action": "^7.10.2",
"next-themes": "^0.3.0",
- "react": "^18",
+ "nuqs": "^2.4.1",
+ "react": "19.1.0",
"react-aria": "^3.33.1",
"react-colorful": "^5.6.1",
"react-day-picker": "^8.10.1",
- "react-dom": "^18",
+ "react-dom": "19.1.0",
"react-hook-form": "^7.51.3",
+ "react-map-gl": "^8.0.2",
"react-qr-code": "^2.0.15",
+ "react-resizable-panels": "^2.1.7",
"react-stately": "^3.31.1",
"recharts": "^2.12.7",
"sonner": "^1.4.41",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
+ "tailwindcss-motion": "^1.1.0",
"use-debounce": "^10.0.1",
+ "vaul": "^1.1.2",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -72,11 +95,17 @@
"@cloudflare/workers-types": "^4.20250313.0",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/node": "^20",
- "@types/react": "^18",
- "@types/react-dom": "^18",
+ "@types/react": "19.1.0",
+ "@types/react-dom": "19.1.1",
"autoprefixer": "^10.0.1",
"postcss": "^8",
- "tailwindcss": "^3.3.0",
+ "tailwindcss": "^3.4.16",
"typescript": "^5"
+ },
+ "pnpm": {
+ "overrides": {
+ "@types/react": "19.1.0",
+ "@types/react-dom": "19.1.1"
+ }
}
}
diff --git a/apps/web/public/img/landing/noise.png b/apps/web/public/img/landing/noise.png
new file mode 100644
index 00000000..b86c36c8
Binary files /dev/null and b/apps/web/public/img/landing/noise.png differ
diff --git a/apps/web/public/img/logos/acm-blue.png b/apps/web/public/img/logos/acm-blue.png
new file mode 100644
index 00000000..a83db3df
Binary files /dev/null and b/apps/web/public/img/logos/acm-blue.png differ
diff --git a/apps/web/public/img/logos/acm-white.png b/apps/web/public/img/logos/acm-white.png
new file mode 100644
index 00000000..5b1555d2
Binary files /dev/null and b/apps/web/public/img/logos/acm-white.png differ
diff --git a/apps/web/public/img/logos/discord_logo.webp b/apps/web/public/img/logos/discord_logo.webp
new file mode 100644
index 00000000..5860cda2
Binary files /dev/null and b/apps/web/public/img/logos/discord_logo.webp differ
diff --git a/apps/web/public/img/logos/hackathons/cq-blue.png b/apps/web/public/img/logos/hackathons/cq-blue.png
new file mode 100644
index 00000000..a4cd1be7
Binary files /dev/null and b/apps/web/public/img/logos/hackathons/cq-blue.png differ
diff --git a/apps/web/public/img/logos/hackathons/rd-blue.png b/apps/web/public/img/logos/hackathons/rd-blue.png
new file mode 100644
index 00000000..0a265ced
Binary files /dev/null and b/apps/web/public/img/logos/hackathons/rd-blue.png differ
diff --git a/apps/web/public/img/logos/hackathons/rh-blue.png b/apps/web/public/img/logos/hackathons/rh-blue.png
new file mode 100644
index 00000000..4620b1dd
Binary files /dev/null and b/apps/web/public/img/logos/hackathons/rh-blue.png differ
diff --git a/apps/web/public/img/logos/sponsor/dell.png b/apps/web/public/img/logos/sponsor/dell.png
new file mode 100644
index 00000000..01fe0881
Binary files /dev/null and b/apps/web/public/img/logos/sponsor/dell.png differ
diff --git a/apps/web/public/img/logos/sponsor/heb.png b/apps/web/public/img/logos/sponsor/heb.png
new file mode 100644
index 00000000..0d8bdd5f
Binary files /dev/null and b/apps/web/public/img/logos/sponsor/heb.png differ
diff --git a/apps/web/public/img/logos/sponsor/utsacs.png b/apps/web/public/img/logos/sponsor/utsacs.png
new file mode 100644
index 00000000..04672b3e
Binary files /dev/null and b/apps/web/public/img/logos/sponsor/utsacs.png differ
diff --git a/apps/web/public/img/logos/suborgs/acmw-blue.png b/apps/web/public/img/logos/suborgs/acmw-blue.png
new file mode 100644
index 00000000..30f5c64e
Binary files /dev/null and b/apps/web/public/img/logos/suborgs/acmw-blue.png differ
diff --git a/apps/web/public/img/logos/suborgs/cnc-blue.png b/apps/web/public/img/logos/suborgs/cnc-blue.png
new file mode 100644
index 00000000..91e90938
Binary files /dev/null and b/apps/web/public/img/logos/suborgs/cnc-blue.png differ
diff --git a/apps/web/public/img/logos/suborgs/icpc-blue.png b/apps/web/public/img/logos/suborgs/icpc-blue.png
new file mode 100644
index 00000000..bfdbfc1b
Binary files /dev/null and b/apps/web/public/img/logos/suborgs/icpc-blue.png differ
diff --git a/apps/web/public/img/logos/suborgs/icpc-color.png b/apps/web/public/img/logos/suborgs/icpc-color.png
new file mode 100644
index 00000000..8ee44c6f
Binary files /dev/null and b/apps/web/public/img/logos/suborgs/icpc-color.png differ
diff --git a/apps/web/public/img/logos/suborgs/rc-blue.png b/apps/web/public/img/logos/suborgs/rc-blue.png
new file mode 100644
index 00000000..dca1e3c1
Binary files /dev/null and b/apps/web/public/img/logos/suborgs/rc-blue.png differ
diff --git a/apps/web/public/img/officer_photos/acm_general/brooke-lane.jpg b/apps/web/public/img/officer_photos/acm_general/brooke-lane.jpg
new file mode 100755
index 00000000..cde96cf7
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_general/brooke-lane.jpg differ
diff --git a/apps/web/public/img/officer_photos/acm_general/eric-lee.jpg b/apps/web/public/img/officer_photos/acm_general/eric-lee.jpg
new file mode 100755
index 00000000..a859da50
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_general/eric-lee.jpg differ
diff --git a/apps/web/public/img/officer_photos/acm_general/james-haddock.jpg b/apps/web/public/img/officer_photos/acm_general/james-haddock.jpg
new file mode 100755
index 00000000..4076bc1b
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_general/james-haddock.jpg differ
diff --git a/apps/web/public/img/officer_photos/acm_general/jonathan-dekoning.png b/apps/web/public/img/officer_photos/acm_general/jonathan-dekoning.png
new file mode 100755
index 00000000..f066b26c
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_general/jonathan-dekoning.png differ
diff --git a/apps/web/public/img/officer_photos/acm_general/josh-silva.jpg b/apps/web/public/img/officer_photos/acm_general/josh-silva.jpg
new file mode 100755
index 00000000..111b001e
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_general/josh-silva.jpg differ
diff --git a/apps/web/public/img/officer_photos/acm_general/juleah-cephus.jpg b/apps/web/public/img/officer_photos/acm_general/juleah-cephus.jpg
new file mode 100755
index 00000000..674661cf
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_general/juleah-cephus.jpg differ
diff --git a/apps/web/public/img/officer_photos/acm_general/lyly-vo.jpg b/apps/web/public/img/officer_photos/acm_general/lyly-vo.jpg
new file mode 100755
index 00000000..c07985e6
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_general/lyly-vo.jpg differ
diff --git a/apps/web/public/img/officer_photos/acm_general/miguel-oseguera.jpg b/apps/web/public/img/officer_photos/acm_general/miguel-oseguera.jpg
new file mode 100755
index 00000000..6bd90574
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_general/miguel-oseguera.jpg differ
diff --git a/apps/web/public/img/officer_photos/acm_general/vivian-tran.jpg b/apps/web/public/img/officer_photos/acm_general/vivian-tran.jpg
new file mode 100755
index 00000000..f61f5487
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_general/vivian-tran.jpg differ
diff --git a/apps/web/public/img/officer_photos/acm_w/reese-sylvester.jpg b/apps/web/public/img/officer_photos/acm_w/reese-sylvester.jpg
new file mode 100755
index 00000000..6d3df0f0
Binary files /dev/null and b/apps/web/public/img/officer_photos/acm_w/reese-sylvester.jpg differ
diff --git a/apps/web/public/img/officer_photos/code_quantum/rahul-paul.jpg b/apps/web/public/img/officer_photos/code_quantum/rahul-paul.jpg
new file mode 100755
index 00000000..60e09be9
Binary files /dev/null and b/apps/web/public/img/officer_photos/code_quantum/rahul-paul.jpg differ
diff --git a/apps/web/public/img/officer_photos/coding_in_color/judith-infante.jpg b/apps/web/public/img/officer_photos/coding_in_color/judith-infante.jpg
new file mode 100755
index 00000000..7f0adc4f
Binary files /dev/null and b/apps/web/public/img/officer_photos/coding_in_color/judith-infante.jpg differ
diff --git a/apps/web/public/img/officer_photos/rowdy_creators/yash-verma.png b/apps/web/public/img/officer_photos/rowdy_creators/yash-verma.png
new file mode 100755
index 00000000..50a50a1f
Binary files /dev/null and b/apps/web/public/img/officer_photos/rowdy_creators/yash-verma.png differ
diff --git a/apps/web/public/img/officer_photos/rowdy_cybercon/samantha-vivanco.jpg b/apps/web/public/img/officer_photos/rowdy_cybercon/samantha-vivanco.jpg
new file mode 100755
index 00000000..d16cdbe5
Binary files /dev/null and b/apps/web/public/img/officer_photos/rowdy_cybercon/samantha-vivanco.jpg differ
diff --git a/apps/web/public/img/officer_photos/rowdy_hacks/zander-brysch.jpg b/apps/web/public/img/officer_photos/rowdy_hacks/zander-brysch.jpg
new file mode 100755
index 00000000..4291aa90
Binary files /dev/null and b/apps/web/public/img/officer_photos/rowdy_hacks/zander-brysch.jpg differ
diff --git a/apps/web/public/img/other/map-pin.png b/apps/web/public/img/other/map-pin.png
new file mode 100644
index 00000000..c35925fd
Binary files /dev/null and b/apps/web/public/img/other/map-pin.png differ
diff --git a/apps/web/public/img/photos/bag.png b/apps/web/public/img/photos/bag.png
new file mode 100644
index 00000000..6a8535ee
Binary files /dev/null and b/apps/web/public/img/photos/bag.png differ
diff --git a/apps/web/public/img/photos/birdsup.png b/apps/web/public/img/photos/birdsup.png
new file mode 100644
index 00000000..48a2a3ae
Binary files /dev/null and b/apps/web/public/img/photos/birdsup.png differ
diff --git a/apps/web/public/img/photos/dinochess.jpg b/apps/web/public/img/photos/dinochess.jpg
new file mode 100644
index 00000000..1580235e
Binary files /dev/null and b/apps/web/public/img/photos/dinochess.jpg differ
diff --git a/apps/web/public/img/photos/dinogreen.jpg b/apps/web/public/img/photos/dinogreen.jpg
new file mode 100644
index 00000000..09af6ad6
Binary files /dev/null and b/apps/web/public/img/photos/dinogreen.jpg differ
diff --git a/apps/web/public/img/photos/rhshirt.jpg b/apps/web/public/img/photos/rhshirt.jpg
new file mode 100644
index 00000000..2a134d54
Binary files /dev/null and b/apps/web/public/img/photos/rhshirt.jpg differ
diff --git a/apps/web/public/img/photos/shock.jpg b/apps/web/public/img/photos/shock.jpg
new file mode 100644
index 00000000..b539784d
Binary files /dev/null and b/apps/web/public/img/photos/shock.jpg differ
diff --git a/apps/web/public/img/photos/walrus.jpg b/apps/web/public/img/photos/walrus.jpg
new file mode 100644
index 00000000..6446c9c6
Binary files /dev/null and b/apps/web/public/img/photos/walrus.jpg differ
diff --git a/apps/web/public/img/thumbnails/default.png b/apps/web/public/img/thumbnails/default.png
index 0d982295..f81c9759 100644
Binary files a/apps/web/public/img/thumbnails/default.png and b/apps/web/public/img/thumbnails/default.png differ
diff --git a/apps/web/src/actions/categories.ts b/apps/web/src/actions/categories.ts
deleted file mode 100644
index 4953de5e..00000000
--- a/apps/web/src/actions/categories.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-"use server";
-import { adminAction } from "@/lib/safe-action";
-import { db, eq } from "db";
-import { createEventCategorySchema, eventCategorySchema } from "db/zod";
-import { customAlphabet } from "nanoid";
-import { LOWER_ALPHANUM_CUSTOM_ALPHABET } from "@/lib/constants";
-import c from "config";
-import { revalidatePath } from "next/cache";
-import { eventCategories } from "db/schema";
-import { UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE } from "@/lib/constants";
-import z from "zod";
-
-const deleteEventCategorySchema = z.string().length(c.events.categoryIDLength);
-
-const nanoid = customAlphabet(
- LOWER_ALPHANUM_CUSTOM_ALPHABET,
- c.events.categoryIDLength,
-);
-
-export const createEventCategory = adminAction
- .schema(createEventCategorySchema)
- .action(async ({ parsedInput }) => {
- try {
- await db.insert(eventCategories).values({
- ...parsedInput,
- id: nanoid(),
- });
- } catch (e) {
- ///@ts-expect-error could not find the type of the error and the status code is the next most accurate way of telling an issue
- if (e.code === UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE) {
- return {
- success: false,
- message: "category_exists",
- };
- }
- throw e;
- }
- // revalidatePath("/admin/categories");
- return {
- success: true,
- message: "category_created",
- };
- });
-
-export const updateEventCategory = adminAction
- .schema(eventCategorySchema)
- .action(async ({ parsedInput }) => {
- const { id: categoryID, ...inputs } = parsedInput;
- try {
- await db
- .update(eventCategories)
- .set(inputs)
- .where(eq(eventCategories.id, categoryID));
- } catch (e) {
- ///@ts-expect-error could not find the type of the error and the status code is the next most accurate way of telling an issue
- if (e.code === UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE) {
- return {
- success: false,
- message: "category_exists",
- };
- }
- throw e;
- }
- // revalidatePath("/admin/categories");
- return {
- success: true,
- message: "category_updated",
- };
- });
-
-export const deleteEventCategory = adminAction
- .schema(deleteEventCategorySchema)
- .action(async ({ parsedInput: categoryID }) => {
- await db
- .delete(eventCategories)
- .where(eq(eventCategories.id, categoryID));
- // revalidatePath("/admin/categories");
- return {
- success: true,
- message: "category_deleted",
- };
- });
diff --git a/apps/web/src/actions/checkin.ts b/apps/web/src/actions/checkin.ts
deleted file mode 100644
index 5e4edf1b..00000000
--- a/apps/web/src/actions/checkin.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-"use server";
-
-import { userAction, adminAction } from "@/lib/safe-action";
-import { userCheckinSchemaFormified } from "db/zod";
-import { UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE } from "@/lib/constants/";
-import { checkInUserClient, checkInUserList } from "@/lib/queries/checkins";
-import { adminCheckinSchema, universityIDSplitter } from "db/zod";
-import { CheckinResult } from "@/lib/types/events";
-import { revalidatePath } from "next/cache";
-import { headers } from "next/headers";
-
-const { ALREADY_CHECKED_IN, SUCCESS, FAILED, SOME_FAILED } = CheckinResult;
-
-export const checkInUserAction = userAction
- .schema(userCheckinSchemaFormified)
- .action(async ({ parsedInput }) => {
- try {
- await checkInUserClient(parsedInput);
- } catch (e) {
- ///@ts-expect-error could not find the type of the error and the status code is the next most accurate way of telling an issue
- if (e.code === UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE) {
- return {
- success: false,
- code: ALREADY_CHECKED_IN,
- };
- }
- throw e;
- }
- return {
- success: true,
- code: SUCCESS,
- };
- });
-
-export const adminCheckin = adminAction
- .schema(adminCheckinSchema)
- .action(async ({ ctx, parsedInput }) => {
- const { universityIDs, eventID } = parsedInput;
- const { userID: adminID } = ctx;
- try {
- const currentPath = headers().get("referer") ?? "";
-
- const idList = universityIDSplitter.parse(universityIDs);
- const failedIDs = await checkInUserList(eventID, idList, adminID);
-
- // revalidatePath(currentPath);
- if (failedIDs.length == 0) {
- return {
- success: true,
- code: SUCCESS,
- };
- } else if (failedIDs.length < idList.length) {
- return {
- success: false,
- code: SOME_FAILED,
- failedIDs,
- };
- } else if (failedIDs.length == idList.length) {
- return {
- success: false,
- code: FAILED,
- };
- }
- return {
- success: false,
- code: FAILED,
- };
- } catch (e) {
- return {
- success: false,
- code: FAILED,
- };
- }
- });
diff --git a/apps/web/src/actions/events/createNewEvent.ts b/apps/web/src/actions/events/createNewEvent.ts
deleted file mode 100644
index f8518f39..00000000
--- a/apps/web/src/actions/events/createNewEvent.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-"use server";
-
-import { db } from "db";
-import { customAlphabet } from "nanoid";
-import { insertEventSchemaFormified } from "db/zod";
-import { adminAction } from "@/lib/safe-action";
-import { events, eventsToCategories } from "db/schema";
-import c from "config";
-import { revalidatePath } from "next/cache";
-import { LOWER_ALPHANUM_CUSTOM_ALPHABET } from "@/lib/constants";
-
-const nanoid = customAlphabet(
- LOWER_ALPHANUM_CUSTOM_ALPHABET,
- c.events.idLength,
-);
-
-export const createEvent = adminAction
- .schema(insertEventSchemaFormified)
- .action(async ({ parsedInput }) => {
- let res = {
- success: true,
- code: "success",
- eventID: "",
- };
- const { categories, ...e } = parsedInput;
- await db.transaction(async (tx) => {
- const eventIDs = await tx
- .insert(events)
- .values({ ...e, id: nanoid() })
- .returning({ eventID: events.id });
-
- if (eventIDs.length === 0) {
- res = {
- success: false,
- code: "insert_event_failed",
- eventID: "",
- };
- tx.rollback();
- return;
- }
-
- const { eventID } = eventIDs[0];
-
- const vals = categories.map((cat: string) => ({
- eventID,
- categoryID: cat,
- }));
-
- const events_to_categories = await tx
- .insert(eventsToCategories)
- .values(vals)
- .returning();
-
- if (events_to_categories.length === 0) {
- res = {
- success: false,
- code: "events_to_categories_failed",
- eventID: "",
- };
- tx.rollback();
- return;
- }
-
- res.eventID = eventID;
- });
- // revalidatePath("/admin/events");
- // revalidatePath("/events");
-
- return res;
- });
diff --git a/apps/web/src/actions/events/delete.ts b/apps/web/src/actions/events/delete.ts
deleted file mode 100644
index 130a8fb1..00000000
--- a/apps/web/src/actions/events/delete.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-"use server";
-import { adminAction } from "@/lib/safe-action";
-import { db, eq } from "db";
-import { deleteEventSchema } from "db/zod";
-import { events } from "db/schema";
-import { revalidatePath } from "next/cache";
-import { redirect } from "next/navigation";
-
-export const deleteEventAction = adminAction
- .schema(deleteEventSchema)
- .action(async ({ parsedInput: id }) => {
- await db.delete(events).where(eq(events.id, id));
- // redirect("/admin/events");
- });
diff --git a/apps/web/src/actions/events/update.ts b/apps/web/src/actions/events/update.ts
deleted file mode 100644
index 78e52c34..00000000
--- a/apps/web/src/actions/events/update.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-"use server";
-
-import { and, db, eq, inArray, sql } from "db";
-import { updateEventSchema } from "db/zod";
-import { adminAction } from "@/lib/safe-action";
-import { events, eventsToCategories } from "db/schema";
-
-export const updateEvent = adminAction
- .schema(updateEventSchema)
- .action(async ({ parsedInput }) => {
- let res = {
- success: true,
- code: "success",
- };
- const { eventID, oldCategories, categories, ...e } = parsedInput;
- await db.transaction(async (tx) => {
- const ids = await tx
- .update(events)
- .set({ ...e })
- .where(eq(events.id, eventID))
- .returning({ eventID: events.id });
-
- if (ids.length === 0) {
- res = {
- success: false,
- code: "update_event_failed",
- };
- tx.rollback();
- }
-
- //find new categories
- const newCategories: string[] = categories.filter(
- (item: string) => !oldCategories.includes(item),
- );
-
- //find deleting categories
- const deletingCategories: string[] = oldCategories.filter(
- (item: string) => !categories.includes(item),
- );
-
- const insertVal = newCategories.map((cat) => ({
- eventID,
- categoryID: cat,
- }));
-
- if (insertVal.length !== 0) {
- await tx.insert(eventsToCategories).values(insertVal);
- }
-
- await tx
- .delete(eventsToCategories)
- .where(
- and(
- inArray(
- eventsToCategories.categoryID,
- deletingCategories,
- ),
- eq(eventsToCategories.eventID, eventID),
- ),
- );
- });
- // VACUUM is handled by Libsql according to: https://discord.com/channels/933071162680958986/1200296371484368956
- // await db.run(sql`PRAGMA VACUUM`);
-
- return res;
- });
diff --git a/apps/web/src/actions/member.ts b/apps/web/src/actions/member.ts
deleted file mode 100644
index 566678d7..00000000
--- a/apps/web/src/actions/member.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-"use server";
-import { executiveAction } from "@/lib/safe-action";
-import z from "zod";
-import { db, eq } from "db";
-import { headers } from "next/headers";
-import { revalidatePath } from "next/cache";
-import { users } from "db/schema";
-import c from "config";
-
-export const updateMemberRole = executiveAction
- .schema(
- z.object({
- userID: z.number().positive(),
- role: z.enum(c.memberRoles),
- }),
- )
- .action(async ({ parsedInput }) => {
- const link = headers().get("referer") ?? "";
- const { userID, role } = parsedInput;
- await db
- .update(users)
- .set({
- role,
- })
- .where(eq(users.userID, userID));
-
- // check if we need to do this
- // revalidatePath(link);
-
- return {
- success: true,
- };
- });
diff --git a/apps/web/src/actions/register/migrate.ts b/apps/web/src/actions/register/migrate.ts
deleted file mode 100644
index fe30b2af..00000000
--- a/apps/web/src/actions/register/migrate.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-"use server";
-
-import { authenticatedAction } from "@/lib/safe-action";
-import { z } from "zod";
-import { db } from "db";
-import { and, eq, isNull } from "db/drizzle";
-import { users, data } from "db/schema";
-import { currentUser } from "@clerk/nextjs/server";
-import c from "config";
-
-export const doPortalLookupCheck = authenticatedAction
- .schema(
- z.object({ universityID: z.string().min(1), email: z.string().min(1) }),
- )
- .action(async ({ parsedInput: { email, universityID } }) => {
- const lookup = await db
- .select()
- .from(users)
- .where(
- and(
- eq(users.email, email.toLowerCase()),
- isNull(users.clerkID),
- eq(users.universityID, universityID.toLowerCase()),
- ),
- )
- .limit(1);
-
- if (lookup[0]) {
- return {
- success: true,
- name: lookup[0].firstName + " " + lookup[0].lastName,
- };
- } else {
- return {
- success: false,
- name: null,
- };
- }
- });
-
-export const doPortalLink = authenticatedAction
- .schema(
- z.object({ universityID: z.string().min(1), email: z.string().min(1) }),
- )
- .action(
- async ({ ctx: { clerkID }, parsedInput: { email, universityID } }) => {
- const lookup = await db
- .select()
- .from(users)
- .where(
- and(
- eq(users.email, email.toLowerCase()),
- isNull(users.clerkID),
- eq(users.universityID, universityID.toLowerCase()),
- ),
- )
- .limit(1);
- const currUser = await currentUser();
- if (!currUser) {
- return {
- success: false,
- };
- }
- const userEmail = currUser.emailAddresses[0].emailAddress;
- if (lookup[0]) {
- await db
- .update(users)
- .set({ clerkID, email: userEmail })
- .where(eq(users.userID, lookup[0].userID));
- return {
- success: true,
- };
- } else {
- return {
- success: false,
- };
- }
- },
- );
diff --git a/apps/web/src/actions/register/new.ts b/apps/web/src/actions/register/new.ts
deleted file mode 100644
index 83aba7c3..00000000
--- a/apps/web/src/actions/register/new.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-"use server";
-
-import { authenticatedAction } from "@/lib/safe-action";
-import { insertUserWithDataSchemaFormified } from "db/zod";
-import { db } from "db";
-import { eq, or } from "db/drizzle";
-import { users, data } from "db/schema";
-
-export const createRegistration = authenticatedAction
- .schema(insertUserWithDataSchemaFormified)
- .action(async ({ parsedInput: registerFormInputs, ctx: { clerkID } }) => {
- const { data: dataSchemaInputs, ...usersSchemaInputs } =
- registerFormInputs;
- const lowerCasedEmail = registerFormInputs.email.toLowerCase();
- const lowerCasedUniversityID =
- registerFormInputs.universityID.toLowerCase();
- const existingUser = await db
- .select()
- .from(users)
- .where(
- or(
- eq(users.email, lowerCasedEmail),
- eq(users.clerkID, clerkID),
- eq(users.universityID, lowerCasedUniversityID),
- ),
- )
- .limit(1);
-
- if (existingUser.length > 0) {
- const foundUser = existingUser[0];
- if (foundUser.clerkID == clerkID) {
- return {
- success: false,
- code: "user_already_exists",
- };
- } else if (foundUser.email == lowerCasedEmail) {
- return {
- success: false,
- code: "email_already_exists",
- };
- } else if (foundUser.universityID == lowerCasedUniversityID) {
- return {
- success: false,
- code: "university_id_already_exists",
- };
- }
- }
-
- await db.transaction(async (tx) => {
- const res = await tx
- .insert(users)
- .values({
- ...usersSchemaInputs,
- email: lowerCasedEmail,
- universityID: lowerCasedUniversityID,
- clerkID,
- })
- .returning({ userID: users.userID });
- const userID = res[0].userID;
- await tx.insert(data).values({
- ...dataSchemaInputs,
- userID,
- // this is a placeholder for now. This isn't super important
- interestedEventTypes: [],
- });
- });
-
- return {
- success: true,
- code: "success",
- };
- });
diff --git a/apps/web/src/actions/semester.ts b/apps/web/src/actions/semester.ts
deleted file mode 100644
index 771cc861..00000000
--- a/apps/web/src/actions/semester.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-"use server";
-import { db, eq } from "db";
-import { semesters } from "db/schema";
-import { executiveAction } from "@/lib/safe-action";
-import {
- createSemesterSchema,
- toggleCurrentSemesterSchema,
- updateSemesterSchema,
-} from "db/zod";
-import { UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE } from "@/lib/constants";
-import {
- SEMESTER_DATE_RANGE_EXISTS,
- SEMESTER_NAME_EXISTS,
-} from "@/lib/constants/semesters";
-import {
- resetCurrentSemesters,
- getExistingSemester,
-} from "@/lib/queries/semesters";
-import z from "zod";
-import { revalidatePath } from "next/cache";
-
-export const createNewSemester = executiveAction
- .schema(createSemesterSchema)
- .action(async ({ parsedInput }) => {
- const { startDate, endDate } = parsedInput;
-
- const existing = await getExistingSemester(startDate, endDate);
-
- if (existing) {
- return {
- success: false,
- code: SEMESTER_DATE_RANGE_EXISTS,
- semesterName: existing.name,
- };
- }
-
- try {
- const res = await db
- .insert(semesters)
- .values(parsedInput)
- .returning({ semesterID: semesters.semesterID });
- if (parsedInput.isCurrent) {
- await resetCurrentSemesters(res[0].semesterID);
- }
- } catch (e) {
- /// @ts-expect-error could not find the type of the error and the status code is the next most accurate way of telling an issue
- if (e.code === UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE) {
- console.log(e);
- return {
- success: false,
- code: SEMESTER_NAME_EXISTS,
- semesterName: parsedInput.name,
- };
- }
- console.log(e);
- throw e;
- }
- // revalidatePath("/admin/semesters");
- return {
- success: true,
- };
- });
-
-export const updateSemester = executiveAction
- .schema(updateSemesterSchema)
- .action(async ({ parsedInput: dbInputs }) => {
- const { startDate, endDate, semesterID } = dbInputs;
- const existing = await getExistingSemester(startDate, endDate);
- if (existing && existing.semesterID !== semesterID) {
- return {
- success: false,
- code: SEMESTER_DATE_RANGE_EXISTS,
- semesterName: existing.name,
- };
- }
-
- try {
- await db
- .update(semesters)
- .set(dbInputs)
- .where(eq(semesters.semesterID, dbInputs.semesterID));
- if (dbInputs.isCurrent) {
- await resetCurrentSemesters(semesterID);
- }
- } catch (e) {
- /// @ts-expect-error could not find the type of the error and the status code is the next most accurate way of telling an issue
- if (e.code === UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE) {
- return {
- success: false,
- code: SEMESTER_NAME_EXISTS,
- semesterName: dbInputs.name,
- };
- }
- throw e;
- }
- // revalidatePath("/admin/semesters");
- return {
- success: true,
- };
- });
-
-export const toggleCurrentSemester = executiveAction
- .schema(toggleCurrentSemesterSchema)
- .action(async ({ parsedInput }) => {
- const { semesterID, isCurrent } = parsedInput;
- if (isCurrent) {
- await resetCurrentSemesters(semesterID);
- }
- await db
- .update(semesters)
- .set({
- isCurrent,
- })
- .where(eq(semesters.semesterID, semesterID));
-
- // revalidatePath("/admin/semesters");
-
- return {
- success: true,
- };
- });
-
-export const deleteSemester = executiveAction
- .schema(z.number().int())
- .action(async ({ parsedInput: semesterID }) => {
- await db.delete(semesters).where(eq(semesters.semesterID, semesterID));
- // revalidatePath("/admin/semesters");
- return {
- success: true,
- };
- });
diff --git a/apps/web/src/actions/settings/edit.ts b/apps/web/src/actions/settings/edit.ts
deleted file mode 100644
index 8e02e9e4..00000000
--- a/apps/web/src/actions/settings/edit.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-"use server";
-
-import { userAction } from "@/lib/safe-action";
-import { db, eq } from "db";
-import { data, users } from "db/schema";
-import { revalidatePath } from "next/cache";
-import { del } from "@/lib/server/file-upload";
-import {
- editAccountSettingsSchema,
- editAcademicSettingsSchema,
- editClubSettingsSchema,
- editResumeActionSchema,
-} from "@/validators/settings";
-
-export const editAccountSettings = userAction
- .schema(editAccountSettingsSchema)
- .action(
- async ({
- parsedInput: { firstName, lastName, ethnicity, gender, birthday },
- ctx: { userID, clerkID },
- }) => {
- try {
- await db.transaction(async (tx) => {
- await tx
- .update(users)
- .set({ firstName, lastName })
- .where(eq(users.clerkID, clerkID));
-
- await tx
- .update(data)
- .set({ ethnicity, gender, birthday })
- .where(eq(data.userID, userID));
- });
- } catch (error) {
- console.error(error);
- return {
- success: false,
- error: "Failed to save account settings",
- };
- }
-
- // revalidatePath("/settings");
- return { success: true };
- },
- );
-
-export const editAcademicSettings = userAction
- .schema(editAcademicSettingsSchema)
- .action(
- async ({
- parsedInput: {
- major,
- classification,
- graduationYear,
- graduationMonth,
- },
- ctx: { userID },
- }) => {
- try {
- await db
- .update(data)
- .set({
- major,
- classification,
- graduationYear,
- graduationMonth,
- })
- .where(eq(data.userID, userID));
- } catch (error) {
- console.error(error);
- return {
- success: false,
- error: "Failed to save academic settings",
- };
- }
-
- // revalidatePath("/settings");
- return { success: true };
- },
- );
-
-export const editResumeUrl = userAction
- .schema(editResumeActionSchema)
- .action(async ({ ctx: { userID }, parsedInput: { resume, oldResume } }) => {
- try {
- await db
- .update(data)
- .set({ resume })
- .where(eq(data.userID, userID));
-
- if (oldResume) await del(oldResume);
-
- // revalidatePath("/settings");
- return { success: true };
- } catch (error) {
- // Failed to update user data to new resume. Delete the new resume from the blob and make the user try again.
- console.error(error);
- await del(resume);
- return {
- success: false,
- error: "Failed to finalize resume upload.",
- };
- }
- });
-
-export const editClubSettings = userAction
- .schema(editClubSettingsSchema)
- .action(
- async ({ ctx: { userID }, parsedInput: { shirtSize, shirtType } }) => {
- try {
- await db
- .update(data)
- .set({ shirtSize, shirtType })
- .where(eq(data.userID, userID));
- } catch (error) {
- console.error(error);
- return {
- success: false,
- error: "Failed to update user settings",
- };
- }
-
- // revalidatePath("/settings");
- return { success: true };
- },
- );
diff --git a/apps/web/src/app/(marketing)/client.tsx b/apps/web/src/app/(marketing)/client.tsx
new file mode 100644
index 00000000..3bb2125b
--- /dev/null
+++ b/apps/web/src/app/(marketing)/client.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import Image from "next/image";
+import React, { useState, useEffect } from "react";
+import { motion, AnimatePresence } from "motion/react";
+
+export function WeArePhotoGrid() {
+ // Ensure we have at least 4 photos for the grid, can be more for cycling
+ const photos = [
+ "bag.png",
+ "birdsup.png",
+ "dinochess.jpg",
+ "shock.jpg",
+ "walrus.jpg",
+ "dinogreen.jpg",
+ "rhshirt.jpg",
+ ];
+ const photoPrefix = "/img/photos/";
+ const totalPhotos = photos.length;
+ const gridSize = 4; // Still conceptually 4 items
+ const idleSeconds = 6;
+
+ // State holds the index of the photo currently displayed in each grid cell
+ const [currentIndices, setCurrentIndices] = useState(() => {
+ // Initialize with unique non-repeating indices
+ const initialIndices: number[] = [];
+ for (let i = 0; i < gridSize; i++) {
+ let nextIndex = i % totalPhotos;
+ // Check for duplicates
+ while (initialIndices.includes(nextIndex)) {
+ nextIndex = (nextIndex + 1) % totalPhotos;
+ }
+ initialIndices.push(nextIndex);
+ }
+ return initialIndices;
+ });
+
+ // Animation variants for different directions
+ const variants = [
+ { initial: { y: "100%" }, exit: { x: "100%" } }, // Cell0 (top-left): in from bottom, exit to right
+ { initial: { x: "-100%" }, exit: { y: "100%" } }, // Cell1 (top-right): in from left, exit downward
+ { initial: { x: "100%" }, exit: { y: "-100%" } }, // Cell2 (bottom-left): in from right, exit upward
+ { initial: { y: "-100%" }, exit: { x: "-100%" } }, // Cell3 (bottom-right): in from top, exit to left
+ ];
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentIndices((prevIndices) => {
+ // Start with the basic rotation pattern
+ const nextIndices = [
+ (prevIndices[2] + 1) % totalPhotos, // cell0 gets next photo after bottom-left
+ (prevIndices[0] + 1) % totalPhotos, // cell1 gets next photo after top-left
+ (prevIndices[3] + 1) % totalPhotos, // cell2 gets next photo after bottom-right
+ (prevIndices[1] + 1) % totalPhotos, // cell3 gets next photo after top-right
+ ];
+
+ // Ensure no duplicates and no unmoved photos
+ for (let i = 0; i < gridSize; i++) {
+ // Fix duplicate photos
+ for (let j = 0; j < i; j++) {
+ if (nextIndices[i] === nextIndices[j]) {
+ nextIndices[i] = (nextIndices[i] + 1) % totalPhotos;
+ // Check again for duplicates after incrementing
+ j = -1; // Restart the inner loop
+ }
+ }
+
+ // Fix unmoved photos
+ if (nextIndices[i] === prevIndices[i]) {
+ nextIndices[i] = (nextIndices[i] + 1) % totalPhotos;
+
+ // After changing, check for duplicates again
+ for (let j = 0; j < i; j++) {
+ if (nextIndices[i] === nextIndices[j]) {
+ nextIndices[i] =
+ (nextIndices[i] + 1) % totalPhotos;
+ j = -1; // Restart the inner loop
+ }
+ }
+ }
+ }
+
+ return nextIndices;
+ });
+ }, idleSeconds * 1000); // Change image every 3 seconds
+
+ // Clear interval on component unmount
+ return () => clearInterval(interval);
+ }, []);
+
+ // Return a fragment containing the four individual animated divs
+ return (
+
+ {[0, 1, 2, 3].map((i) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/web/src/app/(marketing)/layout.tsx b/apps/web/src/app/(marketing)/layout.tsx
new file mode 100644
index 00000000..070883e7
--- /dev/null
+++ b/apps/web/src/app/(marketing)/layout.tsx
@@ -0,0 +1,15 @@
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "ACM UTSA",
+ description:
+ "The premiere organization for students interested in computer science and technology at UTSA",
+};
+
+export default function MarketingLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return <>{children}>;
+}
diff --git a/apps/web/src/app/(marketing)/page.tsx b/apps/web/src/app/(marketing)/page.tsx
new file mode 100644
index 00000000..6ef51f55
--- /dev/null
+++ b/apps/web/src/app/(marketing)/page.tsx
@@ -0,0 +1,440 @@
+import Globe from "@/components/landing/globe";
+import { HeroNav } from "@/components/shared/navbar";
+import Image from "next/image";
+import { WeArePhotoGrid } from "./client";
+import { Suspense, cloneElement, Fragment } from "react";
+import {
+ Code as CodeIcon,
+ Plus as PlusIcon,
+ Users as UsersIcon,
+ Equal as EqualIcon,
+ Braces as BracesIcon,
+ Zap as ZapIcon,
+ Code2 as Code2Icon,
+ Users2 as Users2Icon,
+ Lightbulb as LightbulbIcon,
+ Share2 as Share2Icon,
+ Scaling as ScalingIcon,
+ Laptop as LaptopIcon,
+ Network as NetworkIcon,
+ Building as BuildingIcon,
+ Map as MapIcon,
+ MapPin,
+ Calendar,
+ HeartHandshake,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+import Footer from "@/components/shared/footer";
+// import { getNextEvent, getUpcomingEvents } from "@/lib/queries/events";
+import { SPONSORS } from "@/site.config";
+import { EventType } from "@/lib/types/events";
+
+export default function Page() {
+ return (
+ <>
+
+
+
+
+ acm utsa
+
+
+ The premiere organization for students interested in
+ computer science and technology at UTSA
+
+
+
+
+
+
+
+
+
+
+
+
+ We are the Association for Computing Machinery
+ at{" "}
+
+ UTSA
+
+
+
+
+
Loading... }>
+
+
+
+
+
+
+
+
+ Mission
+
+
+ At
+
+ }
+ >
+ ACM
+
+ , our mission is to empower students passionate
+ about technology and
+ }>
+ computer science
+
+ by fostering a vibrant
+ }>community
+ that supports innovation, collaboration, and
+ growth.
+
+
+ Fig. 2
+
+
+
+
+
+
+
1000+
+
Members
+
+ Fig. 3
+
+
+
+
4
+
+ Sub-organizations
+
+
+ Fig. 4
+
+
+
+
3
+
+ Annual Hackathons
+
+
+ Fig. 5
+
+
+
+
+
+
+ acm is for{" "}
+ everyone.
+
+
+ {"Membership is 100% free, and always will be :)"}
+
+
+
Fig. 6
+
+ Become a Member {">"}
+
+
+
+
+
+
+ Not sure yet or just want to learn more? Read more
+ below!
+
+
+
+
+
+
+ Suborgs
+
+
+ Along with core ACM Events, we are home to
+ numerous sub-organizations which focus on
+ specific areas of interest and communities.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hackathons
+
+
+ ACM hosts 3 annual hackathons, welcoming
+ students of all skill levels from across the
+ country to innovate and build.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sponsors
+
+
+ We are able to operate at no cost to our members
+ through the generous support of our sponsors
+
+
+ {SPONSORS.map((sponsor, index) => (
+
0 ? "border-l-2" : ""
+ }`}
+ >
+
+
+ ))}
+
+
+
+
+ Become a Sponsor
+
+
+
+
+
+
+
+ >
+ );
+}
+
+async function UpcomingEvents() {
+ // const event = await getNextEvent(true);
+ const event: { type: string; event: EventType } = {
+ type: "future",
+ event: {
+ id: "1",
+ name: "ACM Spring Kickoff",
+ description:
+ "Join us for our first meeting of the Spring 2025 semester! We'll be discussing our upcoming events, workshops, and hackathons. Come meet the team and learn how you can get involved.",
+ thumbnailUrl:
+ "https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3",
+ start: new Date("2025-01-22T18:00:00"),
+ end: new Date("2025-01-22T19:00:00"),
+ checkinStart: new Date("2025-01-22T17:45:00"),
+ checkinEnd: new Date("2025-01-22T19:15:00"),
+ location: "NPB 1.202",
+ isUserCheckinable: true,
+ isHidden: false,
+ points: 10,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ semesterID: 1,
+ },
+ };
+ const timezone = "America/Chicago";
+
+ if (!event) {
+ return (
+
+
There Was An Error Fetching Events
+
+ );
+ }
+
+ return (
+
+
+
+ {event.type === "future" ? "Up Next" : "Recently"}
+ @ ACM
+
+
+
+
+
+
+
+ {event.event.name}
+
+
+
+
+ {`${event.event.start.toLocaleString("en-US", {
+ timeZone: timezone,
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ })} CT`}
+
+
+
+ {event.event.location}
+
+
+
+
+
+ View Event
+
+
+
+
+ Explore Other Events {">"}
+
+
+
+
+
+ );
+}
+
+function Pill({
+ icon,
+ children,
+}: {
+ icon: React.ReactNode;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {" "}
+
+ {cloneElement(icon as React.ReactElement, { size: 20 })}
+ {children}
+ {" "}
+
+ );
+}
diff --git a/apps/web/src/app/(marketing)/suborgs/[slug]/page.tsx b/apps/web/src/app/(marketing)/suborgs/[slug]/page.tsx
new file mode 100644
index 00000000..7084406b
--- /dev/null
+++ b/apps/web/src/app/(marketing)/suborgs/[slug]/page.tsx
@@ -0,0 +1,30 @@
+import { SUBORGS } from "@/site.config";
+import SuborgHero from "./suborg-hero";
+import { HeroNav } from "@/components/shared/navbar";
+import { notFound } from "next/navigation";
+
+export default function Page({ params }: { params: { slug: string } }) {
+ const suborg = SUBORGS[params.slug];
+
+ if (!suborg) {
+ return notFound();
+ }
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/(marketing)/suborgs/[slug]/suborg-hero.tsx b/apps/web/src/app/(marketing)/suborgs/[slug]/suborg-hero.tsx
new file mode 100644
index 00000000..f46e6fe9
--- /dev/null
+++ b/apps/web/src/app/(marketing)/suborgs/[slug]/suborg-hero.tsx
@@ -0,0 +1,70 @@
+import Image from "next/image";
+import type { RGBColor, Suborg } from "@/site.config";
+
+function modifyColor(color: RGBColor, alpha: number): string {
+ const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+ if (!match) return color;
+
+ const [, r, g, b] = match;
+ return `rgb(${r} ${g} ${b} / ${alpha})`;
+}
+
+export default function SuborgHero(suborg: Suborg) {
+ const { name, shortDesc, logoUrl, colors, leadingSentence } = suborg;
+
+ return (
+
+
+
+ {name.toLowerCase()}
+
+
+ {shortDesc}
+
+
+
+
+ {leadingSentence}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(marketing)/team/page.tsx b/apps/web/src/app/(marketing)/team/page.tsx
new file mode 100644
index 00000000..23ba88b4
--- /dev/null
+++ b/apps/web/src/app/(marketing)/team/page.tsx
@@ -0,0 +1,26 @@
+import { HeroNav } from "@/components/shared/navbar";
+import MeetTheTeamClient from "@/components/team/meet-the-team.client";
+
+export default function TeamPage() {
+ return (
+ <>
+
+
+
+
+
+ meet our team.
+
+
+
+ Browse the people behind ACM UTSA, our sub-organizations, and our major
+ events.
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/app/admin/categories/columns.tsx b/apps/web/src/app/admin/categories/columns.tsx
deleted file mode 100644
index e06a781c..00000000
--- a/apps/web/src/app/admin/categories/columns.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-"use client";
-import { ColumnDef } from "@tanstack/react-table";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { MoreHorizontal } from "lucide-react";
-import { EventCategoryType } from "@/lib/types/events";
-import { DataTableColumnHeader } from "@/components/ui/data-table";
-import React, { useState, useEffect } from "react";
-import { Dialog } from "@radix-ui/react-dialog";
-import { Button } from "@/components/ui/button";
-import {
- AlertDialog,
- AlertDialogContent,
- AlertDialogTrigger,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogCancel,
- AlertDialogAction,
-} from "@/components/ui/alert-dialog";
-import EditCategory from "@/components/dash/admin/categories/EditCategoryDialogue";
-import DeleteCategoryDialogue from "@/components/dash/admin/categories/DeleteCategoryDialogue";
-import { DialogTrigger } from "@/components/ui/dialog";
-import { toast } from "sonner";
-
-export const eventCategoryColumns: ColumnDef[] = [
- {
- accessorKey: "id",
- header: "ID",
- },
- {
- accessorKey: "name",
- header: ({ column }) => {
- return ;
- },
- enableSorting: true,
- },
- {
- accessorKey: "color",
- header: ({ column }) => {
- return ;
- },
- cell: ({ row }) => {
- return (
-
-
-
- {row.getValue("color")}
-
-
- );
- },
- enableSorting: true,
- },
- {
- id: "actions",
- enablePinning: true,
- header: ({}) => {},
- cell: ({ row }) => {
- const [open, setOpen] = useState(false);
-
- const data = row.original;
- return (
-
-
-
-
-
- Open menu
-
-
-
-
-
- toast.promise(
- navigator.clipboard.writeText(
- data.id,
- ),
- {
- loading: "Copying ID...",
- success: "ID copied",
- error: "Failed to copy ID",
- },
- )
- }
- >
- Copy ID
-
-
- toast.promise(
- navigator.clipboard.writeText(
- data.color,
- ),
- {
- loading: "Copying color...",
- success: "Color copied",
- error: "Failed to copy color",
- },
- )
- }
- >
- Copy color
-
-
-
-
- Edit
-
-
-
-
- Delete
-
-
-
-
-
-
-
-
- );
- },
- },
-];
diff --git a/apps/web/src/app/admin/categories/page.tsx b/apps/web/src/app/admin/categories/page.tsx
deleted file mode 100644
index a706d16d..00000000
--- a/apps/web/src/app/admin/categories/page.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Suspense } from "react";
-import AdminCategoryView from "@/components/dash/admin/categories/CategoryView";
-export default function Page() {
- return (
-
-
-
- Categories
-
-
-
Grabbing checkin stats. One sec... }>
-
-
-
- );
-}
diff --git a/apps/web/src/app/admin/checkins/page.tsx b/apps/web/src/app/admin/checkins/page.tsx
deleted file mode 100644
index cd2ac801..00000000
--- a/apps/web/src/app/admin/checkins/page.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Suspense } from "react";
-import { UserRoundPlus } from "lucide-react";
-import AddCheckinDialogue from "@/components/dash/shared/AddCheckinDialogue";
-import CheckinsStatsSheet from "@/components/dash/shared/CheckinStatsSheet";
-import { Button } from "@/components/ui/button";
-import AdminCheckinLog from "@/components/dash/shared/AdminCheckinLog";
-import { getEventList } from "@/lib/queries/events";
-import { Dialog, DialogTrigger } from "@/components/ui/dialog";
-
-export default async function Page() {
- const eventList = await getEventList();
- return (
-
-
-
- Checkins
-
-
-
- Grabbing checkin stats. One sec...
}
- >
-
-
-
-
-
-
-
- Add Checkin
-
-
-
-
-
-
- {/* {events?.[0].name}
*/}
-
-
- Grabbing checkin log. One sec...
}
- >
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/admin/events/[id]/edit/loading.tsx b/apps/web/src/app/admin/events/[id]/edit/loading.tsx
deleted file mode 100644
index 50c6a60b..00000000
--- a/apps/web/src/app/admin/events/[id]/edit/loading.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from "react";
-
-function loading() {
- return Loading your form. One sec...
;
-}
-
-export default loading;
diff --git a/apps/web/src/app/admin/events/[id]/edit/page.tsx b/apps/web/src/app/admin/events/[id]/edit/page.tsx
deleted file mode 100644
index f1a75b1a..00000000
--- a/apps/web/src/app/admin/events/[id]/edit/page.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from "react";
-
-import EditEventForm from "@/components/dash/admin/events/EditEventForm";
-
-import { getAllCategoriesKeyValue } from "@/lib/queries/categories";
-import { iEvent, uEvent } from "@/lib/types/events";
-import { getEventWithCategoriesById } from "@/lib/queries/events";
-import { IDParamProp } from "@/lib/types/shared";
-import FullScreenMessage from "@/components/shared/fullscreen-message";
-import c from "config";
-import { getAllSemestersDesc } from "@/lib/queries/semesters";
-export default async function Page({ params: { id } }: IDParamProp) {
- const categoryOptionsAsync = getAllCategoriesKeyValue();
- const oldValuesAsync = getEventWithCategoriesById(id);
- const getAllSemestersDescAsync = getAllSemestersDesc();
- const [categoryOptions, oldValues, semesterOptions] = await Promise.all([
- categoryOptionsAsync,
- oldValuesAsync,
- getAllSemestersDescAsync,
- ]);
- if (oldValues === undefined) {
- return (
-
- );
- }
- return (
-
-
-
- Edit Event
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/admin/events/[id]/page.tsx b/apps/web/src/app/admin/events/[id]/page.tsx
deleted file mode 100644
index 061c8882..00000000
--- a/apps/web/src/app/admin/events/[id]/page.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { getEventById, getEventCheckins } from "@/lib/queries/events";
-
-export default async function Page({ params }: { params: { slug: string } }) {
- const event = await getEventById(params.slug);
- const checkins = await getEventCheckins(params.slug);
-
- return (
-
-
{event?.name}
-
- {checkins.map((checkin) => (
-
- User {checkin.userID} checked in at{" "}
- {checkin.time.toDateString()}
-
- ))}
-
-
- );
-}
diff --git a/apps/web/src/app/admin/events/columns.tsx b/apps/web/src/app/admin/events/columns.tsx
deleted file mode 100644
index 4834be0f..00000000
--- a/apps/web/src/app/admin/events/columns.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-"use client";
-
-import { ColumnDef, Row } from "@tanstack/react-table";
-import Image from "next/image";
-import Link from "next/link";
-import { MoreHorizontal, ArrowUpDown } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { DataTableColumnHeader } from "@/components/ui/data-table";
-import type { EventType } from "@/lib/types/events";
-import { formatDate } from "date-fns";
-import AddCheckinDialogue from "@/components/dash/shared/AddCheckinDialogue";
-import { useEffect, useState } from "react";
-import { Dialog } from "@radix-ui/react-dialog";
-import { DialogTrigger } from "@/components/ui/dialog";
-import DeleteEventDialog from "@/components/dash/admin/events/DeleteEventDialogue";
-import { toast } from "sonner";
-import { usePathname } from "next/navigation";
-import ViewQRCode from "@/components/dash/admin/events/ViewQRCode";
-import { useBasePath } from "@/lib/hooks/useBasePath";
-
-type EventWithCheckins = Partial & {
- checkin_count: number;
- avg_rating: number;
-};
-
-const timeFormatString = "eee, MMM dd yyyy HH:mm bb";
-
-const timeCell =
- (key: string) =>
- ({ row }: { row: Row }) => {
- const formattedDate = formatDate(row.getValue(key), timeFormatString);
- return {formattedDate}
;
- };
-
-export const columns: ColumnDef[] = [
- {
- accessorKey: "id",
- header: "ID",
- cell: ({ row }) => {
- return {row.getValue("id")}
;
- },
- },
- {
- accessorKey: "name",
- header: ({ column }) => {
- return ;
- },
- enableSorting: true,
- },
- {
- accessorKey: "description",
- header: "Description",
- cell: ({ row }) => {
- return (
-
- {row.getValue("description")}
-
- );
- },
- },
- {
- accessorKey: "thumbnailUrl",
- header: "Thumbnail",
- cell: ({ row }) => {
- return (
-
-
-
- );
- },
- },
- {
- accessorKey: "avg_rating",
- header: ({ column }) => {
- return (
-
- );
- },
- cell: ({ row }) => {
- const rating: number | string =
- row.getValue("avg_rating") || "unrated";
- return (
-
- {typeof rating === "string" ? rating : rating.toFixed(2)}
-
- );
- },
- },
- {
- accessorKey: "start",
- header: ({ column }) => {
- return ;
- },
- cell: timeCell("start"),
- },
- {
- accessorKey: "checkin_count",
- header: ({ column }) => {
- return ;
- },
- },
- {
- accessorKey: "updatedAt",
- header: ({ column }) => {
- return (
-
- );
- },
- cell: timeCell("updatedAt"),
- },
- {
- accessorKey: "createdAt",
- header: ({ column }) => {
- return ;
- },
- cell: timeCell("createdAt"),
- },
- {
- id: "actions",
- enablePinning: true,
- cell: ({ row }) => {
- const [showDelete, setShowDelete] = useState(false);
- const [open, setOpen] = useState(false);
- const data = row.original;
- const basePath = useBasePath();
-
- return (
-
-
-
-
- Open menu
-
-
-
-
-
-
- View
-
-
-
-
-
-
- {
- e.stopPropagation();
- toast.promise(
- navigator.clipboard.writeText(
- `${basePath}/events/${data.id}`,
- ),
- {
- loading: "Copying...",
- success: () => {
- return "Link copied!";
- },
- error: "Error",
- },
- );
- }}
- >
- Copy link
-
-
-
-
-
- Edit
-
-
-
-
- {
- e.stopPropagation();
- setShowDelete(true);
- }}
- >
- Delete
-
-
-
-
-
- {
- e.stopPropagation();
- setShowDelete(false);
- }}
- >
- Add Checkin
-
-
-
-
-
-
-
- );
- },
- },
-];
-
-function EventColumnActions({
- setOpen,
- showDelete,
- id,
- name,
-}: {
- setOpen: React.Dispatch>;
- showDelete: boolean;
- id: string;
- name: string;
-}) {
- if (showDelete) {
- return ;
- }
- return (
-
- );
-}
diff --git a/apps/web/src/app/admin/events/new/page.tsx b/apps/web/src/app/admin/events/new/page.tsx
deleted file mode 100644
index 6802ca26..00000000
--- a/apps/web/src/app/admin/events/new/page.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import NewEventForm from "@/components/dash/admin/events/NewEventForm";
-import { getAllCategoriesKeyValue } from "@/lib/queries/categories";
-import { getUTCDate } from "@/lib/utils";
-import { getAllSemesters } from "@/lib/queries/semesters";
-import { db, desc } from "db";
-import { semesters } from "db/schema";
-
-export default async function Page() {
- const defaultDate = getUTCDate();
- defaultDate.setSeconds(0);
- const categoryOptionsAsync = getAllCategoriesKeyValue();
- const semesterOptionsAsync = db.query.semesters.findMany({
- orderBy: desc(semesters.isCurrent),
- });
- const [categoryOptions, semesterOptions] = await Promise.all([
- categoryOptionsAsync,
- semesterOptionsAsync,
- ]);
- return (
-
-
-
- New Event
-
-
-
-
-
-
- );
-}
-
-export const runtime = "edge";
diff --git a/apps/web/src/app/admin/events/page.tsx b/apps/web/src/app/admin/events/page.tsx
deleted file mode 100644
index 4d7f33c7..00000000
--- a/apps/web/src/app/admin/events/page.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Suspense } from "react";
-import Link from "next/link";
-import { getEventsWithCheckins } from "@/lib/queries/events";
-import { CalendarPlus } from "lucide-react";
-import { columns } from "./columns";
-
-import { DataTable } from "@/components/ui/data-table";
-import EventStatsSheet from "@/components/dash/admin/events/EventStatsSheet";
-import { Button } from "@/components/ui/button";
-import AdminCheckinLog from "@/components/dash/shared/AdminCheckinLog";
-import { unstable_noStore as noStore } from "next/cache";
-
-async function Page() {
- noStore();
- const events = await getEventsWithCheckins();
- return (
-
-
-
- Events
-
-
-
- Grabbing event stats. One sec...
}
- >
-
-
-
-
-
-
- Create Event
-
-
-
-
-
-
-
-
- );
-}
-
-export const runtime = "edge";
-
-export default Page;
diff --git a/apps/web/src/app/admin/layout.tsx b/apps/web/src/app/admin/layout.tsx
deleted file mode 100644
index 225c4e59..00000000
--- a/apps/web/src/app/admin/layout.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { auth } from "@clerk/nextjs/server";
-import { redirect } from "next/navigation";
-import { db } from "db";
-import { users } from "db/schema";
-import { eq } from "db/drizzle";
-import FullScreenMessage from "@/components/shared/fullscreen-message";
-import Navbar from "@/components/shared/navbar";
-import DashNavItem from "@/components/dash/shared/DashNavItem";
-import ClientToast from "@/components/shared/client-toast";
-import { Suspense } from "react";
-import c from "config";
-
-export default async function AdminLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- const { userId } = await auth();
-
- if (!userId) {
- return redirect("/sign-in");
- }
-
- const user = await db.query.users.findFirst({
- where: eq(users.clerkID, userId),
- });
-
- if (!user || (user.role !== "admin" && user.role !== "super_admin")) {
- console.log("Denying admin access to user", user);
- return (
-
- );
- }
- return (
- <>
-
-
-
- {Object.entries(c.dashPaths.admin).map(([name, path]) => {
- return ;
- })}
-
- Loading...}>{children}
- >
- );
-}
-export const runtime = "edge";
diff --git a/apps/web/src/app/admin/members/[slug]/page.tsx b/apps/web/src/app/admin/members/[slug]/page.tsx
deleted file mode 100644
index fac06a41..00000000
--- a/apps/web/src/app/admin/members/[slug]/page.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { auth, clerkClient } from "@clerk/nextjs/server";
-import { notFound } from "next/navigation";
-import { getAdminUser, getUser } from "@/lib/queries/users";
-import MemberPage from "@/components/dash/admin/members/MemberPage";
-
-export default async function Page({ params }: { params: { slug: string } }) {
- const { userId } = await auth();
-
- if (!userId) return notFound();
-
- const admin = await getAdminUser(userId);
- if (!admin) return notFound();
-
- const user = await getUser(params.slug);
-
- if (!user) {
- return User Not Found
;
- }
-
- let clerkUser = undefined;
- let imageUrl: string | undefined = undefined;
- if (user.clerkID) {
- const client = await clerkClient();
- clerkUser = await client.users.getUser(user.clerkID);
- }
- if (clerkUser) imageUrl = clerkUser.imageUrl;
-
- return (
-
-
-
- );
-}
-
-export const runtime = "edge";
diff --git a/apps/web/src/app/admin/members/columns.tsx b/apps/web/src/app/admin/members/columns.tsx
deleted file mode 100644
index 43d4e5b7..00000000
--- a/apps/web/src/app/admin/members/columns.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-"use client";
-
-import { ColumnDef, Row } from "@tanstack/react-table";
-import { Badge } from "@/components/ui/badge";
-import { DataTableColumnHeader } from "@/components/ui/data-table";
-import { UserWithData } from "db/types";
-import { formatDate } from "date-fns";
-import { Dialog, DialogTrigger, DialogTitle } from "@/components/ui/dialog";
-import { toast } from "sonner";
-import {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
-} from "@/components/ui/dropdown-menu";
-import { Button } from "@/components/ui/button";
-import { MoreHorizontal } from "lucide-react";
-import { useState } from "react";
-import UpdateRoleDialogue from "@/components/dash/shared/UpdateRoleDialogue";
-import Link from "next/link";
-
-const timeFormatString = "eee, MMM dd yyyy HH:mm bb";
-
-const timeCell = ({ row }: { row: Row }) => {
- const formattedDate = formatDate(row.getValue(""), timeFormatString);
- return {formattedDate}
;
-};
-
-export const columns: ColumnDef[] = [
- // {
- // accessorKey: "user.userID",
- // id: "userID",
- // header: "User ID",
- // cell: ({ row }) => {
- // return {row.original.user.userID}
;
- // },
- // },
- {
- id: "name",
- accessorFn: (row) => `${row.user.firstName} ${row.user.lastName}`,
- header: ({ column }) => {
- return ;
- },
- enableSorting: true,
- },
- {
- accessorKey: "user.email",
- id: "email",
- header: ({ column }) => {
- return ;
- },
- },
- {
- accessorKey: "checkin_count",
- id: "checkins",
- header: ({ column }) => {
- return ;
- },
- },
- {
- accessorKey: "data.major",
- header: ({ column }) => {
- return ;
- },
- id: "major",
- },
-
- {
- accessorKey: "user.universityID",
- id: "universityID",
- header: ({ column }) => {
- return ;
- },
- },
- {
- accessorKey: "data.classification",
- id: "classification",
- header: ({ column }) => {
- return (
-
- );
- },
- },
- {
- accessorKey: "data.ethnicity",
- id: "ethnicity",
- header: ({ column }) => {
- return ;
- },
- cell: ({ row }) => {
- return (
-
- {row.original.data.ethnicity.map((e) => (
-
- {e}
-
- ))}
-
- );
- },
- },
- {
- accessorKey: "data.gender",
- id: "gender",
- header: ({ column }) => {
- return ;
- },
- cell: ({ row }) => {
- return (
-
- {row.original.data.gender.map((e) => (
- {e}
- ))}
-
- );
- },
- },
- {
- accessorKey: "user.role",
- id: "interestedEventTypes",
- header: ({ column }) => {
- return ;
- },
- },
- {
- id: "actions",
- enablePinning: true,
- header: ({ column }) => {},
- cell: ({ row }) => {
- const {
- user: { userID, clerkID, email, role },
- } = row.original;
- const [open, setOpen] = useState(false);
- return (
-
-
-
-
- Open menu
-
-
-
-
-
-
- View Member
-
-
-
-
- {
- e.stopPropagation();
- toast.promise(
- navigator.clipboard.writeText(
- clerkID ?? "Not found",
- ),
- {
- loading: "Copying...",
- success: () => {
- return "Link copied!";
- },
- error: "Error",
- },
- );
- }}
- >
- {clerkID ? "Copy Clerk ID" : "No Clerk ID"}
-
-
-
- {
- e.stopPropagation();
- toast.promise(
- navigator.clipboard.writeText(
- userID.toString(),
- ),
- {
- loading: "Copying...",
- success: () => {
- return "Link copied!";
- },
- error: "Error",
- },
- );
- }}
- >
- Copy User ID
-
-
-
-
- Email User
-
-
-
- {
- e.stopPropagation();
- }}
- >
- Change Role
-
-
-
-
-
-
-
- );
- },
- },
-];
diff --git a/apps/web/src/app/admin/members/page.tsx b/apps/web/src/app/admin/members/page.tsx
deleted file mode 100644
index 858076dc..00000000
--- a/apps/web/src/app/admin/members/page.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Suspense } from "react";
-import { getUserWithData } from "@/lib/queries/users";
-import { columns } from "./columns";
-
-import { DataTable } from "@/components/ui/data-table";
-import MemberStatsSheet from "@/components/dash/admin/members/MemberStatsSheet";
-
-async function Page() {
- const members = await getUserWithData();
- return (
-
-
-
- Members
-
-
-
- ...loading
}>
-
-
-
- {/* {events?.[0].name}
*/}
-
-
-
-
- );
-}
-
-export default Page;
diff --git a/apps/web/src/app/admin/page.tsx b/apps/web/src/app/admin/page.tsx
deleted file mode 100644
index dd1bdac8..00000000
--- a/apps/web/src/app/admin/page.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import DemographicsStats from "@/components/dash/admin/overview/DemographicsStats";
-import MonthlyRegistrationChart from "@/components/dash/admin/overview/MonthlyRegistrationChart";
-import { Separator } from "@/components/ui/separator";
-import {
- getRegistrationsByMonth,
- getUserClassifications,
-} from "@/lib/queries/charts";
-import { Suspense } from "react";
-
-export default async function Page() {
- const monthlyRegistrations = await getRegistrationsByMonth();
- const classifications = await getUserClassifications();
- return (
-
-
-
- Trends
-
-
-
-
-
- Retrieving registration chart. One sec...
-
- }
- >
-
-
-
-
-
-
-
- Demographics
-
-
-
-
- Retrieving demographics charts. One sec...
-
- }
- >
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/admin/semesters/columns.tsx b/apps/web/src/app/admin/semesters/columns.tsx
deleted file mode 100644
index ef70ef52..00000000
--- a/apps/web/src/app/admin/semesters/columns.tsx
+++ /dev/null
@@ -1,206 +0,0 @@
-"use client";
-import { ColumnDef } from "@tanstack/react-table";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { MoreHorizontal } from "lucide-react";
-import { DataTableColumnHeader } from "@/components/ui/data-table";
-import React, { useState, useEffect } from "react";
-import { Dialog } from "@radix-ui/react-dialog";
-import { Button } from "@/components/ui/button";
-import {
- AlertDialog,
- AlertDialogContent,
- AlertDialogTrigger,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogCancel,
- AlertDialogAction,
-} from "@/components/ui/alert-dialog";
-import EditCategory from "@/components/dash/admin/categories/EditCategoryDialogue";
-import DeleteCategoryDialogue from "@/components/dash/admin/semesters/DeleteSemesterDialogue";
-import { DialogTrigger } from "@/components/ui/dialog";
-import { toast } from "sonner";
-import { Semester } from "db/types";
-import { Switch } from "@/components/ui/switch";
-import { useOptimisticAction } from "next-safe-action/hooks";
-import { toggleCurrentSemester } from "@/actions/semester";
-import { formatInTimeZone } from "date-fns-tz";
-import { getClientTimeZone } from "@/lib/utils";
-import DeleteSemesterDialogue from "@/components/dash/admin/semesters/DeleteSemesterDialogue";
-import UpdateSemesterDialogue from "@/components/dash/admin/semesters/UpdateSemesterDialogue";
-import { useRouter } from "next/navigation";
-
-export const semesterColumns: ColumnDef[] = [
- {
- accessorKey: "semesterID",
- header: "ID",
- },
- {
- accessorKey: "name",
- header: ({ column }) => {
- return ;
- },
- enableSorting: true,
- },
- {
- accessorKey: "Duration",
- header: ({ column }) => {
- return (
-
- );
- },
- cell: ({ row }) => {
- const clientTimeZone = getClientTimeZone();
- const { startDate, endDate } = row.original;
- const startDateFormatted = formatInTimeZone(
- startDate,
- clientTimeZone,
- "eeee, MMMM dd yyyy",
- );
- const endDateFormatted = formatInTimeZone(
- endDate,
- clientTimeZone,
- "eeee, MMMM dd yyyy",
- );
- return {`${startDateFormatted} - ${endDateFormatted}`}
;
- },
- enableSorting: true,
- },
- {
- accessorKey: "isCurrent",
- header: ({ column }) => {
- return (
-
- );
- },
- cell: ({ row }) => {
- const { isCurrent, semesterID } = row.original;
- const { refresh } = useRouter();
- const {
- execute: runToggleSemester,
- result,
- optimisticState,
- } = useOptimisticAction(toggleCurrentSemester, {
- currentState: { isCurrent },
- updateFn: (_, newChecked) => {
- return {
- isCurrent: newChecked.isCurrent,
- };
- },
- onError: ({}) => {
- toast.dismiss();
- toast.error("Failed to update current semester");
- },
- onExecute: ({}) => {
- toast.dismiss();
- toast.success("Semester toggled.", {
- duration: 1500,
- });
- },
- onSuccess: () => {
- refresh();
- },
- });
- return (
- {
- runToggleSemester({
- semesterID,
- isCurrent: checked_value,
- });
- }}
- />
- );
- },
- },
- {
- accessorKey: "pointsRequired",
- header: ({ column }) => {
- return (
-
- );
- },
- },
- {
- id: "actions",
- enablePinning: true,
- header: ({}) => {},
- cell: ({ row }) => {
- const [open, setOpen] = useState(false);
-
- const data = row.original;
- return (
-
-
-
-
-
- Open menu
-
-
-
-
-
- toast.promise(
- navigator.clipboard.writeText(
- data.semesterID.toString(),
- ),
- {
- loading: "Copying ID...",
- success: "ID copied",
- error: "Failed to copy ID",
- },
- )
- }
- >
- Copy ID
-
-
-
-
-
- Edit
-
-
-
-
- Delete
-
-
-
-
-
-
-
-
- );
- },
- },
-];
diff --git a/apps/web/src/app/admin/semesters/page.tsx b/apps/web/src/app/admin/semesters/page.tsx
deleted file mode 100644
index 6254c704..00000000
--- a/apps/web/src/app/admin/semesters/page.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Suspense } from "react";
-import AdminSemesterView from "@/components/dash/admin/semesters/SemesterView";
-export default function Page() {
- return (
-
-
-
- Semesters
-
-
-
Grabbing checkin stats. One sec... }>
-
-
-
- );
-}
diff --git a/apps/web/src/app/api/admin/export/route.ts b/apps/web/src/app/api/admin/export/route.ts
deleted file mode 100644
index 15008638..00000000
--- a/apps/web/src/app/api/admin/export/route.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import { db, desc } from "db";
-import c from "config";
-import { NextRequest, NextResponse } from "next/server";
-import { ExportNames } from "@/lib/types/shared";
-import { getClientTimeZone } from "@/lib/utils";
-import { formatInTimeZone } from "date-fns-tz";
-import { getEventsWithCheckins } from "@/lib/queries/events";
-import { getCheckinLog } from "@/lib/queries/checkins";
-import { getAllCategories } from "@/lib/queries/categories";
-import { getAllSemesters } from "@/lib/queries/semesters";
-import { getRequestContext } from "@cloudflare/next-on-pages";
-
-const basicDateFormatterString = "eeee, MMMM dd yyyy HH:mm a";
-
-function escape(value: any) {
- if (value === null) return "None";
-
- // convert to string if it's not already
- const stringValue =
- typeof value !== "string" ? JSON.stringify(value) : value;
-
- // escape double quotes and enclose in quotes if it contains comma, newline or double quote
- if (/[",\n]/.test(stringValue)) {
- return `"${stringValue.replace(/"/g, '""')}"`;
- }
-
- return stringValue;
-}
-
-function jsonToCSV(json: any[]): string {
- if (!Array.isArray(json) || json.length === 0) {
- return "";
- }
-
- const header = Object.keys(json[0]);
- let csv = json.map((row) =>
- header.map((fieldName) => escape(row[fieldName])).join(","),
- );
- csv.unshift(header.join(","));
-
- return csv.join("\r\n");
-}
-
-async function hanldExportRequest(
- exportName: ExportNames,
- tz: string,
-): Promise {
- switch (exportName as ExportNames) {
- case "members":
- const memberTableData =
- (await db.query.users.findMany({
- with: {
- data: true,
- },
- })) ?? [];
- return memberTableData.map((user) => {
- let toRet = {
- ...user,
- ...user.data,
- joinDate: formatInTimeZone(
- user.joinDate,
- tz,
- basicDateFormatterString,
- ),
- birthday: user.data.birthday
- ? formatInTimeZone(
- user.data.birthday,
- tz,
- "eeee, MMMM dd yyyy",
- )
- : "Not provided",
- };
- ///@ts-ignore We know this breaks contract, but has been tested.
- delete toRet?.data;
- return toRet;
- });
- case "events":
- return (await getEventsWithCheckins()).map((event) => {
- return {
- id: event.id,
- name: event.name,
- description: event.description,
- points: event.points,
- location: event.location,
- event_start: formatInTimeZone(
- event.start,
- tz,
- basicDateFormatterString,
- ),
- event_end: formatInTimeZone(
- event.end,
- tz,
- basicDateFormatterString,
- ),
- event_checkin_start: formatInTimeZone(
- event.checkinStart,
- tz,
- basicDateFormatterString,
- ),
- event_checkin_end: formatInTimeZone(
- event.checkinEnd,
- tz,
- basicDateFormatterString,
- ),
- last_updated: formatInTimeZone(
- event.updatedAt,
- tz,
- basicDateFormatterString,
- ),
- checkins: event.checkin_count,
- };
- });
- case "categories":
- return getAllCategories();
- case "checkins":
- // need to come back and flatten this one
- return (await getCheckinLog()).map((checkin) => {
- return {
- event_name: checkin.event.name,
- user:
- checkin.author.firstName +
- " " +
- checkin.author.lastName,
- checkin_time: formatInTimeZone(
- checkin.time,
- tz,
- basicDateFormatterString,
- ),
- rating: checkin.rating ?? "No rating",
- feedback: checkin.feedback || "",
- };
- });
- case "semesters":
- return await getAllSemesters();
- default:
- return [];
- }
-}
-
-export async function GET(request: NextRequest) {
- const exportName = request.nextUrl.searchParams.get("name");
- if (!exportName) {
- return NextResponse.json(
- { error: "No name provided" },
- { status: 400 },
- );
- }
- const {
- cf: { timezone },
- } = getRequestContext();
- const clientTimeZone = getClientTimeZone(timezone);
- const flattendedResults = await hanldExportRequest(
- exportName as ExportNames,
- clientTimeZone,
- );
- const csv = jsonToCSV(flattendedResults);
-
- const formattedDate = formatInTimeZone(
- new Date(),
- clientTimeZone,
- "MMMM_dd_yyyy_HH:mm:ss_a",
- );
-
- return new Response(csv, {
- headers: {
- "Content-Type": "text/csv",
- "Content-Disposition": `attachment; filename=${c.clubName}_${exportName}_export_${formattedDate}.csv`,
- },
- });
-}
-
-export const runtime = "edge";
diff --git a/apps/web/src/app/api/ics-calendar/route.ts b/apps/web/src/app/api/ics-calendar/route.ts
deleted file mode 100644
index cc4feb50..00000000
--- a/apps/web/src/app/api/ics-calendar/route.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-import { getEventById } from "@/lib/queries/events";
-import { ics } from "calendar-link";
-
-export async function GET(request: NextRequest, res: NextResponse) {
- const eventID = request.nextUrl.searchParams.get("event_id");
- if (!eventID) {
- return new Response("Missing event_id", { status: 400 });
- }
- const event = await getEventById(eventID);
- if (!event) {
- return new Response("Event not found", { status: 404 });
- }
- const cal = ics({
- title: event.name,
- description: event.description,
- start: event.start,
- end: event.end,
- location: event.location,
- });
-
- return new Response(cal, {
- headers: {
- "Content-Type": "text/calendar; charset=utf-8",
- "Content-Disposition": `attachment; filename="event_${event.name}.ics"`,
- },
- });
-}
diff --git a/apps/web/src/app/api/upload/resume/route.ts b/apps/web/src/app/api/upload/resume/route.ts
deleted file mode 100644
index 85fd6325..00000000
--- a/apps/web/src/app/api/upload/resume/route.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { getPresignedUploadUrl } from "@/lib/server/s3";
-import { NextResponse } from "next/server";
-import { auth } from "@clerk/nextjs/server";
-import { staticUploads } from "config";
-
-interface RequestBody {
- location: string;
- fileName: string;
-}
-
-export async function POST(request: Request): Promise {
- try {
- const body: RequestBody = (await request.json()) as RequestBody;
-
- const { userId } = await auth();
- if (!userId) {
- return new NextResponse(
- "You do not have permission to upload files",
- {
- status: 401,
- },
- );
- }
-
- const randomSeq = crypto.randomUUID();
- const [fileName, extension] = body.fileName.split(".");
- const key = `${body.location}/${fileName}-${randomSeq}.${extension}`;
- const url = await getPresignedUploadUrl(staticUploads.bucketName, key);
-
- return NextResponse.json({ url, key });
- } catch (error) {
- return NextResponse.json(
- { error: (error as Error).message },
- { status: 400 },
- );
- }
-}
-
-export const runtime = "edge";
diff --git a/apps/web/src/app/api/upload/view/route.ts b/apps/web/src/app/api/upload/view/route.ts
index 7d9eb68c..8742ff06 100644
--- a/apps/web/src/app/api/upload/view/route.ts
+++ b/apps/web/src/app/api/upload/view/route.ts
@@ -6,7 +6,7 @@ import { headers } from "next/headers";
export async function GET(request: Request) {
const { userId } = await auth();
- const referPath = headers().get("referer") ?? "";
+ const referPath = (await headers()).get("referer") ?? "";
if (!userId && !referPath.includes("events")) {
return new Response("You must be logged in to access this resource", {
status: 401,
diff --git a/apps/web/src/app/dash/layout.tsx b/apps/web/src/app/dash/layout.tsx
deleted file mode 100644
index d353869c..00000000
--- a/apps/web/src/app/dash/layout.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { auth } from "@clerk/nextjs/server";
-import { db } from "db";
-import { users } from "db/schema";
-import { eq } from "db/drizzle";
-import { redirect } from "next/navigation";
-import Navbar from "@/components/shared/navbar";
-
-export default async function DashLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- const { userId } = await auth();
-
- if (!userId) {
- return redirect("/sign-in");
- }
-
- const user = await db.query.users.findFirst({
- where: eq(users.clerkID, userId),
- });
-
- if (!user) {
- return redirect("/onboarding");
- }
-
- return (
- <>
-
- {children}
- >
- );
-}
diff --git a/apps/web/src/app/dash/page.tsx b/apps/web/src/app/dash/page.tsx
deleted file mode 100644
index 8affb275..00000000
--- a/apps/web/src/app/dash/page.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { auth } from "@clerk/nextjs/server";
-import { redirect } from "next/navigation";
-import UserDash from "@/components/dash/UserDash";
-import { headers } from "next/headers";
-import { getClientTimeZone } from "@/lib/utils";
-import { Suspense } from "react";
-import { LoaderCircle } from "lucide-react";
-import { getRequestContext } from "@cloudflare/next-on-pages";
-
-export default async function Page() {
- const { userId: clerkID } = await auth();
-
- if (!clerkID) return redirect("/sign-in");
-
- const {
- cf: { timezone },
- } = getRequestContext();
-
- const clientTimeZone = getClientTimeZone(timezone);
-
- return (
-
-
-
- Loading. Please wait.
-
- }
- >
-
-
-
- );
-}
-
-export const runtime = "edge";
-export const revalidate = 30;
diff --git a/apps/web/src/app/events/[slug]/checkin/page.tsx b/apps/web/src/app/events/[slug]/checkin/page.tsx
deleted file mode 100644
index f8b34ced..00000000
--- a/apps/web/src/app/events/[slug]/checkin/page.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import EventCheckin from "@/components/events/id/checkin/EventCheckin";
-import Navbar from "@/components/shared/navbar";
-import { auth } from "@clerk/nextjs/server";
-import { redirect } from "next/navigation";
-import PageError from "@/components/shared/PageError";
-import { Suspense } from "react";
-import { getUserCheckin } from "@/lib/queries/users";
-import { getUTCDate } from "@/lib/utils";
-
-export default async function Page({ params }: { params: { slug: string } }) {
- const { userId: clerkId } = await auth();
- if (!clerkId) {
- redirect("/sign-in");
- }
-
- if (!params?.slug) {
- return (
-
- );
- }
-
- const currentDateUTC = getUTCDate();
-
- return (
-
- {" "}
- Grabbing the event. One sec...}>
-
-
-
- );
-}
-export const runtime = "edge";
diff --git a/apps/web/src/app/events/[slug]/client.tsx b/apps/web/src/app/events/[slug]/client.tsx
new file mode 100644
index 00000000..4ccb01b0
--- /dev/null
+++ b/apps/web/src/app/events/[slug]/client.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { MapPin } from "lucide-react";
+import Map, { Marker } from "react-map-gl/mapbox";
+import Image from "next/image";
+import "mapbox-gl/dist/mapbox-gl.css";
+
+interface MapProps {
+ coords: {
+ latitude: number;
+ longitude: number;
+ };
+}
+
+export default function EventLocationMap({
+ coords,
+}: {
+ coords: {
+ latitude: number;
+ longitude: number;
+ };
+}) {
+ console.log(process.env.NEXT_PUBLIC_MAPKIT_TOKEN);
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/app/events/[slug]/page.tsx b/apps/web/src/app/events/[slug]/page.tsx
index 8c5f63f0..fcd25516 100644
--- a/apps/web/src/app/events/[slug]/page.tsx
+++ b/apps/web/src/app/events/[slug]/page.tsx
@@ -1,26 +1,369 @@
-import EventDetails from "@/components/events/id/EventDetails";
-import Navbar from "@/components/shared/navbar";
-import { Suspense } from "react";
+import { HeroNav } from "@/components/shared/navbar";
+import { cloneElement, Fragment, Suspense } from "react";
import { headers } from "next/headers";
+import { eq } from "db/drizzle";
+import { db } from "db";
+import { events } from "db/schema";
+import Image from "next/image";
+import { notFound } from "next/navigation";
+import { formatInTimeZone } from "date-fns-tz";
+import { getClientTimeZone } from "@/lib/utils";
+import {
+ MapPin as MapPinIcon,
+ ArrowUpRight,
+ CalendarDays as CalendarDaysIcon,
+ Trophy as TrophyIcon,
+ Youtube as YoutubeIcon,
+ Twitch as TwitchIcon,
+} from "lucide-react";
+import { isAfter } from "date-fns";
+import Footer from "@/components/shared/footer";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import EventLocationMap from "./client";
+import { fuzzyFindLocation } from "@/lib/shared/geo";
+import { Badge } from "@/components/ui/badge";
-export default function Page({ params }: { params: { slug: string } }) {
- const userAgent = headers().get("user-agent")?.toLowerCase();
+export default async function Page(props: {
+ params: Promise<{ slug: string }>;
+}) {
+ const params = await props.params;
+ const userAgent = (await headers()).get("user-agent")?.toLowerCase();
const isBroswerSafari =
(userAgent?.includes("safari") &&
!userAgent?.includes("crios") &&
!userAgent.includes("chrome")) ||
false;
+ const event = await db.query.events.findFirst({
+ where: eq(events.id, params.slug),
+ // TODO: re-add this once we are properly indexing the eventsToCategories table
+ // with: {
+ // eventsToCategories: {
+ // with: {
+ // category: true,
+ // },
+ // },
+ // },
+ });
+
+ if (!event) {
+ return notFound();
+ }
+
+ const location = fuzzyFindLocation(event.location);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ Presented By
+
+ {/* TODO: re-add this once we are properly indexing the eventsToCategories table */}
+ {/* {event.eventsToCategories
+ .map((c) => c.category.name)
+ .join(", ")
+ .replace(/,(?=[^,]*$)/, " &")} */}
+
+ ACM
+
+
+
+
+ Virtual Options
+
+
+
+
+ Watch on YouTube
+
+
+
+
+
+ Watch on Twitch
+
+
+
+
+
+
+
+ {event.name}
+
+
+
+
+
+
+
+
+ About The Event
+
+
+ {event.description}
+
+
+ {location.found ? (
+
+
+ Location
+
+
+
+
+
+
+
+ ) : null}
+
+
+
+
+ >
+ );
+}
+
+function CalendarWidget({
+ startDate,
+ endDate,
+}: {
+ startDate: Date;
+ endDate: Date;
+}) {
+ const clientTimeZone = getClientTimeZone();
+
+ const monthAbrevs = [
+ "JAN",
+ "FEB",
+ "MAR",
+ "APR",
+ "MAY",
+ "JUN",
+ "JUL",
+ "AUG",
+ "SEP",
+ "OCT",
+ "NOV",
+ "DEC",
+ ];
+
+ return (
+
+
+
+
+ {monthAbrevs[startDate.getMonth()]}
+
+
+
+
+ {startDate.getDate()}
+
+
+
+
+
+ {formatInTimeZone(
+ startDate,
+ clientTimeZone,
+ "EEEE, MMMM d, yyyy",
+ )}
+
+
+ {`${formatInTimeZone(startDate, clientTimeZone, "h:mm a")} - ${formatInTimeZone(endDate, clientTimeZone, "h:mm a")}`}
+
+
+
+ );
+}
+
+function LocationWidget({ location }: { location: string }) {
return (
-
-
-
Grabbing the event. One sec...}>
-
-
+
+
+
+
+
+
+ {location}
+
+
+ View on map
+
+
);
}
+
+function Pill({
+ // icon,
+ children,
+}: {
+ // icon: React.ReactNode;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {/* {cloneElement(icon as React.ReactElement, { size: 20 })} */}
+ {children}
+
+
+ );
+}
+
+function EventInteractionCard({
+ start,
+ end,
+ checkinStart,
+ checkinEnd,
+ points,
+}: {
+ start: Date;
+ end: Date;
+ checkinStart: Date;
+ checkinEnd: Date;
+ points: number;
+}) {
+ type timeStatus = "before" | "during" | "after";
+
+ const tz = getClientTimeZone();
+
+ const eventStatus: timeStatus = isAfter(new Date(), start)
+ ? isAfter(new Date(), end)
+ ? "after"
+ : "during"
+ : "before";
+
+ const checkinStatus: timeStatus = isAfter(new Date(), checkinStart)
+ ? isAfter(new Date(), checkinEnd)
+ ? "after"
+ : "during"
+ : "before";
+
+ const eventStatusTitle =
+ eventStatus === "before"
+ ? "Coming Up"
+ : eventStatus === "during"
+ ? "Happening Now"
+ : "Past Event";
+
+ const eventStatusText =
+ eventStatus === "before"
+ ? "This event has not started yet."
+ : eventStatus === "during"
+ ? "This event is happening right now!"
+ : "This event has already occured.";
+
+ const checkinStatusText =
+ checkinStatus === "before"
+ ? "Check-in has not started yet."
+ : checkinStatus === "during"
+ ? "Check-in is happening right now!"
+ : "Check-in has already ended. If you need to check in, please contact the event organizer!";
+
+ return (
+
+
+
+ Check-in
+
+
+
+
+
+
+
+
+
+ {eventStatusTitle}
+
+
+ {eventStatusText}
+
+
+
+
+
+
+
+
+
+ {`${points} Point${points === 1 ? "" : "s"}`}
+
+
+ {`This event is worth ${points} membership point${points === 1 ? "" : "s"}.`}
+
+
+
+
+
+
+ {checkinStatusText}
+
+
+
+ Check-in
+
+
+
+
+
+ );
+}
+
export const runtime = "edge";
diff --git a/apps/web/src/app/events/client.tsx b/apps/web/src/app/events/client.tsx
new file mode 100644
index 00000000..3a2cb450
--- /dev/null
+++ b/apps/web/src/app/events/client.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import { EVENT_FILTERS } from "@/lib/constants/events";
+import { eventsParams } from "./params";
+import { useQueryStates } from "nuqs";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+ DropdownMenuCheckboxItem,
+} from "@/components/ui/dropdown-menu";
+import { ChevronDown } from "lucide-react";
+import type { EventCategoryType } from "@/lib/types/events";
+import { use } from "react";
+
+interface EventToolBarProps {
+ availableCategories: Promise
;
+}
+
+export function EventsToolBar({ availableCategories }: EventToolBarProps) {
+ const [params, setParams] = useQueryStates(eventsParams, {
+ shallow: false,
+ throttleMs: 200,
+ });
+ const cats = use(availableCategories);
+
+ return (
+
+
+
+ setParams({
+ query: e.target.value,
+ })
+ }
+ defaultValue={params.query}
+ />
+
+
+
+
+
+ );
+}
+
+function FiltersDropdown({ categories }: { categories: EventCategoryType[] }) {
+ const [params, setParams] = useQueryStates(eventsParams, {
+ shallow: false,
+ });
+
+ return (
+
+
+
+ Filters
+
+
+
+ Filter by Categories
+
+ {categories.map((cat) => (
+ 0 &&
+ params.categories.includes(cat.name)
+ }
+ onCheckedChange={() =>
+ setParams({
+ categories: params.categories.includes(cat.name)
+ ? params.categories.filter(
+ (c) => c !== cat.name,
+ )
+ : [...params.categories, cat.name],
+ })
+ }
+ >
+ {cat.name}
+
+ ))}
+
+ Time
+
+ setParams({ past: checked })}
+ >
+ Include Past Events
+
+
+
+ );
+}
diff --git a/apps/web/src/app/events/page.tsx b/apps/web/src/app/events/page.tsx
index 75987795..af07be37 100644
--- a/apps/web/src/app/events/page.tsx
+++ b/apps/web/src/app/events/page.tsx
@@ -1,30 +1,194 @@
-import EventsTitle from "@/components/events/EventsTitle";
-import EventsOptionsBar from "@/components/events/filters/EventsOptionsBar";
-import EventsView from "@/components/events/EventsView";
import { Suspense } from "react";
-import Navbar from "@/components/shared/navbar";
-import type { SearchParams } from "@/lib/types/shared";
-export default function EventsPage({
- searchParams,
+import { HeroNav } from "@/components/shared/navbar";
+import type { SearchParams } from "nuqs/server";
+import { EventsToolBar } from "./client";
+import { getAllCategories } from "@/lib/queries/categories";
+import { createLoader } from "nuqs/server";
+import { loadSearchParams } from "./params";
+import { events } from "db/schema";
+import { db } from "db";
+import { gt, asc, desc } from "db/drizzle";
+import { EventAndCategoriesType } from "@/lib/types/events";
+import { getClientTimeZone, getUTCDate } from "@/lib/utils";
+import { getRequestContext } from "@cloudflare/next-on-pages";
+import Image from "next/image";
+import { Calendar, MapPin } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import Footer from "@/components/shared/footer";
+import Link from "next/link";
+
+export default async function EventsPage(props: {
+ searchParams: Promise;
+}) {
+ const searchParams = await props.searchParams;
+ const categories = getAllCategories();
+ const params = await loadSearchParams(searchParams);
+
+ const allEvents = db.query.events
+ .findMany({
+ with: {
+ eventsToCategories: {
+ with: {
+ category: {
+ columns: {
+ name: true,
+ color: true,
+ },
+ },
+ },
+ },
+ },
+ where: params.past
+ ? undefined
+ : gt(events.start, new Date(Date.now() - 12 * 60 * 60 * 1000)),
+ orderBy: params.past ? desc(events.start) : asc(events.start),
+ })
+ .then((events) => {
+ if (params.categories.length > 0) {
+ return events.filter((event) => {
+ return event.eventsToCategories.some((eventToCategory) => {
+ return params.categories.includes(
+ eventToCategory.category.name,
+ );
+ });
+ });
+ }
+
+ // TODO: implement fuzzy search on sql query
+
+ if (params.query) {
+ const searchQuery = params.query.toLowerCase();
+ return events.filter((event) => {
+ return event.name.toLowerCase().includes(searchQuery);
+ });
+ }
+
+ return events;
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+ 0 ||
+ params.query !== ""
+ }
+ />
+
+
+
+
+ >
+ );
+}
+
+async function CardEventsView({
+ eventsQuery,
+ hadFilters,
}: {
- searchParams: SearchParams;
+ eventsQuery: Promise;
+ hadFilters: boolean;
}) {
+ const events = await eventsQuery;
+
+ if (events.length === 0) {
+ return (
+
+
+ {hadFilters
+ ? "No events found with the given filters. Please try again with different filters."
+ : "We have no events scheduled at the moment. Stay tuned!"}
+
+
+ );
+ }
+
+ const timeZone = getClientTimeZone(getRequestContext().cf.timezone);
+
+ const cards = events.map((event) => (
+
+ ));
+
return (
-
-
-
-
-
- Grabbing Events. One sec...
-
- }
- >
-
-
+
+ {cards}
);
}
+
+function EventCard({
+ event,
+ timezone,
+}: {
+ event: EventAndCategoriesType;
+ timezone: string;
+}) {
+ const currentDateUTC = getUTCDate();
+
+ return (
+
+
+
+
+
+
+
+ {event.name}
+
+
+
+
+ {event.start.toLocaleString("en-US", {
+ timeZone: timezone,
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ })}
+
+
+
+ {event.location}
+
+
+
+ {event.eventsToCategories.map((cat) => {
+ return (
+
+ {cat.category.name}
+
+ );
+ })}
+
+
+
+
+
+ );
+}
+
export const runtime = "edge";
-export const revalidate = 30;
diff --git a/apps/web/src/app/events/params.ts b/apps/web/src/app/events/params.ts
new file mode 100644
index 00000000..82a96247
--- /dev/null
+++ b/apps/web/src/app/events/params.ts
@@ -0,0 +1,14 @@
+import {
+ parseAsArrayOf,
+ parseAsBoolean,
+ parseAsString,
+ createLoader,
+} from "nuqs/server";
+
+export const eventsParams = {
+ query: parseAsString.withDefault(""),
+ categories: parseAsArrayOf(parseAsString).withDefault([]),
+ past: parseAsBoolean.withDefault(false),
+};
+
+export const loadSearchParams = createLoader(eventsParams);
diff --git a/apps/web/src/app/fonts/calsans-reg.woff2 b/apps/web/src/app/fonts/calsans-reg.woff2
new file mode 100644
index 00000000..6c83a2df
Binary files /dev/null and b/apps/web/src/app/fonts/calsans-reg.woff2 differ
diff --git a/apps/web/src/app/fonts/calsans-semi.woff2 b/apps/web/src/app/fonts/calsans-semi.woff2
new file mode 100644
index 00000000..36d71b70
Binary files /dev/null and b/apps/web/src/app/fonts/calsans-semi.woff2 differ
diff --git a/apps/web/src/app/fonts/chillax.woff2 b/apps/web/src/app/fonts/chillax.woff2
new file mode 100644
index 00000000..fc4ec89d
Binary files /dev/null and b/apps/web/src/app/fonts/chillax.woff2 differ
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 074c5462..a77e99de 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -110,6 +110,10 @@
@apply hidden;
}
+.mapboxgl-ctrl-attrib {
+ display: none;
+}
+
.live-glow::after {
@apply rounded-md;
content: "";
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index c50e1c2e..7271d1cb 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -1,32 +1,46 @@
import type { Metadata } from "next";
-import { Inter } from "next/font/google";
import "./globals.css";
-import { ClerkProvider } from "@clerk/nextjs";
-const inter = Inter({ subsets: ["latin"] });
import { cookies } from "next/headers";
import { defaultTheme } from "config";
+import localFont from "next/font/local";
+import { Inter } from "next/font/google";
+import { NuqsAdapter } from "nuqs/adapters/next/app";
// export const metadata: Metadata = {
// title: "Create Next App",
// description: "Generated by create next app",
// };
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- const theme = cookies().get("ck_theme")?.value || defaultTheme;
+const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
+
+const chillax = localFont({
+ src: "./fonts/chillax.woff2",
+ display: "swap",
+ variable: "--font-chillax",
+});
+
+const calsans = localFont({
+ src: "./fonts/calsans-semi.woff2",
+ variable: "--font-calsans",
+});
+
+export default async function RootLayout(
+ {
+ children,
+ }: Readonly<{
+ children: React.ReactNode;
+ }>
+) {
+ const theme = (await cookies()).get("ck_theme")?.value || defaultTheme;
return (
-
-
-
- {children}
-
-
-
+
+
+
{children}
+
+
);
}
diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx
index 4505892f..3bba96c0 100644
--- a/apps/web/src/app/not-found.tsx
+++ b/apps/web/src/app/not-found.tsx
@@ -1,13 +1,39 @@
+import { Button } from "@/components/ui/button";
import Link from "next/link";
-
-// TODO: make this look better
+import Image from "next/image";
export default function NotFound() {
return (
-
-
Not Found
-
Could not find requested resource
-
Return Home
+
+
+
+
+ 404 - not found
+
+
+ We couldn't find that page! If you believe this is an error,
+ please{" "}
+
+ let us know
+
+ !
+
+
+
+
+ Return Home
+
+
+
);
}
diff --git a/apps/web/src/app/onboarding/layout.tsx b/apps/web/src/app/onboarding/layout.tsx
deleted file mode 100644
index 123aa827..00000000
--- a/apps/web/src/app/onboarding/layout.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Toaster } from "@/components/ui/sonner";
-import { auth } from "@clerk/nextjs/server";
-import { db } from "db";
-import { users } from "db/schema";
-import { eq } from "db/drizzle";
-import { redirect } from "next/navigation";
-
-export default async function OnboardingLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- // TODO: protect stuffs from re-registration
-
- const { userId } = await auth();
-
- if (!userId) return redirect("/sign-up");
-
- const user = await db.query.users.findFirst({
- where: eq(users.clerkID, userId),
- });
-
- if (user) {
- return redirect("/dash");
- }
-
- return (
- <>
- {children}
-
- >
- );
-}
diff --git a/apps/web/src/app/onboarding/migrate/page.tsx b/apps/web/src/app/onboarding/migrate/page.tsx
deleted file mode 100644
index 6570698a..00000000
--- a/apps/web/src/app/onboarding/migrate/page.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import Migrator from "@/components/onboarding/migrator";
-import { currentUser } from "@clerk/nextjs/server";
-import { redirect } from "next/navigation";
-
-export default async function Page() {
- const user = await currentUser();
- if (!user) {
- return redirect("/sign-in");
- }
- const clerkEmail = user.emailAddresses[0].emailAddress;
- return (
-
-
-
Migrate
-
-
-
- );
-}
diff --git a/apps/web/src/app/onboarding/page.tsx b/apps/web/src/app/onboarding/page.tsx
deleted file mode 100644
index df61cb6e..00000000
--- a/apps/web/src/app/onboarding/page.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import RegisterForm from "@/components/onboarding/registerForm";
-import Link from "next/link";
-import { Button } from "@/components/ui/button";
-import { db, eq } from "db";
-import { users } from "db/schema";
-import { auth, currentUser } from "@clerk/nextjs/server";
-import { redirect } from "next/navigation";
-import c from "config";
-
-export default async function Page() {
- const clerkUser = await currentUser();
-
- if (!clerkUser) return redirect("/sign-in");
- const userEmail = clerkUser.emailAddresses[0].emailAddress;
-
- const userByEmail = await db.query.users.findFirst({
- where: eq(users.email, userEmail),
- });
-
- if (userByEmail) {
- return (
-
-
Account Detected
-
-
-
- An unconnected account with the email{" "}
- {userEmail} {" "}
- has been found.
-
-
- Please follow the link below to connect the account.
-
-
-
-
Connect Account
-
-
-
- If you believe this is a mistake or have trouble connecting
- the account, please reach out to{" "}
-
-
- {c.contactEmail}.
-
-
-
-
- );
- }
-
- return (
-
-
-
-
-
Registration
-
- Welcome! Please
- fill out the form below to complete your
- registration.
-
-
-
-
- Had a legacy portal (abc123 & email) account?
-
-
-
- Migrate from Portal
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
deleted file mode 100644
index 347e349a..00000000
--- a/apps/web/src/app/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import Navbar from "@/components/shared/navbar";
-export default function Home() {
- return (
- // bg-[var(--my-var,var(--my-background,pink))]
-
-
-
- ClubKit
-
-
- );
-}
diff --git a/apps/web/src/app/settings/layout.tsx b/apps/web/src/app/settings/layout.tsx
deleted file mode 100644
index cc811184..00000000
--- a/apps/web/src/app/settings/layout.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { PropsWithChildren } from "react";
-import { Toaster } from "sonner";
-import Navbar from "@/components/shared/navbar";
-import { SettingsNav } from "@/components/settings/settings-nav";
-import { SettingsDropdown } from "@/components/settings/settings-dropdown";
-
-interface UserSettingsLayoutProps extends PropsWithChildren {}
-
-const links: { href: string; title: string }[] = [
- { href: "/settings#account", title: "Account" },
- { href: "/settings#profile", title: "Profile" },
- { href: "/settings#club", title: "Club" },
- { href: "/settings#school", title: "School" },
-];
-
-export default async function UserSettingsLayout({
- children,
-}: UserSettingsLayoutProps) {
- return (
- <>
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx
deleted file mode 100644
index 9e38b734..00000000
--- a/apps/web/src/app/settings/page.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import { AccountSettingsForm } from "@/components/settings/forms/account-settings-form";
-import { ChangeProfilePictureForm } from "@/components/settings/forms/change-profile-picture-form";
-import { ChangeResumeForm } from "@/components/settings/forms/change-resume-form";
-import { ClubSettingsForm } from "@/components/settings/forms/club-settings-form";
-import { SchoolSettingsForm } from "@/components/settings/forms/school-settings-form";
-import { getUserSettings } from "@/lib/queries/user-settings";
-import {
- GenderType,
- EthnicityType,
- ShirtSizeType,
- ShirtType,
- ClassificationType,
- MajorType,
-} from "@/lib/types/shared";
-import { auth } from "@clerk/nextjs/server";
-import { redirect } from "next/navigation";
-import { clerkClient } from "@clerk/nextjs/server";
-import { Separator } from "@/components/ui/separator";
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-
-export default async function UserSettingsProfilePage() {
- const { userId } = await auth();
-
- if (!userId) return redirect("/sign-up");
-
- const userSettings = await getUserSettings(userId);
-
- if (!userSettings) return redirect("/onboarding");
- const authClient = await clerkClient();
- const user = await authClient.users.getUser(userId);
-
- const params = new URLSearchParams({
- height: "112",
- width: "112",
- quality: "100",
- fit: "crop",
- });
-
- return (
-
-
-
- Account
-
- View and change your account details
-
-
-
-
-
-
-
-
-
-
-
-
-
- Profile
-
- Update your profile information
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Club
-
- Edit your club related settings here
-
-
-
-
-
-
-
-
-
-
-
-
-
- School
-
- Edit your existing academic information
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/sign-in/[[...sign-in]]/page.tsx b/apps/web/src/app/sign-in/[[...sign-in]]/page.tsx
deleted file mode 100644
index ada2756c..00000000
--- a/apps/web/src/app/sign-in/[[...sign-in]]/page.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-// "use client"
-import { SignIn } from "@clerk/nextjs";
-import { Button } from "@/components/ui/button";
-import c from "config";
-import Link from "next/link";
-import PortalMigrationExplainer from "@/components/dash/shared/PortalMigrationExplainer";
-export default function Page() {
- return (
-
-
-
{c.clubName}
-
-
-
- Migrate From Legacy Portal
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/app/sign-up/[[...sign-up]]/page.tsx b/apps/web/src/app/sign-up/[[...sign-up]]/page.tsx
deleted file mode 100644
index 650dedf3..00000000
--- a/apps/web/src/app/sign-up/[[...sign-up]]/page.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { SignUp } from "@clerk/nextjs";
-import { Button } from "@/components/ui/button";
-import c from "config";
-import Link from "next/link";
-import PortalMigrationExplainer from "@/components/dash/shared/PortalMigrationExplainer";
-export default function Page() {
- return (
-
-
-
ClubKit
-
- {/* TODO: Add Explainer For Portal Accounts as a Dialog */}
-
-
- Migrate From Legacy Portal
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/dash/UserDash.tsx b/apps/web/src/components/dash/UserDash.tsx
deleted file mode 100644
index af5541b5..00000000
--- a/apps/web/src/components/dash/UserDash.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import { auth } from "@clerk/nextjs/server";
-import { db } from "db";
-import { users, data, checkins, events } from "db/schema";
-import { eq, count, between, sum, sql } from "db/drizzle";
-import { redirect } from "next/navigation";
-import c from "config";
-import { RadialChartProgress } from "@/components/shared/circular-progress";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { Button } from "@/components/ui/button";
-import { headers } from "next/headers";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
- CardFooter,
-} from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import { CalendarIcon, GraduationCapIcon, MapPinIcon } from "lucide-react";
-import Link from "next/link";
-import { formatInTimeZone } from "date-fns-tz";
-import { getCurrentSemester } from "@/lib/queries/semesters";
-import { dashEventSchema } from "db/zod";
-import z from "zod";
-
-export default async function UserDash({
- clerkID,
- clientTimeZone,
-}: {
- clerkID: string;
- clientTimeZone: string;
-}) {
- const currentSemester = (await getCurrentSemester()) ?? c.semesters.current;
-
- const queryResult = await db
- .select({
- user: users,
- userData: data,
- attendedEvents: sql
`
- CASE
- WHEN COUNT(${events.id}) = 0 THEN NULL
- ELSE json_group_array(json_object(
- 'id', ${events.id},
- 'name', ${events.name},
- 'points', ${events.points},
- 'start', ${events.start}
- ))
- END
-`.as("attendedEvents"),
- currentSemesterPoints: sql<
- number | null
- >`SUM(${events.points}) FILTER (WHERE ${events.start} BETWEEN ${currentSemester.startDate} AND ${currentSemester.endDate} OR ${events.checkinStart} BETWEEN ${currentSemester.startDate} AND ${currentSemester.endDate})`.mapWith(
- Number,
- ),
- currentSemesterEventsAttended: sql<
- number | null
- >`COUNT(${events.id}) FILTER (WHERE ${events.start} BETWEEN ${currentSemester.startDate} AND ${currentSemester.endDate} OR ${events.checkinStart} BETWEEN ${currentSemester.startDate} AND ${currentSemester.endDate})`.mapWith(
- Number,
- ),
- totalEventsAttended: sql<
- number | null
- >`COUNT(${checkins.userID})`.mapWith(Number),
- })
- .from(users)
- .innerJoin(data, eq(users.userID, data.userID))
- .leftJoin(checkins, eq(users.userID, checkins.userID))
- .leftJoin(events, eq(events.id, checkins.eventID))
- .groupBy(users.userID, data.userID)
- .where(eq(users.clerkID, clerkID));
-
- if (queryResult.length === 0) {
- return redirect("/sign-in");
- }
-
- const userDashResult = queryResult[0];
- const {
- user,
- userData,
- currentSemesterPoints,
- currentSemesterEventsAttended,
- totalEventsAttended,
- attendedEvents,
- } = userDashResult;
-
- let userEvents: z.infer = [];
- const userEventsParseResult = dashEventSchema.safeParse(
- JSON.parse(attendedEvents ?? "[]"),
- );
- if (!userEventsParseResult.success) {
- console.error(userEventsParseResult.error);
- } else {
- userEvents = userEventsParseResult.data;
- }
-
- const joinedDate = formatInTimeZone(
- user.joinDate,
- clientTimeZone,
- "MMMM dd, yyyy",
- );
-
- const hasUserMetRequiredPoints =
- currentSemesterPoints >= currentSemester.pointsRequired;
-
- const radialChartProgressProps = {
- titleText: "Attendance Points",
- descriptionText: currentSemester.name,
- current: currentSemesterPoints ?? 0,
- total: currentSemester.pointsRequired,
- footerText: hasUserMetRequiredPoints
- ? `Way to go! You have gained enough points to attend our ${currentSemester.name} banquet 🎉`
- : `Keep attending events to earn more points!`,
- fill: "#3b82f6",
- };
- const slicedEvents = userEvents?.slice(0, 5) ?? [];
-
- return (
-
-
-
Welcome,
- {user.firstName}
-
-
-
-
- Profile
-
-
-
-
-
- {`${user.firstName.trim().charAt(0) + user.lastName.trim().charAt(0)}`}
-
-
-
-
- {user.firstName} {user.lastName}
-
-
-
- {`${userData.major}, ${userData.graduationYear}`}
-
-
- {`Member since ${joinedDate}`}
-
- {/* come back and configure wrangler json */}
-
-
-
-
-
-
-
-
- Activity
-
-
-
- {totalEventsAttended}
- {" "}
- Total event(s) attented
-
-
-
- {currentSemesterEventsAttended}
- {" "}
- Event(s) attended this semester
-
-
-
-
- {slicedEvents.length > 0 && (
-
- {slicedEvents?.map((event, index) => (
-
- {event.name}
-
- ))}
-
- )}
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/dash/admin/categories/CategoryView.tsx b/apps/web/src/components/dash/admin/categories/CategoryView.tsx
deleted file mode 100644
index 8db110b5..00000000
--- a/apps/web/src/components/dash/admin/categories/CategoryView.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { getAllCategories } from "@/lib/queries/categories";
-import { DataTable } from "@/components/ui/data-table";
-import CreateCategory from "./CreateCategoryDialogue";
-import { eventCategoryColumns } from "@/app/admin/categories/columns";
-
-export default async function AdminCategoryView() {
- const categories = await getAllCategories();
-
- return (
- <>
-
-
-
-
- Total Categories
-
-
- {categories.length}
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/components/dash/admin/categories/CreateCategoryDialogue.tsx b/apps/web/src/components/dash/admin/categories/CreateCategoryDialogue.tsx
deleted file mode 100644
index c230f5f4..00000000
--- a/apps/web/src/components/dash/admin/categories/CreateCategoryDialogue.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-"use client";
-
-import {
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogFooter,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { createEventCategory } from "@/actions/categories";
-import { useAction } from "next-safe-action/hooks";
-import { Input } from "@/components/ui/input";
-import { useState } from "react";
-import { HexColorPicker } from "react-colorful";
-import { useForm } from "react-hook-form";
-import { createEventCategorySchema } from "db/zod";
-import { Loader2 } from "lucide-react";
-import z from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { Dialog } from "@radix-ui/react-dialog";
-import { Plus } from "lucide-react";
-import { useRouter } from "next/navigation";
-
-export default function CreateCategoryDialogue() {
- const [open, setOpen] = useState(false);
- const form = useForm>({
- resolver: zodResolver(createEventCategorySchema),
- defaultValues: {
- color: "#000000",
- name: "",
- },
- });
- const { refresh } = useRouter();
- const { execute: runCreateEventCategory, status } = useAction(
- createEventCategory,
- {
- onSuccess: ({ data }) => {
- toast.dismiss();
- if (data?.message == "category_exists") {
- return toast.error(
- `Event category ${form.getValues("name")} already exists`,
- );
- }
- form.reset();
- setOpen(false);
- toast.success("Event category created successfully");
- refresh();
- },
- onError: (e) => {
- toast.dismiss();
- toast.error("Failed to create event category");
- },
- },
- );
- const isLoading = status === "executing";
- return (
-
-
-
-
- Add Category
-
-
-
-
- Create Event Category
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/dash/admin/categories/DeleteCategoryDialogue.tsx b/apps/web/src/components/dash/admin/categories/DeleteCategoryDialogue.tsx
deleted file mode 100644
index d954e864..00000000
--- a/apps/web/src/components/dash/admin/categories/DeleteCategoryDialogue.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-"use client";
-import {
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogAction,
- AlertDialogTitle,
- AlertDialogCancel,
-} from "@/components/ui/alert-dialog";
-import { useAction } from "next-safe-action/hooks";
-import { deleteEventCategory } from "@/actions/categories";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-export default function DeleteCategoryDialogue({
- name,
- categoryID,
-}: {
- name: string;
- categoryID: string;
-}) {
- const { refresh } = useRouter();
- const { execute: runDeleteEventCategory, status } = useAction(
- deleteEventCategory,
- {
- onSuccess: () => {
- toast.dismiss();
- toast.success("Event category deleted successfully");
- refresh();
- },
- onError: () => {
- toast.dismiss();
- toast.error("Failed to delete event category");
- },
- },
- );
- const isLoading = status === "executing";
- return (
-
-
- {`Are you want to delete "${name}"`}
-
- This action cannot be undone and will remove any category
- associations with existing events.
-
-
-
- Cancel
- {
- toast.loading("Deleting event category");
- runDeleteEventCategory(categoryID);
- }}
- >{`Delete ${name}`}
-
-
- );
-}
diff --git a/apps/web/src/components/dash/admin/categories/EditCategoryDialogue.tsx b/apps/web/src/components/dash/admin/categories/EditCategoryDialogue.tsx
deleted file mode 100644
index 7094abe5..00000000
--- a/apps/web/src/components/dash/admin/categories/EditCategoryDialogue.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-"use client";
-
-import {
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { updateEventCategory } from "@/actions/categories";
-import { useAction } from "next-safe-action/hooks";
-import { Input } from "@/components/ui/input";
-import { FormEvent, SetStateAction, useEffect, useState } from "react";
-import { HexColorPicker } from "react-colorful";
-import { useForm } from "react-hook-form";
-import { eventCategorySchema } from "db/zod";
-import { Loader2 } from "lucide-react";
-import z from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { useRouter } from "next/navigation";
-
-type EditCategoryProps = {
- eventCategory: z.infer;
- open: boolean;
- setOpen: React.Dispatch>;
-};
-
-export default function EditCategoryDialogue(
- editCategoryProps: EditCategoryProps,
-) {
- const { eventCategory: inputProps, setOpen, open } = editCategoryProps;
- const form = useForm>({
- resolver: zodResolver(eventCategorySchema),
- defaultValues: {
- ...inputProps,
- },
- });
- const { refresh } = useRouter();
- // this is required here in order to reset the dialog as router.refresh / revalidatePath will not properly make the change
- useEffect(() => {
- if (open) {
- form.reset({
- ...inputProps,
- });
- }
- }, [open]);
-
- const { execute: runUpdateEventCategory, status } = useAction(
- updateEventCategory,
- {
- onSuccess: ({ data }) => {
- toast.dismiss();
- if (data?.message == "category_exists") {
- return toast.error(
- `Event category ${form.getValues("name")} already exists`,
- );
- }
- // form.reset({
- // ...inputProps,
- // })
- setOpen(false);
- toast.success("Event category created successfully");
- refresh();
- },
- onError: (e) => {
- toast.dismiss();
- toast.error(`Failed to update ${form.getValues("name")}`);
- },
- },
- );
- const isLoading = status === "executing";
- useEffect(() => {
- console.log("form dirty", form.formState.isDirty);
- }, [form.formState.isDirty]);
- function onSubmit(data: z.infer) {
- if (!form.formState.isDirty) {
- return toast.error("No changes made");
- }
- runUpdateEventCategory(data);
- }
- return (
- <>
-
-
- Update Event Category
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/components/dash/admin/events/DeleteEventDialogue.tsx b/apps/web/src/components/dash/admin/events/DeleteEventDialogue.tsx
deleted file mode 100644
index 4550ff82..00000000
--- a/apps/web/src/components/dash/admin/events/DeleteEventDialogue.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-"use client";
-import {
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogDescription,
- DialogFooter,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { useAction } from "next-safe-action/hooks";
-import { deleteEventAction } from "@/actions/events/delete";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-export default function DeleteEventDialog({
- id,
- name,
- setOpen,
-}: {
- id: string;
- name: string;
- setOpen: React.Dispatch>;
-}) {
- const { refresh } = useRouter();
- const { status: deleteEventStatus, execute: runDeleteEvent } = useAction(
- deleteEventAction,
- {
- onSuccess: () => {
- setOpen(false);
- toast.dismiss();
- toast.success(`"${name}"Event deleted.`);
- refresh();
- console.log("refreshed");
- },
- onError: () => {
- setOpen(false);
- toast.dismiss();
- toast.error(`An error occured attempting to delete "${name}"`);
- },
- },
- );
-
- const isLoading = deleteEventStatus === "executing";
-
- return (
-
-
- {`Are you sure you want to delete "${name}"?`}
-
- This action cannot be undone. All of the checkins for this
- event will also be deleted.
-
-
-
- {
- toast.dismiss();
- toast.loading(`Deleting "${name}"...`);
- runDeleteEvent(id);
- }}
- disabled={isLoading}
- >
- Delete
-
-
-
- );
-}
diff --git a/apps/web/src/components/dash/admin/events/EditEventForm.tsx b/apps/web/src/components/dash/admin/events/EditEventForm.tsx
deleted file mode 100644
index 2264e5b9..00000000
--- a/apps/web/src/components/dash/admin/events/EditEventForm.tsx
+++ /dev/null
@@ -1,659 +0,0 @@
-"use client";
-import {
- Form,
- FormField,
- FormItem,
- FormControl,
- FormLabel,
- FormMessage,
- FormDescription,
-} from "@/components/ui/form";
-import {
- MultiSelector,
- MultiSelectorContent,
- MultiSelectorInput,
- MultiSelectorItem,
- MultiSelectorList,
- MultiSelectorTrigger,
-} from "@/components/ui/MultiSelect";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
-import { Switch } from "@/components/ui/switch";
-import { useState, useEffect } from "react";
-import { cn } from "@/lib/utils";
-import { format } from "date-fns";
-import { getLocalTimeZone, parseAbsolute } from "@internationalized/date";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-import { updateEventSchemaFormified as formSchema } from "db/zod";
-import { CalendarWithYears } from "@/components/ui/calendarWithYearSelect";
-import { FormGroupWrapper } from "@/components/shared/form-group-wrapper";
-import { DateTimePicker } from "@/components/ui/date-time-picker/date-time-picker";
-import c from "config";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-import { useAction } from "next-safe-action/hooks";
-import { put } from "@/lib/client/file-upload";
-import { updateEvent } from "@/actions/events/update";
-import { iEvent, uEvent } from "@/lib/types/events";
-import { bucketEventThumbnailBaseUrl } from "config";
-import {
- Select,
- SelectContent,
- SelectTrigger,
- SelectValue,
- SelectItem,
-} from "@/components/ui/select";
-import { Semester } from "db/types";
-import { staticUploads } from "config";
-
-type EditEventFormProps = {
- eventID: string;
- oldValues: uEvent;
- categoryOptions: { [key: string]: string };
- semesterOptions: Semester[];
-};
-// marked to add seemster
-export default function EditEventForm({
- eventID,
- oldValues,
- categoryOptions,
- semesterOptions,
-}: EditEventFormProps) {
- const [error, setError] = useState<{
- title: string;
- description: string;
- } | null>(null);
- const router = useRouter();
- const form = useForm>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- ...oldValues,
- },
- });
- const [thumbnail, setThumbnail] = useState(null);
- const [hasDifferentCheckinTime, setHasDifferentCheckinTime] = useState(
- oldValues.start != oldValues.checkinStart ||
- oldValues.end != oldValues.checkinEnd,
- );
-
- function validateAndSetThumbnail(
- event: React.ChangeEvent,
- ) {
- const file = event.target.files?.[0];
- if (!file) {
- setThumbnail(null);
- return false;
- }
- if (file.size > c.thumbnails.maxSizeInBytes) {
- form.setError("thumbnailUrl", {
- message: "thumbnail file is too large.",
- });
- setThumbnail(null);
- return false;
- }
- if (
- ![
- "image/jpeg",
- "image/png",
- "image/gif",
- "image/webp",
- "image/svg+xml",
- "image/bmp",
- ].includes(file.type)
- ) {
- form.setError("thumbnailUrl", {
- message:
- "Invalid image format. Only jpeg, png, gif, webp, svg+xml, bmp.",
- });
- setThumbnail(null);
- return false;
- }
- setThumbnail(file);
- return true;
- }
-
- useEffect(() => {
- if (Object.keys(form.formState.errors).length > 0) {
- console.log("Errors: ", form.formState.errors);
- }
- }, [form.formState]);
-
- const {
- execute: runUpdateEvent,
- status: actionStatus,
- result: actionResult,
- reset: resetAction,
- } = useAction(updateEvent, {
- onSuccess: async ({ data }) => {
- toast.dismiss();
-
- if (!data?.success) {
- const code = data?.code || "unknown";
- switch (code) {
- case "update_event_failed":
- setError({
- title: "Updating event failed",
- description: `Attempt to create event has failed. Please try again or contact ${c.contactEmail}.`,
- });
- break;
- default:
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- setError({
- title: "Some error",
- description: "Error occured",
- });
- break;
- }
- resetAction();
- return;
- }
- toast.success("Event Updated successfully!", {
- description: "You'll be redirected shortly.",
- });
- setTimeout(() => {
- router.push(`/events/${eventID}`);
- }, 1500);
- },
- onError: async (error) => {
- toast.dismiss();
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- console.log("error: ", error);
- resetAction();
- },
- });
-
- const onSubmit = async (values: z.infer) => {
- console.log("Submit: ", values);
- toast.loading("Updating Event...");
- const checkinStart = hasDifferentCheckinTime
- ? values.checkinStart
- : values.start;
- const checkinEnd = hasDifferentCheckinTime
- ? values.checkinEnd
- : values.end;
-
- const categories = values.categories.map(
- (name) => categoryOptions[name],
- );
- const oldCategories = oldValues.categories.map(
- (name) => categoryOptions[name],
- );
-
- if (thumbnail) {
- const thumbnailUrl = await put(
- staticUploads.bucketEventThumbnailBaseUrl,
- thumbnail,
- {
- presignHandlerUrl: "/api/upload/thumbnail",
- },
- );
- runUpdateEvent({
- ...values,
- eventID,
- categories,
- oldCategories,
- thumbnailUrl,
- checkinStart,
- checkinEnd,
- });
- } else {
- runUpdateEvent({
- ...values,
- eventID,
- categories,
- oldCategories,
- checkinStart,
- checkinEnd,
- });
- }
- };
-
- return (
- <>
-
-
-
- {error?.title}
-
- {error?.description}
-
-
-
- setError(null)}>
- Ok
-
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/components/dash/admin/events/EventStatsSheet.tsx b/apps/web/src/components/dash/admin/events/EventStatsSheet.tsx
deleted file mode 100644
index 3130faff..00000000
--- a/apps/web/src/components/dash/admin/events/EventStatsSheet.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from "react";
-import { getEventStatsOverview } from "@/lib/queries/events";
-
-import { Separator } from "@/components/ui/separator";
-
-type Props = {};
-
-async function EventStatsSheet({}: Props) {
- const stats = await getEventStatsOverview();
- return (
-
-
-
- Total Events
-
-
- {stats.totalEvents}
-
-
-
-
- This Week
- {stats.thisWeek}
-
-
-
-
- Past Events
-
-
- {stats.pastEvents}
-
-
-
- );
-}
-
-export default EventStatsSheet;
diff --git a/apps/web/src/components/dash/admin/events/NewEventForm.tsx b/apps/web/src/components/dash/admin/events/NewEventForm.tsx
deleted file mode 100644
index b30a31d9..00000000
--- a/apps/web/src/components/dash/admin/events/NewEventForm.tsx
+++ /dev/null
@@ -1,632 +0,0 @@
-"use client";
-
-import {
- Form,
- FormField,
- FormItem,
- FormControl,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import {
- MultiSelector,
- MultiSelectorContent,
- MultiSelectorInput,
- MultiSelectorItem,
- MultiSelectorList,
- MultiSelectorTrigger,
-} from "@/components/ui/MultiSelect";
-import {
- AlertDialog,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
-import { Switch } from "@/components/ui/switch";
-import { useEffect, useState } from "react";
-import { getLocalTimeZone, parseAbsolute } from "@internationalized/date";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-import { insertEventSchemaFormified } from "db/zod";
-import { FormGroupWrapper } from "@/components/shared/form-group-wrapper";
-import { DateTimePicker } from "@/components/ui/date-time-picker/date-time-picker";
-import c, { staticUploads } from "config";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-import { useAction } from "next-safe-action/hooks";
-import { put } from "@/lib/client/file-upload";
-import { createEvent } from "@/actions/events/createNewEvent";
-import { ONE_HOUR_IN_MILLISECONDS } from "@/lib/constants";
-import { bucketEventThumbnailBaseUrl } from "config";
-import type { NewEventFormProps } from "@/lib/types/events";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-
-const formSchema = insertEventSchemaFormified;
-
-export default function NewEventForm({
- defaultDate,
- categoryOptions,
- semesterOptions,
-}: NewEventFormProps) {
- const [error, setError] = useState<{
- title: string;
- description: string;
- } | null>(null);
- const router = useRouter();
-
- const form = useForm>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- name: "",
- description: "",
- start: defaultDate,
- checkinStart: defaultDate,
- end: new Date(defaultDate.getTime() + ONE_HOUR_IN_MILLISECONDS),
- checkinEnd: new Date(
- defaultDate.getTime() + ONE_HOUR_IN_MILLISECONDS,
- ),
- thumbnailUrl: c.thumbnails.default,
- categories: [],
- isUserCheckinable: true,
- points: 1,
- },
- });
- const [thumbnail, setThumbnail] = useState(null);
- const [hasDifferentCheckinTime, setHasDifferentCheckinTime] =
- useState(false);
-
- function validateAndSetThumbnail(
- event: React.ChangeEvent,
- ) {
- const file = event.target.files?.[0];
- if (!file) {
- setThumbnail(null);
- return false;
- }
- if (file.size > c.thumbnails.maxSizeInBytes) {
- form.setError("thumbnailUrl", {
- message: "thumbnail file is too large.",
- });
- setThumbnail(null);
- return false;
- }
- if (!c.thumbnails.acceptedFiles.includes(file.type as any)) {
- form.setError("thumbnailUrl", {
- message:
- "Invalid image format. Only jpeg, png, gif, webp, svg+xml, bmp.",
- });
- setThumbnail(null);
- return false;
- }
- setThumbnail(file);
- return true;
- }
-
- const {
- execute: runCreateEvent,
- status: actionStatus,
- result: actionResult,
- reset: resetAction,
- } = useAction(createEvent, {
- onSuccess: async ({ data }) => {
- toast.dismiss();
- if (!data) {
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- resetAction();
- return;
- }
- const { success, code, eventID } = data;
- if (!success) {
- switch (code) {
- case "insert_event_failed":
- setError({
- title: "Creating event failed",
- description: `Attempt to create event has failed. Please try again or contact ${c.contactEmail}.`,
- });
- break;
- default:
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- setError({
- title: "Some error",
- description: "Error occured",
- });
- break;
- }
- resetAction();
- return;
- }
- toast.success("Event Created successfully!", {
- description: "You'll be redirected shortly.",
- });
- setTimeout(() => {
- router.push(`/events/${eventID}`);
- }, 1500);
- },
- onError: async (error) => {
- toast.dismiss();
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- resetAction();
- },
- });
-
- const onSubmit = async (values: z.infer) => {
- toast.loading("Creating Event...");
- const checkinStart = hasDifferentCheckinTime
- ? values.checkinStart
- : values.start;
- const checkinEnd = hasDifferentCheckinTime
- ? values.checkinEnd
- : values.end;
- // Come back and make cleaner
- if (thumbnail) {
- const thumbnailBlob = await put(
- staticUploads.bucketEventThumbnailBaseUrl,
- thumbnail,
- {
- presignHandlerUrl: "/api/upload/thumbnail",
- },
- );
- runCreateEvent({
- ...values,
- thumbnailUrl: thumbnailBlob,
- checkinStart,
- checkinEnd,
- categories: values.categories.map(
- (cat) => categoryOptions[cat],
- ),
- });
- } else {
- runCreateEvent({
- ...values,
- checkinStart,
- checkinEnd,
- categories: values.categories.map(
- (cat) => categoryOptions[cat],
- ),
- });
- }
- };
-
- return (
- <>
-
-
-
- {error?.title}
-
- {error?.description}
-
-
-
- setError(null)}>
- Ok
-
-
-
-
-
-
-
-
- (
-
- Name
-
-
-
-
-
- )}
- />
- (
-
- Description
-
-
-
-
-
- )}
- />
- (
-
- Thumbnail
-
- {
- const success =
- validateAndSetThumbnail(
- event,
- );
- if (!success) {
- event.target.value = "";
- }
- }}
- />
-
-
-
- )}
- />
-
-
-
- (
-
- Start
-
- {
- field.onChange(
- !!date
- ? date.toDate(
- getLocalTimeZone(),
- )
- : null,
- );
- }}
- shouldCloseOnSelect={false}
- granularity={"minute"}
- label="Event Start"
- />
-
-
-
- )}
- />
- (
-
- End
-
- {
- field.onChange(
- !!date
- ? date.toDate(
- getLocalTimeZone(),
- )
- : null,
- );
- }}
- shouldCloseOnSelect={false}
- granularity={"minute"}
- label="Event End"
- />
-
-
-
- )}
- />
-
-
-
- Use Different Check-In Time?
-
- {
- setHasDifferentCheckinTime(
- (prev) => !prev,
- );
- }}
- aria-readonly
- />
-
- {hasDifferentCheckinTime && (
-
- (
-
-
- Check-In Start
-
-
- {
- field.onChange(
- !!date
- ? date.toDate(
- getLocalTimeZone(),
- )
- : null,
- );
- }}
- shouldCloseOnSelect={
- false
- }
- granularity={"minute"}
- label="Check-In Start"
- />
-
-
-
- )}
- />
- (
-
-
- Check-In End
-
-
- {
- field.onChange(
- !!date
- ? date.toDate(
- getLocalTimeZone(),
- )
- : null,
- );
- }}
- shouldCloseOnSelect={
- false
- }
- granularity={"minute"}
- label="Check-In End"
- />
-
-
-
- )}
- />
-
- )}
- (
-
- Location
-
-
-
- )}
- />
-
-
- {semesterOptions.length > 0 && (
- (
-
- Semester
-
- {
- console.log(value);
- field.onChange(
- parseInt(value, 10),
- );
- }}
- >
-
-
-
-
- {semesterOptions.map(
- ({
- name,
- semesterID,
- }) => (
-
- {name}
-
- ),
- )}
-
-
-
-
-
- )}
- />
- )}
- (
-
- Categories
-
-
-
-
-
-
- {Object.entries(
- categoryOptions,
- ).map(([name, id]) => (
-
- {name}
-
- ))}
-
-
-
-
- )}
- />
- (
-
- Points
-
- {
- const parsedPoints =
- parseInt(
- e.target.value,
- 10,
- );
- const points =
- parsedPoints < 1
- ? 1
- : parsedPoints;
- field.onChange(points);
- }}
- />
-
-
-
- )}
- />
- (
-
- Check-In
-
-
-
- )}
- />
- (
-
- Hidden
-
-
-
- )}
- />
-
-
-
- {form.formState.errors.root?.message}
-
-
-
- Submit
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/components/dash/admin/events/ViewQRCode.tsx b/apps/web/src/components/dash/admin/events/ViewQRCode.tsx
deleted file mode 100644
index 0f4e5795..00000000
--- a/apps/web/src/components/dash/admin/events/ViewQRCode.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import QRCode from "react-qr-code";
-import { useRef } from "react";
-import { toast } from "sonner";
-
-type ViewQRCodeProps = {
- id?: string;
- name?: string;
- description?: string;
- basePath?: string;
-};
-
-export default function ViewQRCode(props: ViewQRCodeProps) {
- const { id, name, description, basePath } = props;
- const qrCodeID = `${name}-QRCode`;
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d")!;
- const img = new Image();
-
- const copyToClipboard = async () => {
- const svg = document.getElementById(qrCodeID);
- if (!svg) throw new Error("QR Code not found");
- img.onload = () => {
- canvas.width = img.width;
- canvas.height = img.height;
- ctx.drawImage(img, 0, 0);
- canvas.toBlob(async (blob) => {
- console.log(blob);
- await navigator.clipboard.write([
- new ClipboardItem({ "image/png": blob! }),
- ]);
- });
- };
- img.src =
- "data:image/svg+xml;base64," +
- btoa(new XMLSerializer().serializeToString(svg));
- };
- const download = (fileName: string) => {
- toast.loading("Downloading QR Code...");
- try {
- const svg = document.getElementById(qrCodeID);
- if (!svg) return toast.error("QR Code not found");
- const svgData = new XMLSerializer().serializeToString(svg);
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
- if (!ctx) return toast.error("Failed to create canvas context");
- img.onload = () => {
- canvas.width = img.width;
- canvas.height = img.height;
- ctx.drawImage(img, 0, 0);
- const pngFile = canvas.toDataURL("image/png");
- const downloadLink = document.createElement("a");
- downloadLink.download = fileName;
- downloadLink.href = `${pngFile}`;
- downloadLink.click();
- toast.dismiss();
- toast.success("QR Code downloaded");
- };
- img.src = `data:image/svg+xml;base64,${btoa(svgData)}`;
- } catch (e) {
- toast.dismiss();
- console.log(e);
- toast.error("Failed to download QR Code");
- }
- };
-
- return (
- <>
-
-
- e.stopPropagation()}>QR Code
-
-
-
- {name}
- {description}
-
-
-
-
-
- {
- toast.promise(copyToClipboard(), {
- loading: "Copying QR Code...",
- success: "QR Code copied",
- error: "Failed to copy QR Code",
- });
- }}
- >
- Copy to Clipboard
-
- {
- download(qrCodeID);
- }}
- >
- Download
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/components/dash/admin/members/MemberPage.tsx b/apps/web/src/components/dash/admin/members/MemberPage.tsx
deleted file mode 100644
index 7a1ffe32..00000000
--- a/apps/web/src/components/dash/admin/members/MemberPage.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { Info } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import Link from "next/link";
-import {
- AccountInfo,
- PersonalInfo,
-} from "@/components/dash/admin/members/ServerSections";
-import UpdateRoleDialogue from "@/components/dash/shared/UpdateRoleDialogue";
-import { Dialog } from "@/components/ui/dialog";
-import { getUser } from "@/lib/queries/users";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-
-export type userType = NonNullable>>;
-
-interface MemberPageProps {
- user: userType;
- clerkUserImage?: string;
-}
-
-export default async function MemberPage({
- user,
- clerkUserImage,
-}: MemberPageProps) {
- const [open, setOpen] = useState(false);
-
- return (
-
-
-
-
-
-
- Email Hacker
-
- setOpen(true)}>
- Update Role
-
-
-
-
-
-
-
-
-
- Profile Photo for {user.firstName}
- {user.lastName}
-
-
-
-
-
- {user.firstName}
-
-
- {user.lastName}
-
-
-
- {user.universityID}
-
-
{user.role}
-
- Joined{" "}
- {user.joinDate
- .toDateString()
- .split(" ")
- .slice(1)
- .join(" ")}
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/dash/admin/members/MemberStatsSheet.tsx b/apps/web/src/components/dash/admin/members/MemberStatsSheet.tsx
deleted file mode 100644
index c1611c77..00000000
--- a/apps/web/src/components/dash/admin/members/MemberStatsSheet.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from "react";
-import { getMemberStatsOverview } from "@/lib/queries/users";
-
-import { Separator } from "@/components/ui/separator";
-
-type Props = {};
-
-async function MemberStatsSheet({}: Props) {
- const stats = await getMemberStatsOverview();
- return (
-
-
-
- Total Members
-
-
- {stats.totalMembers}
-
-
-
- {/* Put recent registration count here */}
- {/*
- This Week
- {stats.thisWeek}
-
-
*/}
-
-
- Active Members
-
-
- {stats.activeMembers}
-
-
-
- );
-}
-
-export default MemberStatsSheet;
diff --git a/apps/web/src/components/dash/admin/members/ServerSections.tsx b/apps/web/src/components/dash/admin/members/ServerSections.tsx
deleted file mode 100644
index 28ab851f..00000000
--- a/apps/web/src/components/dash/admin/members/ServerSections.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import UserInfoSection from "@/components/dash/admin/members/UserInfoSection";
-import type { userType } from "@/components/dash/admin/members/MemberPage";
-import Link from "next/link";
-
-interface PersonalInfoProps {
- user: userType;
-}
-
-export function PersonalInfo({ user }: PersonalInfoProps) {
- return (
-
-
-
|
-
|
-
|
-
|
-
|
-
|
-
|
-
|
-
-
Resume
- {user.data.resume ? (
-
- View User Resume
-
- ) : (
-
N/A
- )}
-
-
-
- );
-}
-
-interface AccountInfoProps {
- user: userType;
-}
-
-export function AccountInfo({ user }: AccountInfoProps) {
- return (
-
-
- |
- |
- |
-
-
- );
-}
-
-function Cell({
- title,
- value,
-}: {
- title: string;
- value: string | number | boolean;
-}) {
- return (
-
-
{title}
-
{value.toString()}
-
- );
-}
diff --git a/apps/web/src/components/dash/admin/members/UserInfoSection.tsx b/apps/web/src/components/dash/admin/members/UserInfoSection.tsx
deleted file mode 100644
index 503edf2f..00000000
--- a/apps/web/src/components/dash/admin/members/UserInfoSection.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-interface UserInfoSectionProps {
- children?: React.ReactNode;
- title: string;
- subtitle?: string;
-}
-
-export default function UserInfoSection({
- children,
- title,
-}: UserInfoSectionProps) {
- return (
-
-
-
{title}
- {/*
-
-
*/}
-
- {children ? children : null}
-
- );
-}
diff --git a/apps/web/src/components/dash/admin/overview/DemographicsStats.tsx b/apps/web/src/components/dash/admin/overview/DemographicsStats.tsx
deleted file mode 100644
index e368d93c..00000000
--- a/apps/web/src/components/dash/admin/overview/DemographicsStats.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-"use client";
-
-import React from "react";
-
-import { Label, Pie, PieChart } from "recharts";
-
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-
-import {
- ChartConfig,
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
- ChartLegend,
- ChartLegendContent,
-} from "@/components/ui/chart";
-
-type Props = {
- classifications: {
- classification: string;
- count: number;
- fill?: string;
- }[];
-};
-
-const classChartConfig = {
- freshman: {
- label: "Freshman",
- color: "hsl(var(--chart-1))",
- },
- sophomore: {
- label: "Sophomore",
- color: "hsl(var(--chart-2))",
- },
- sophmore: {
- label: "Sophmore 😭",
- color: "hsl(173 58% 75%)",
- },
- junior: {
- label: "Junior",
- color: "hsl(var(--chart-3))",
- },
- senior: {
- label: "Senior",
- color: "hsl(var(--chart-4))",
- },
- graduate: {
- label: "Graduate",
- color: "hsl(var(--chart-5))",
- },
-} satisfies ChartConfig;
-
-function DemographicsStats({ classifications }: Props) {
- return (
-
-
-
-
- Classifications
-
-
-
-
- }
- />
-
-
- }
- className="-translate-y-2 flex-wrap gap-2 [&>*]:basis-1/4 [&>*]:justify-center"
- />
-
-
-
-
-
-
- );
-}
-
-export default DemographicsStats;
diff --git a/apps/web/src/components/dash/admin/overview/MonthlyRegistrationChart.tsx b/apps/web/src/components/dash/admin/overview/MonthlyRegistrationChart.tsx
deleted file mode 100644
index b4ca4939..00000000
--- a/apps/web/src/components/dash/admin/overview/MonthlyRegistrationChart.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-"use client";
-
-import React from "react";
-
-import { getRegistrationsByMonth } from "@/lib/queries/charts";
-
-import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import {
- ChartConfig,
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
-} from "@/components/ui/chart";
-
-type Props = {
- registrations: {
- month: number;
- count: number;
- }[];
-};
-
-const chartConfig = {
- numRegistered: {
- label: "Registrations",
- color: "hsl(var(--chart-1))",
- },
-} satisfies ChartConfig;
-
-const monthList = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
-];
-
-async function MonthlyRegistrationChart({ registrations }: Props) {
- return (
-
-
- Registrations by Month
-
- Showing registration trends over the last year
-
-
-
-
-
-
-
- monthList[value - 1].slice(0, 3)
- }
- />
- }
- />
-
-
-
-
-
- );
-}
-
-export default MonthlyRegistrationChart;
diff --git a/apps/web/src/components/dash/admin/semesters/CreateSemesterDialogue.tsx b/apps/web/src/components/dash/admin/semesters/CreateSemesterDialogue.tsx
deleted file mode 100644
index 5a15fc30..00000000
--- a/apps/web/src/components/dash/admin/semesters/CreateSemesterDialogue.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-"use client";
-
-import {
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogFooter,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { createNewSemester } from "@/actions/semester";
-import { useAction } from "next-safe-action/hooks";
-import { Input } from "@/components/ui/input";
-import { useEffect, useState } from "react";
-import { useForm } from "react-hook-form";
-import { createSemesterSchema } from "db/zod";
-import { Loader2 } from "lucide-react";
-import z from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { Dialog } from "@radix-ui/react-dialog";
-import { Plus } from "lucide-react";
-import { Switch } from "@/components/ui/switch";
-import { SEMESTER_DAYS_OFFSET } from "@/lib/constants/semesters";
-import {
- SEMESTER_DATE_RANGE_EXISTS,
- SEMESTER_NAME_EXISTS,
-} from "@/lib/constants/semesters";
-import { DatePickerWithRange } from "@/components/ui/date-time-picker/date-picker-with-range";
-import { addDays } from "date-fns";
-import { useRouter } from "next/navigation";
-
-export default function CreateSemesterDialogue() {
- const [open, setOpen] = useState(false);
- const form = useForm>({
- resolver: zodResolver(createSemesterSchema),
- defaultValues: {
- name: "",
- startDate: new Date(),
- endDate: addDays(new Date(), SEMESTER_DAYS_OFFSET),
- isCurrent: false,
- pointsRequired: 0,
- },
- });
- const { refresh } = useRouter();
- const { execute: runCreateSemester, status } = useAction(
- createNewSemester,
- {
- onSuccess: ({ data }) => {
- toast.dismiss();
- if (data?.code == SEMESTER_DATE_RANGE_EXISTS) {
- return toast.error(
- `${data?.semesterName} has dates that overlap with the new semester`,
- );
- } else if (data?.code == SEMESTER_NAME_EXISTS) {
- return toast.error(
- `${data?.semesterName} already exists. Please choose a different name`,
- );
- }
- form.reset();
- setOpen(false);
- toast.success("Semester created successfully");
- refresh();
- },
- onError: (err) => {
- if (
- err.error.validationErrors?._errors?.[0] ===
- "Unauthorized (Not a super admin)"
- ) {
- return toast.error(
- "You need super admin permissions to create semesters",
- );
- }
- console.log("error: ", err);
- toast.dismiss();
- toast.error("Failed to create semester");
- },
- },
- );
-
- useEffect(() => {
- console.log("create semester form errors", form.formState.errors);
- console.log("create semester form values", form.getValues());
- }, [form.formState.errors]);
-
- const isLoading = status === "executing";
- return (
-
-
-
-
- Add Semester
-
-
-
-
- Create Event Category
-
-
-
- (
-
- Semester Title
-
-
-
-
-
- )}
- />
-
- (
-
- Semester Duration
- {
- if (range) {
- form.setValue(
- "startDate",
- range.from ??
- new Date(),
- );
- form.setValue(
- "endDate",
- range.to ??
- new Date(
- Date.now() +
- SEMESTER_DAYS_OFFSET,
- ),
- );
- }
- }}
- />
-
-
- )}
- />
-
- (
-
- Current Semester?
-
-
-
-
-
- )}
- />
- (
-
- Points Required
-
-
- field.onChange(
- parseInt(e.target.value),
- )
- }
- type="number"
- placeholder="Enter Points Required"
- />
-
-
-
- )}
- />
-
-
- {isLoading ? (
-
- ) : (
- "Create"
- )}
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/dash/admin/semesters/DeleteSemesterDialogue.tsx b/apps/web/src/components/dash/admin/semesters/DeleteSemesterDialogue.tsx
deleted file mode 100644
index 5a82c131..00000000
--- a/apps/web/src/components/dash/admin/semesters/DeleteSemesterDialogue.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-"use client";
-import { deleteSemester } from "@/actions/semester";
-import { useAction } from "next-safe-action/hooks";
-import {
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogDescription,
- AlertDialogAction,
- AlertDialogFooter,
- AlertDialogTitle,
- AlertDialogCancel,
-} from "@/components/ui/alert-dialog";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-
-export default function DeleteSemesterDialogue({
- semesterID,
- name,
-}: {
- semesterID: number;
- name: string;
-}) {
- const { refresh } = useRouter();
- const { status, execute: runDeleteSemester } = useAction(deleteSemester, {
- onSuccess: () => {
- toast.dismiss();
- toast.success("Semester deleted successfully");
- refresh();
- },
- onError: (err) => {
- if (
- err.error.validationErrors?._errors?.[0] ===
- "Unauthorized (Not a super admin)"
- ) {
- return toast.error(
- "You need super admin permissions to update roles",
- );
- }
- toast.dismiss();
- toast.error("Failed to delete semester");
- },
- });
-
- const isLoading = status === "executing";
-
- return (
-
-
- {`Are you want to delete "${name}"`}
-
- This action cannot be undone and will remove any category
- associations with existing events.
-
-
-
- Cancel
- {
- toast.loading("Deleting event category");
- runDeleteSemester(semesterID);
- }}
- >{`Delete ${name}`}
-
-
- );
-}
diff --git a/apps/web/src/components/dash/admin/semesters/SemesterView.tsx b/apps/web/src/components/dash/admin/semesters/SemesterView.tsx
deleted file mode 100644
index d0613d33..00000000
--- a/apps/web/src/components/dash/admin/semesters/SemesterView.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { DataTable } from "@/components/ui/data-table";
-import { getAllSemesters } from "@/lib/queries/semesters";
-import { semesterColumns } from "@/app/admin/semesters/columns";
-import CreateSemesterDialogue from "./CreateSemesterDialogue";
-
-export default async function AdminSemesterView() {
- const semesters = await getAllSemesters();
-
- return (
- <>
-
-
-
-
- Total Semesters
-
-
- {semesters.length}
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/components/dash/admin/semesters/UpdateSemesterDialogue.tsx b/apps/web/src/components/dash/admin/semesters/UpdateSemesterDialogue.tsx
deleted file mode 100644
index b4af8370..00000000
--- a/apps/web/src/components/dash/admin/semesters/UpdateSemesterDialogue.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-"use client";
-
-import {
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogFooter,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { updateSemester } from "@/actions/semester";
-import { useAction } from "next-safe-action/hooks";
-import { Input } from "@/components/ui/input";
-import { useEffect, useState } from "react";
-import { useForm } from "react-hook-form";
-import { updateSemesterSchema } from "db/zod";
-import { Loader2 } from "lucide-react";
-import z from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { Dialog } from "@radix-ui/react-dialog";
-import { Plus } from "lucide-react";
-import { Switch } from "@/components/ui/switch";
-import {
- SEMESTER_DATE_RANGE_EXISTS,
- SEMESTER_NAME_EXISTS,
-} from "@/lib/constants/semesters";
-import { DatePickerWithRange } from "@/components/ui/date-time-picker/date-picker-with-range";
-import { addDays } from "date-fns";
-import { SEMESTER_DAYS_OFFSET } from "@/lib/constants/semesters";
-import { Semester } from "db/types";
-import { useRouter } from "next/navigation";
-
-type UpdateSemesterProps = {
- semesterData: Semester;
- open: boolean;
- setOpen: React.Dispatch>;
-};
-
-export default function UpdateSemesterDialogue(props: UpdateSemesterProps) {
- const { semesterData: semesterProps, setOpen, open } = props;
- const form = useForm>({
- resolver: zodResolver(updateSemesterSchema),
- defaultValues: {
- ...semesterProps,
- },
- });
- const { refresh } = useRouter();
- const { execute: runUpdateSemester, status } = useAction(updateSemester, {
- onSuccess: ({ data }) => {
- toast.dismiss();
- if (data?.code == SEMESTER_DATE_RANGE_EXISTS) {
- return toast.error(
- `${data?.semesterName} has dates that overlap with the new semester`,
- );
- } else if (data?.code == SEMESTER_NAME_EXISTS) {
- return toast.error(
- `${data?.semesterName} already exists. Please choose a different name`,
- );
- }
- setOpen(false);
- form.reset();
- toast.success("Semester updated successfully");
- refresh();
- },
- onError: (err) => {
- if (
- err.error.validationErrors?._errors?.[0] ===
- "Unauthorized (Not a super admin)"
- ) {
- return toast.error(
- "You need super admin permissions to update roles",
- );
- }
- toast.dismiss();
- toast.error("Failed to update semester");
- },
- });
-
- useEffect(() => {
- console.log("form dirty", form.formState.isDirty);
- }, [form.formState.isDirty]);
-
- function onSubmit(data: z.infer) {
- if (!form.formState.isDirty) {
- return toast.error("No changes made");
- }
- runUpdateSemester(data);
- }
-
- const isLoading = status === "executing";
-
- return (
- <>
-
-
- Update Event Category
-
-
-
- (
-
- Semester Title
-
-
-
-
-
- )}
- />
-
- (
-
- Semester Duration
- {
- if (range) {
- form.setValue(
- "startDate",
- range.from ??
- new Date(),
- {
- shouldDirty: true,
- },
- );
- form.setValue(
- "endDate",
- range.to ??
- new Date(
- Date.now() +
- SEMESTER_DAYS_OFFSET,
- ),
- {
- shouldDirty: true,
- },
- );
- }
- }}
- />
-
-
- )}
- />
-
- (
-
- Points Required
-
-
- field.onChange(
- parseInt(e.target.value),
- )
- }
- type="number"
- placeholder="Enter Points Required"
- />
-
-
-
- )}
- />
-
-
- {isLoading ? (
-
- ) : (
- "Update"
- )}
-
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/components/dash/shared/AddCheckinDialogue.tsx b/apps/web/src/components/dash/shared/AddCheckinDialogue.tsx
deleted file mode 100644
index 0e90d5c7..00000000
--- a/apps/web/src/components/dash/shared/AddCheckinDialogue.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-"use client";
-
-import {
- DialogTitle,
- DialogContent,
- DialogHeader,
- DialogFooter,
-} from "@/components/ui/dialog";
-import { toast } from "sonner";
-import {
- Form,
- FormDescription,
- FormItem,
- FormField,
- FormControl,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { CheckinResult } from "@/lib/types/events";
-import { useForm } from "react-hook-form";
-import { useAction } from "next-safe-action/hooks";
-import React from "react";
-import { adminCheckinSchema } from "db/zod";
-import { adminCheckin } from "@/actions/checkin";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Button } from "@/components/ui/button";
-import { Textarea } from "@/components/ui/textarea";
-import {
- Select,
- SelectTrigger,
- SelectValue,
- SelectContent,
- SelectItem,
-} from "@/components/ui/select";
-import c from "config";
-import z from "zod";
-import { useRouter } from "next/navigation";
-
-type Props = {
- eventList: { id: string; name: string }[];
- setOpen?: React.Dispatch>;
- default?: {
- eventID?: string;
- universityIDs?: string;
- };
-};
-type AdminCheckinProps = z.infer;
-
-function AddCheckinDialogue({ eventList, ...props }: Props) {
- const form = useForm({
- resolver: zodResolver(adminCheckinSchema),
- defaultValues: {
- eventID:
- props.default?.eventID ||
- (eventList.length > 0 ? eventList[0].id : ""),
- universityIDs: props.default?.universityIDs || "",
- },
- });
- const { refresh } = useRouter();
- const {
- execute: runAddCheckin,
- status: actionStatus,
- result: actionResult,
- reset: resetAction,
- } = useAction(adminCheckin, {
- onSuccess: async ({ data }) => {
- if (props.setOpen) props.setOpen(false);
- toast.dismiss();
- if (!data) {
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- resetAction();
- return;
- }
- const { success, code, failedIDs } = data;
- if (!success) {
- switch (code) {
- case CheckinResult.FAILED:
- toast.error("All checkins failed.");
- break;
- case CheckinResult.SOME_FAILED:
- toast.warning(
- `The following checkins failed: ${failedIDs?.join()}`,
- );
- break;
- default:
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- break;
- }
- return;
- }
- toast.success("Checkins Successfully Added!");
- resetAction();
- refresh();
- },
- onError: async (error) => {
- toast.dismiss();
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- console.log("error: ", error);
- resetAction();
- },
- });
-
- function onSubmit(data: AdminCheckinProps, evt: any) {
- evt.preventDefault();
- toast.loading("Creating Checkins...");
- runAddCheckin(data);
- }
-
- return (
- <>
- e.stopPropagation()}>
-
- Add Checkin
-
-
- (
-
- Event
-
-
-
-
-
-
-
- {eventList.map((event) => (
-
- {event.name}
-
- ))}
-
-
-
-
- )}
- />
- (
-
- University ID(s)
-
-
-
-
- Please enter the university ID(s)
- (comma separated) of the event you
- would like to add checkins to.
-
-
-
- )}
- />
-
-
- {eventList.length < 1
- ? "No Events"
- : "Submit"}
-
-
-
-
-
-
- >
- );
-}
-
-export default AddCheckinDialogue;
diff --git a/apps/web/src/components/dash/shared/AdminCheckinLog.tsx b/apps/web/src/components/dash/shared/AdminCheckinLog.tsx
deleted file mode 100644
index 4ae5eb13..00000000
--- a/apps/web/src/components/dash/shared/AdminCheckinLog.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-import { getCheckinLog } from "@/lib/queries/checkins";
-import { DataTable } from "@/components/ui/data-table";
-import { checkinLogColumns } from "./CheckinLogColumns";
-
-type Props = {};
-
-async function AdminCheckinLog({}: Props) {
- const data = await getCheckinLog();
- return (
-
- );
-}
-
-export default AdminCheckinLog;
diff --git a/apps/web/src/components/dash/shared/CheckinLogColumns.tsx b/apps/web/src/components/dash/shared/CheckinLogColumns.tsx
deleted file mode 100644
index e2c4c55d..00000000
--- a/apps/web/src/components/dash/shared/CheckinLogColumns.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-"use client";
-
-import { ColumnDef, Row } from "@tanstack/react-table";
-import Image from "next/image";
-import Link from "next/link";
-import { MoreHorizontal, ArrowUpDown } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { DataTableColumnHeader } from "@/components/ui/data-table";
-import { formatDate } from "date-fns";
-
-const timeFormatString = "eee, MMM dd yyyy HH:mm bb";
-
-type CheckinLogEntry = {
- time: Date;
- feedback: string | null;
- event: {
- name: string;
- };
- author: {
- userID: number;
- firstName: string;
- lastName: string;
- };
- rating: number | null;
-};
-
-const timeCell = ({ row }: { row: Row }) => {
- const formattedDate = formatDate(row.getValue("time"), timeFormatString);
- return {formattedDate}
;
-};
-
-export const checkinLogColumns: ColumnDef[] = [
- {
- accessorKey: "event.name",
- header: ({ column }) => {
- return ;
- },
-
- enableSorting: true,
- },
- {
- accessorKey: "author.userID",
- header: ({ column }) => {
- return ;
- },
-
- enableSorting: true,
- },
- {
- id: "author",
- accessorFn: (row) => `${row.author.firstName} ${row.author.lastName}`,
- header: ({ column }) => {
- return ;
- },
- cell: ({ row }) => {
- return (
-
- {row.original.author.firstName}{" "}
- {row.original.author.lastName}
-
- );
- },
- enableSorting: true,
- },
- {
- accessorKey: "time",
- header: ({ column }) => {
- return ;
- },
- cell: timeCell,
- enableSorting: true,
- },
- {
- accessorKey: "rating",
- id: "rating",
- header: ({ column }) => {
- return ;
- },
- cell: ({ row }) => {
- return (
-
-
{row.getValue("rating") ?? "unrated"}
-
- );
- },
- },
- {
- accessorKey: "feedback",
- header: ({ column }) => {
- return ;
- },
- cell: ({ row }) => {
- return (
-
-
{row.getValue("feedback") ?? ""}
-
- );
- },
- enableSorting: true,
- },
-];
diff --git a/apps/web/src/components/dash/shared/CheckinStatsSheet.tsx b/apps/web/src/components/dash/shared/CheckinStatsSheet.tsx
deleted file mode 100644
index 24edacae..00000000
--- a/apps/web/src/components/dash/shared/CheckinStatsSheet.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from "react";
-import { Separator } from "@/components/ui/separator";
-import { getCheckinStatsOverview } from "@/lib/queries/checkins";
-
-type Props = {};
-
-async function CheckinStatsSheet({}: Props) {
- const stats = await getCheckinStatsOverview();
- return (
-
-
-
- Total Checkins
-
-
- {stats.total_checkins}
-
-
- {/*
-
- This Week
- {stats.thisWeek}
-
-
-
-
- Past Events
-
-
- {stats.pastEvents}
-
-
*/}
-
- );
-}
-
-export default CheckinStatsSheet;
diff --git a/apps/web/src/components/dash/shared/PortalMigrationExplainer.tsx b/apps/web/src/components/dash/shared/PortalMigrationExplainer.tsx
deleted file mode 100644
index efe835ea..00000000
--- a/apps/web/src/components/dash/shared/PortalMigrationExplainer.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from "../../ui/accordion";
-export default function PortalMigrationExplainer() {
- return (
-
-
-
- Why Migrate?
-
-
- If you had a portal account before the swap over to the new
- version, you can migrate your account to the new system.
- This will allow you to keep your account and all of your
- data.
-
-
-
- );
-}
diff --git a/apps/web/src/components/dash/shared/UpdateRoleDialogue.tsx b/apps/web/src/components/dash/shared/UpdateRoleDialogue.tsx
deleted file mode 100644
index 1dad0e83..00000000
--- a/apps/web/src/components/dash/shared/UpdateRoleDialogue.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-"use client";
-import {
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogDescription,
- DialogFooter,
-} from "@/components/ui/dialog";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { updateMemberRole } from "@/actions/member";
-import c from "config";
-import { useAction } from "next-safe-action/hooks";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { useState } from "react";
-import { MemberType } from "@/lib/types/shared";
-import { useRouter } from "next/navigation";
-
-export default function UpdateRoleDialogue({
- userID,
- currentRole,
- setOpen,
-}: {
- userID: number;
- currentRole: MemberType;
- setOpen: React.Dispatch>;
-}) {
- const [role, setRole] = useState(currentRole);
- const { refresh } = useRouter();
- const { execute: runUpdateMemberRole, status } = useAction(
- updateMemberRole,
- {
- onSuccess: () => {
- setOpen(false);
- toast.dismiss();
- toast.success("Role updated");
- refresh();
- },
- onError: (err) => {
- toast.dismiss();
- if (
- err.error.validationErrors?._errors?.[0] ===
- "Unauthorized (Not a super admin)"
- ) {
- return toast.error(
- "You need super admin permissions to update roles",
- );
- }
- toast.error("Failed to update role");
- },
- },
- );
- const isLoading = status === "executing";
- return (
-
-
- Update Member Role
-
- {`Note: Only those with super admin permissions can update roles. If you need this permission, please reach out ${c.contactEmail}`}
-
-
- setRole(v as MemberType)}
- defaultValue={role}
- >
-
-
-
-
- {c.memberRoles.map((role) => {
- return (
-
- {role}
-
- );
- })}
-
-
-
- {
- if (role === currentRole) {
- return toast.error("Choose a different role");
- }
- toast.dismiss();
- toast.loading("Updating role...");
- runUpdateMemberRole({ userID, role });
- }}
- disabled={isLoading}
- >
- Update
-
-
-
- );
-}
diff --git a/apps/web/src/components/events/EventCardComponent.tsx b/apps/web/src/components/events/EventCardComponent.tsx
deleted file mode 100644
index 109d8754..00000000
--- a/apps/web/src/components/events/EventCardComponent.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import type { EventAndCategoriesType } from "@/lib/types/events";
-import Image from "next/image";
-import {
- Card,
- CardContent,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import Link from "next/link";
-import clsx from "clsx";
-import EventCategories from "./EventCategories";
-import { Badge } from "../ui/badge";
-import { formatInTimeZone } from "date-fns-tz";
-import {
- EVENT_DATE_FORMAT_STRING,
- EVENT_TIME_FORMAT_STRING,
-} from "@/lib/constants/events";
-import EventImage from "./shared/EventImage";
-export default function EventCardComponent({
- event,
- isPast,
- isEventCurrentlyHappening,
- isEventCheckinAllowed,
- clientTimezone,
-}: {
- event: EventAndCategoriesType;
- isPast: boolean;
- isEventCurrentlyHappening: boolean;
- clientTimezone: string;
- isEventCheckinAllowed: boolean;
-}) {
- const { thumbnailUrl, start, id, points } = event;
-
- const eventDetailsLink = `/events/${id}`;
- const eventCheckinLink = `/events/${id}/checkin`;
- const startDateFormatted = formatInTimeZone(
- start,
- clientTimezone,
- `${EVENT_DATE_FORMAT_STRING} @ ${EVENT_TIME_FORMAT_STRING}`,
- );
-
- return (
-
-
-
-
-
-
- {event.name}
-
-
-
-
- {`${isPast ? "Ended on: " : ""}`}
- {startDateFormatted}
-
-
- Points:
- {points}
-
-
-
-
-
- Details
-
-
-
-
- Check-In
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/events/EventCategories.tsx b/apps/web/src/components/events/EventCategories.tsx
deleted file mode 100644
index 67878e2c..00000000
--- a/apps/web/src/components/events/EventCategories.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { EventAndCategoriesType } from "@/lib/types/events";
-import { Badge } from "../ui/badge";
-import { cn } from "@/lib/utils";
-import c from "config";
-export default function EventCategories({
- event,
- isPast,
- className,
-}: {
- event: EventAndCategoriesType;
- isPast: boolean;
- className?: string;
-}) {
- return (
-
- {event.eventsToCategories.length > 0 ? (
- event.eventsToCategories.map((category) => {
- // Style is like this for now because of the way tailwind ships, it prevents you from using arbitrary colors that are not known ahead of time
- return (
-
-
- {category.category.name}
-
-
- );
- })
- ) : (
-
- {c.clubName}
-
- )}
- {isPast && (
-
- Past
-
- )}
-
- );
-}
diff --git a/apps/web/src/components/events/EventsCalendarView.tsx b/apps/web/src/components/events/EventsCalendarView.tsx
deleted file mode 100644
index 53783795..00000000
--- a/apps/web/src/components/events/EventsCalendarView.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { EventAndCategoriesType } from "@/lib/types/events";
-
-export default function EventsCalendarView({
- events,
- clientTimeZone,
- currentDateUTC,
-}: {
- events: Array;
- clientTimeZone: string;
- currentDateUTC: Date;
-}) {
- return (
-
-
Events Calendar View
-
- );
-}
diff --git a/apps/web/src/components/events/EventsCardView.tsx b/apps/web/src/components/events/EventsCardView.tsx
deleted file mode 100644
index fe0922b4..00000000
--- a/apps/web/src/components/events/EventsCardView.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { EventAndCategoriesType } from "@/lib/types/events";
-import EventCardComponent from "./EventCardComponent";
-import { ScrollArea } from "../ui/scroll-area";
-import { isAfter } from "date-fns";
-import { isEventCurrentlyHappening, isEventCheckinAllowed } from "@/lib/utils";
-export default function EventsCardView({
- events,
- clientTimeZone,
- currentDateUTC,
-}: {
- events: Array;
- clientTimeZone: string;
- currentDateUTC: Date;
-}) {
- return (
-
-
-
- {events.map((event) => (
-
- ))}
-
-
-
- );
-}
diff --git a/apps/web/src/components/events/EventsTitle.tsx b/apps/web/src/components/events/EventsTitle.tsx
deleted file mode 100644
index 5a6ffc2f..00000000
--- a/apps/web/src/components/events/EventsTitle.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import c from "config";
-export default function EventsTitle() {
- return (
-
-
- Events
-
-
- Check Out What {c.clubName} Has Planned!
-
-
- );
-}
diff --git a/apps/web/src/components/events/EventsView.tsx b/apps/web/src/components/events/EventsView.tsx
deleted file mode 100644
index 35dcf72b..00000000
--- a/apps/web/src/components/events/EventsView.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import EventsCardView from "./EventsCardView";
-import EventsCalendarView from "./EventsCalendarView";
-import { db, like, gte, and, lt } from "db";
-import { events } from "db/schema";
-import type { SearchParams } from "@/lib/types/shared";
-import { EVENT_FILTERS } from "@/lib/constants/events";
-import { unstable_noStore as noStore } from "next/cache";
-import PageError from "../shared/PageError";
-import { headers } from "next/headers";
-import { getClientTimeZone, getUTCDate } from "@/lib/utils";
-import { getRequestContext } from "@cloudflare/next-on-pages";
-
-export default async function EventsView({ params }: { params: SearchParams }) {
- const { VIEW, CARD, SHOW_EVENTS, SHOW_UPCOMING_EVENTS, QUERY, CATEGORIES } =
- EVENT_FILTERS;
-
- const cardViewSelected = params[EVENT_FILTERS.VIEW]
- ? CARD === (params[VIEW] ?? CARD)
- : true;
-
- const showUpcomingEvents = params[SHOW_EVENTS]
- ? SHOW_UPCOMING_EVENTS === (params[SHOW_EVENTS] ?? SHOW_UPCOMING_EVENTS)
- : true;
-
- const currentDateUTC = getUTCDate();
-
- const dateComparison = showUpcomingEvents
- ? gte(events.end, currentDateUTC)
- : lt(events.end, currentDateUTC);
-
- const eventSearch = params[QUERY] ?? "";
- const eventSearchQuery = like(events.name, `%${eventSearch}%`);
- const categories = new Set(params[CATEGORIES]?.split(",") ?? []);
-
- // Currently written like this because of weirdness with the 'where' clause where it cannot be nested far down the 'with' clauses
- noStore();
- const allEvents = await db.query.events
- .findMany({
- with: {
- eventsToCategories: {
- with: {
- category: {
- columns: {
- name: true,
- color: true,
- },
- },
- },
- },
- },
- where: and(eventSearchQuery, dateComparison),
- orderBy: events.start,
- })
- .then((events) => {
- if (categories.size > 0) {
- return events.filter((event) => {
- return event.eventsToCategories.some((eventToCategory) => {
- return categories.has(eventToCategory.category.name);
- });
- });
- }
- return events;
- });
-
- if (allEvents.length < 1) {
- return (
-
- );
- }
-
- const clientTimeZone = getClientTimeZone(getRequestContext().cf.timezone);
-
- return (
-
- {cardViewSelected ? (
-
- ) : (
-
- )}
-
- );
-}
diff --git a/apps/web/src/components/events/filters/EventsOptionsBar.tsx b/apps/web/src/components/events/filters/EventsOptionsBar.tsx
index 3d061d7c..10285c20 100644
--- a/apps/web/src/components/events/filters/EventsOptionsBar.tsx
+++ b/apps/web/src/components/events/filters/EventsOptionsBar.tsx
@@ -14,10 +14,10 @@ export default async function EventsOptionsBar({
}) {
const { VIEW, CARD, SHOW_EVENTS, SHOW_UPCOMING_EVENTS } = EVENT_FILTERS;
- const cardViewSelected = params.view ? CARD === params[VIEW] ?? CARD : true;
+ const cardViewSelected = params.view ? CARD === params[VIEW] : true;
const showUpcomingEvents = params[SHOW_EVENTS]
- ? SHOW_UPCOMING_EVENTS === params[SHOW_EVENTS] ?? SHOW_UPCOMING_EVENTS
+ ? SHOW_UPCOMING_EVENTS === params[SHOW_EVENTS]
: true;
const categories = await getAllCategories();
diff --git a/apps/web/src/components/events/id/EventDetails.tsx b/apps/web/src/components/events/id/EventDetails.tsx
deleted file mode 100644
index 09df76ef..00000000
--- a/apps/web/src/components/events/id/EventDetails.tsx
+++ /dev/null
@@ -1,326 +0,0 @@
-import { getEventDetails } from "@/lib/queries/events";
-import PageError from "../../shared/PageError";
-import EventImage from "../shared/EventImage";
-import { headers } from "next/headers";
-import { TWENTY_FOUR_HOURS, ONE_HOUR_IN_MILLISECONDS } from "@/lib/constants";
-import c from "config";
-import {
- getClientTimeZone,
- getUTCDate,
- isEventCurrentlyHappening,
-} from "@/lib/utils";
-import { differenceInHours, isAfter } from "date-fns";
-import { formatInTimeZone } from "date-fns-tz";
-import {
- EVENT_DATE_FORMAT_STRING,
- EVENT_TIME_FORMAT_STRING,
-} from "@/lib/constants/events";
-import EventDetailsLiveIndicator from "../shared/EventDetailsLiveIndicator";
-import EventCategories from "../EventCategories";
-import {
- BellRing,
- Calendar,
- Clock,
- CircleArrowUp,
- Hourglass,
- MapPin,
- MonitorPlay,
- UserRoundCheck,
-} from "lucide-react";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import StreamingLink from "./StreamingLink";
-import CalendarLink from "./CalendarLink";
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from "@/components/ui/accordion";
-import Link from "next/link";
-
-const {
- streamingLinks,
- calendarLinks,
- events: { checkingInInfo, aboutOrg },
-} = c;
-import iCalIcon from "../../../../public/img/logos/ical-icon.svg";
-import Image from "next/image";
-import { ics } from "calendar-link";
-import { getRequestContext } from "@cloudflare/next-on-pages";
-
-export default async function EventDetails({
- id,
- isBroswerSafari,
-}: {
- id: string;
- isBroswerSafari: boolean;
-}) {
- const clientTimeZone = getClientTimeZone(getRequestContext().cf.timezone);
- const event = await getEventDetails(id);
-
- if (!event) {
- return ;
- }
- const { start, end, checkinStart, checkinEnd } = event;
- const currentDateUTC = getUTCDate();
- const isEventPassed = isAfter(currentDateUTC, end);
- const isEventHappening = isEventCurrentlyHappening(
- currentDateUTC,
- start,
- end,
- );
-
- const startTime = formatInTimeZone(
- start,
- clientTimeZone,
- `${EVENT_TIME_FORMAT_STRING}`,
- );
-
- const startDateFormatted = formatInTimeZone(
- start,
- clientTimeZone,
- `${EVENT_DATE_FORMAT_STRING}`,
- );
- const rawEventDuration =
- (end.getTime() - start.getTime()) / ONE_HOUR_IN_MILLISECONDS;
-
- const isEventLongerThanADay = rawEventDuration > TWENTY_FOUR_HOURS;
-
- const formattedEventDuration = isEventLongerThanADay
- ? (rawEventDuration / TWENTY_FOUR_HOURS).toFixed(2) + " day(s)"
- : rawEventDuration.toFixed(2) + " hour(s)";
-
- const checkInUrl = `/events/${event.id}/checkin`;
-
- const isCheckinAvailable =
- checkinStart <= currentDateUTC && currentDateUTC <= checkinEnd;
-
- const checkInMessage = isCheckinAvailable
- ? "Ready to check-in? Click here!"
- : isEventPassed
- ? "Check-in is closed"
- : `Check-in starts on ${formatInTimeZone(start, clientTimeZone, `${EVENT_TIME_FORMAT_STRING} @${EVENT_DATE_FORMAT_STRING}`)}`;
-
- const eventCalendarLink = {
- title: event.name,
- description: event.description,
- start: event.start.toISOString(),
- end: event.end.toISOString(),
- location: event.location,
- };
-
- const detailsProps = {
- event,
- startTime,
- startDate: startDateFormatted,
- formattedEventDuration,
- checkInUrl,
- checkInMessage,
- eventCalendarLink,
- isEventPassed,
- isCheckinAvailable,
- isEventHappening,
- };
-
- const { thumbnailUrl, location, description, points } = event;
- const width = 500;
- const height = 500;
-
- return (
-
-
- {event.name}
-
-
-
-
-
-
-
-
-
-
- Description
-
-
- {description}
-
-
-
-
-
-
-
-
{startDateFormatted}
-
-
-
-
-
- {formattedEventDuration}
-
-
-
-
-
-
-
-
-
- {event.points}
- {" "}
- pt{event.points != 1 ? "s" : ""}
-
-
-
-
-
-
-
-
-
-
-
-
- {streamingLinks.map((link) => (
-
- ))}
-
-
-
-
-
-
-
-
-
- {calendarLinks.map((cal) => (
-
- ))}
- {isBroswerSafari ? (
-
-
-
- {"iCal"}
-
-
- ) : (
-
-
-
- {"iCal"}
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- About ACM
-
-
-
- {aboutOrg}
-
-
-
-
-
-
-
-
- Checking In
-
-
-
- {checkingInInfo}
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/events/id/checkin/EventCheckin.tsx b/apps/web/src/components/events/id/checkin/EventCheckin.tsx
deleted file mode 100644
index e3789820..00000000
--- a/apps/web/src/components/events/id/checkin/EventCheckin.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import PageError from "../../../shared/PageError";
-import { getEventById } from "@/lib/queries/events";
-import { getUserDataAndCheckin } from "@/lib/queries/users";
-import EventCheckinForm from "./EventCheckinForm";
-import { getClientTimeZone } from "@/lib/utils";
-import { headers } from "next/headers";
-import { formatInTimeZone } from "date-fns-tz";
-import { isAfter } from "date-fns";
-import {
- EVENT_TIME_FORMAT_STRING,
- EVENT_DATE_FORMAT_STRING,
-} from "@/lib/constants/events";
-import { getRequestContext } from "@cloudflare/next-on-pages";
-export default async function EventCheckin({
- eventID,
- clerkId,
- currentDateUTC,
-}: {
- eventID: string;
- clerkId: string;
- currentDateUTC: Date;
-}) {
- const event = await getEventById(eventID);
-
- const clientTimeZone = getClientTimeZone(getRequestContext().cf.timezone);
-
- if (!event) {
- return ;
- }
-
- const href = `/events/${event.id}`;
-
- const isPassed = isAfter(currentDateUTC, event.end);
-
- if (isPassed) {
- return ;
- }
- const userEventData = await getUserDataAndCheckin(eventID, clerkId);
-
- if (!userEventData) {
- return (
-
- );
- }
-
- const { userID, checkins } = userEventData;
-
- if (checkins.length > 0) {
- return ;
- }
-
- const isCheckinAvailable =
- event.checkinStart <= currentDateUTC &&
- currentDateUTC <= event.checkinEnd;
- if (!isCheckinAvailable) {
- return (
-
- );
- }
-
- return (
-
-
-
Thanks for attending
- {`${event.name}`}
-
-
-
- );
-}
diff --git a/apps/web/src/components/events/id/checkin/EventCheckinForm.tsx b/apps/web/src/components/events/id/checkin/EventCheckinForm.tsx
deleted file mode 100644
index 9f7f7d8e..00000000
--- a/apps/web/src/components/events/id/checkin/EventCheckinForm.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-"use client";
-
-import {
- Form,
- FormField,
- FormItem,
- FormControl,
- FormLabel,
- FormMessage,
- FormDescription,
-} from "@/components/ui/form";
-import { Toaster } from "sonner";
-import { Textarea } from "@/components/ui/textarea";
-import { Button } from "@/components/ui/button";
-import z from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import { userCheckinSchemaFormified } from "db/zod";
-import { useAction } from "next-safe-action/hooks";
-import c from "config";
-import React, { useEffect, useState } from "react";
-import { X, Check } from "lucide-react";
-import { toast } from "sonner";
-import { checkInUserAction } from "@/actions/checkin";
-import { useRouter } from "next/navigation";
-import type { CheckInUserClientProps } from "db/types";
-import RatingStars from "./RatingStars";
-import { CheckinResult } from "@/lib/types/events";
-
-export default function EventCheckinForm({
- eventID,
- userID,
-}: {
- eventID: string;
- userID: number;
-}) {
- const maxCheckinDescriptionLength = c.maxCheckinDescriptionLength;
- const [feedbackLengthMessage, setFeedbackLengthMessage] = useState(
- `0 / ${maxCheckinDescriptionLength} characters`,
- );
- const userCheckinForm = useForm>(
- {
- resolver: zodResolver(userCheckinSchemaFormified),
- defaultValues: {
- feedback: "",
- rating: 0,
- eventID,
- userID,
- },
- },
- );
-
- const { ALREADY_CHECKED_IN } = CheckinResult;
-
- const { push } = useRouter();
-
- const {
- execute: runCheckInUser,
- status: checkInUserStatus,
- result: checkInUserResult,
- reset: resetCheckInUser,
- } = useAction(checkInUserAction, {
- onSuccess: async ({ data }) => {
- toast.dismiss();
- const success = data?.success;
- const code = data?.code;
-
- if (!success) {
- const msg =
- code === ALREADY_CHECKED_IN
- ? "You have already checked in."
- : "Something went wrong checking in user.";
- toast.error(msg, {
- duration: Infinity,
- cancel: {
- label: "Close",
- // cancel object requires an onclick so a blank one is passed
- onClick: () => {},
- },
- });
- if (code === ALREADY_CHECKED_IN) {
- setTimeout(() => {
- push(`/events`);
- }, 2000);
- }
- return;
- }
- toast.success("Thanks for stopping by. See you next time!", {
- duration: Infinity,
- description: "Redirecting to events page...",
- });
- setTimeout(() => {
- push("/events");
- }, 2500);
- },
- onError: async ({ error: e }) => {
- toast.dismiss();
- if (e.validationErrors) {
- toast.error(`Please check your input. ${e.validationErrors}`, {
- duration: Infinity,
- cancel: {
- label: "Dismiss",
- onClick: () => {},
- },
- });
- } else {
- console.log(e.serverError);
- toast.error(`Something went wrong checking in user.`, {
- duration: Infinity,
- cancel: {
- label: "Close",
- // cancel object requires an onclick so a blank one is passed
- onClick: () => {},
- },
- });
- }
- },
- });
-
- const onSubmit = async (checkInValues: CheckInUserClientProps) => {
- toast.dismiss();
- resetCheckInUser();
-
- toast.loading("Checking in...");
- runCheckInUser({
- ...checkInValues,
- userID,
- eventID,
- });
- };
-
- const isSuccess =
- checkInUserStatus === "hasSucceeded" && checkInUserResult.data?.success;
- const isError = checkInUserStatus === "hasErrored";
- useEffect(() => {
- console.log(userCheckinForm.formState.errors);
- }, [userCheckinForm.formState.errors]);
- return (
- <>
-
-
-
-
(
-
-
- {"Rating"}
-
-
-
-
-
-
- )}
- />
- (
-
-
- {"Feedback (Optional)"}
-
-
- Please let us know what we can work on
- to make the event better.
-
-
- {
- setFeedbackLengthMessage(
- `${e.target.value.length} / ${maxCheckinDescriptionLength} characters`,
- );
- field.onChange(e);
- }}
- />
-
- {feedbackLengthMessage}
-
-
-
- )}
- />
-
-
- Check In
-
- {isSuccess ? (
-
- ) : isError ? (
-
- ) : null}
-
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/components/events/shared/EventDetailsLiveIndicator.tsx b/apps/web/src/components/events/shared/EventDetailsLiveIndicator.tsx
deleted file mode 100644
index 3e4347ef..00000000
--- a/apps/web/src/components/events/shared/EventDetailsLiveIndicator.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-export default function EventDetailsLiveIndicator({
- className,
-}: {
- className?: string;
-}) {
- return (
-
- );
-}
diff --git a/apps/web/src/components/landing/globe.tsx b/apps/web/src/components/landing/globe.tsx
new file mode 100644
index 00000000..89b86429
--- /dev/null
+++ b/apps/web/src/components/landing/globe.tsx
@@ -0,0 +1,137 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { cn } from "@/lib/utils";
+
+export default function Globe() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+interface CircleDotsProps {
+ numberOfDots?: number;
+ dotSize?: number;
+ speed?: number;
+ circleRadius?: number;
+ circleColor?: string;
+ dotColor?: string;
+ className?: string;
+}
+
+function CircleDots({
+ numberOfDots = 5,
+ dotSize = 6,
+ speed = 0.01,
+ circleRadius = 80,
+ circleColor = "white",
+ dotColor = "white",
+ className,
+}: CircleDotsProps) {
+ const [mounted, setMounted] = useState(false);
+ const [angle, setAngle] = useState(0);
+
+ // Use a fixed viewBox size
+ const viewBoxSize = 500; // Fixed size for consistent scaling
+ const centerPoint = viewBoxSize / 2;
+ const maxRadius = viewBoxSize / 2 - dotSize; // Maximum allowed radius
+
+ // Scale the circle radius to fit within the viewBox
+ const scaledRadius = Math.min(circleRadius, maxRadius);
+
+ // Only start animation after component is mounted
+ useEffect(() => {
+ setMounted(true);
+ let lastTime = performance.now();
+ let frameId: number;
+
+ const animate = (currentTime: number) => {
+ const deltaTime = (currentTime - lastTime) / 1000; // Convert to seconds
+ lastTime = currentTime;
+ // Update angle and schedule next frame
+ setAngle(
+ (prevAngle) =>
+ (prevAngle + speed * deltaTime * 120) % (Math.PI * 2),
+ );
+ frameId = requestAnimationFrame(animate);
+ };
+
+ // Kick off the animation loop
+ frameId = requestAnimationFrame(animate);
+ // Cleanup the most recent frame
+ return () => cancelAnimationFrame(frameId);
+ }, [speed]);
+
+ // Generate dots
+ const dots = Array.from({ length: numberOfDots }).map((_, index) => {
+ const dotAngle = angle + (index * (Math.PI * 2)) / numberOfDots;
+ const x = centerPoint + scaledRadius * Math.cos(dotAngle);
+ const y = centerPoint + scaledRadius * Math.sin(dotAngle);
+ return { x, y, id: index };
+ });
+
+ return (
+
+
+
+ {/* Base circle with white border */}
+
+
+ {/* Animated dots - only render on client side */}
+ {mounted &&
+ dots.map((dot) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/onboarding/migrator.tsx b/apps/web/src/components/onboarding/migrator.tsx
deleted file mode 100644
index 495fabe2..00000000
--- a/apps/web/src/components/onboarding/migrator.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-"use client";
-
-import { doPortalLookupCheck, doPortalLink } from "@/actions/register/migrate";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { useAction } from "next-safe-action/hooks";
-import c from "config";
-import { useState } from "react";
-import { toast } from "sonner";
-import Link from "next/link";
-
-// mobile could be touched up a bit
-export default function Migrator({ clerkEmail }: { clerkEmail: string }) {
- const [stage, setStage] = useState<
- "lookup" | "confirmation" | "notfound" | "success"
- >("lookup");
- const [legacyEmail, setLegacyEmail] = useState(null);
- const [universityID, setUniversityID] = useState(null);
-
- const {
- execute: runDoPortalLookup,
- result: portalLookupResult,
- status: portalLookupStatus,
- reset: resetPortalLookupAction,
- } = useAction(doPortalLookupCheck, {
- onSuccess({ data }) {
- toast.dismiss();
- if (data?.success) {
- setStage("confirmation");
- } else {
- setLegacyEmail(null);
- setUniversityID(null);
- setStage("notfound");
- resetPortalLookupAction();
- }
- },
- onError() {
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- },
- });
-
- const { execute: runDoPortalLink, status: portalLinkStatus } = useAction(
- doPortalLink,
- {
- onSuccess() {
- toast.dismiss();
- setStage("success");
- },
- onError(error) {
- console.log("error: ", error);
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- },
- },
- );
-
- function dbLookup() {
- if (
- !legacyEmail ||
- !universityID ||
- legacyEmail.length === 0 ||
- universityID.length === 0 ||
- universityID.length > c.universityID.maxLength
- ) {
- toast.error("Please enter a valid email and university ID");
- return;
- }
-
- toast.loading("Looking up your portal account...");
- runDoPortalLookup({
- email: legacyEmail,
- universityID: universityID,
- });
- }
-
- return (
-
- {stage === "lookup" && (
- <>
-
-
- Please enter the {c.universityID.name} and email
- address of your legacy portal account.
-
-
-
- {c.universityID.name}
-
- setUniversityID(
- e.target.value && e.target.value.length > 0
- ? e.target.value
- : null,
- )
- }
- className="mb-4 mt-1 w-full"
- placeholder={c.universityID.name}
- type="text"
- maxLength={c.universityID.maxLength}
- />
- Email
-
- setLegacyEmail(
- e.target.value && e.target.value.length > 0
- ? e.target.value
- : null,
- )
- }
- className="mb-4 mt-1 w-full"
- placeholder="email@example.com"
- type="text"
- />
-
- Find Portal Account
-
-
- >
- )}
- {stage === "confirmation" && (
-
-
- Account Found!
-
-
- We found a Portal account under the name{" "}
- {portalLookupResult.data?.name} .
-
-
{
- if (
- !legacyEmail ||
- !universityID ||
- legacyEmail.length === 0 ||
- universityID.length === 0 ||
- universityID.length > c.universityID.maxLength
- ) {
- toast.error(
- `A state desync has occurred. Please try again or contact ${c.contactEmail}.`,
- );
- return;
- }
- toast.loading("Migrating your account...");
- runDoPortalLink({
- universityID: universityID,
- email: legacyEmail,
- });
- }}
- >
- Migrate Account
-
- {clerkEmail !== legacyEmail?.toLowerCase() && (
-
- ⚠️The new email you signed in with (
- {clerkEmail} )
- seems to differ from your legacy portal email (
- {legacyEmail}
- ). By clicking migrate account, you are
- acknowledging that the new portal account will be
- linked to{" "}
- {clerkEmail} .
- ⚠️
-
- )}
-
- )}
- {stage === "notfound" && (
-
-
- Account Not Found.
-
-
- We could not find an account under the name.
-
- Please try again, or if you believe this is an error,
- contact {c.contactEmail}.
-
-
setStage("lookup")}>
- Try Again
-
-
- )}
- {/* if success is true, we want to allow the user to check over all of the data that they have entered and make any changes */}
- {stage === "success" && (
-
-
- Your Account Has Been Successfully Migrated!
-
-
Click the button below to go to the Dashboard.
-
-
Go to Dashboard
-
-
- )}
-
- );
-}
diff --git a/apps/web/src/components/onboarding/registerForm.tsx b/apps/web/src/components/onboarding/registerForm.tsx
deleted file mode 100644
index 121eb550..00000000
--- a/apps/web/src/components/onboarding/registerForm.tsx
+++ /dev/null
@@ -1,866 +0,0 @@
-"use client";
-import c, { majors, staticUploads } from "config";
-import { Button } from "@/components/ui/button";
-import { insertUserWithDataSchemaFormified } from "db/zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "@/components/ui/command";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Calendar } from "@/components/ui/calendar";
-import { CalendarWithYears } from "@/components/ui/calendarWithYearSelect";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import {
- MultiSelector,
- MultiSelectorContent,
- MultiSelectorInput,
- MultiSelectorItem,
- MultiSelectorList,
- MultiSelectorTrigger,
-} from "@/components/ui/MultiSelect";
-import { FormGroupWrapper } from "@/components/shared/form-group-wrapper";
-import {
- Check,
- ChevronsUpDown,
- CalendarIcon,
- TriangleAlert,
-} from "lucide-react";
-import { cn, range } from "@/lib/utils";
-import { format } from "date-fns";
-import { useEffect, useState } from "react";
-import { useAction } from "next-safe-action/hooks";
-import { createRegistration } from "@/actions/register/new";
-import { toast } from "sonner";
-import { useRouter } from "next/navigation";
-import { put } from "@/lib/client/file-upload";
-import FormDisplayName from "../shared/FormDisplayName";
-import { bucketBaseUrl } from "config";
-import { ClassificationType, MajorType } from "@/lib/types/shared";
-
-const formSchema = insertUserWithDataSchemaFormified;
-
-interface RegisterFormProps {
- defaultEmail: string;
-}
-
-export default function RegisterForm({ defaultEmail }: RegisterFormProps) {
- const [error, setError] = useState<{
- title: string;
- description: string;
- } | null>(null);
- const [resume, setResume] = useState(null);
- const router = useRouter();
-
- const form = useForm>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- email: defaultEmail,
- firstName: "",
- lastName: "",
- universityID: "",
- data: {
- major: "" as MajorType,
- classification: "" as ClassificationType,
- gender: [],
- ethnicity: [],
- },
- },
- });
-
- const {
- execute: runCreateRegistration,
- status: actionStatus,
- result: actionResult,
- reset: resetAction,
- } = useAction(createRegistration, {
- onSuccess: async ({ data }) => {
- toast.dismiss();
-
- if (!data?.success) {
- const code = data?.code || "unknown";
- switch (code) {
- case "user_already_exists":
- setError({
- title: "User Already Exists",
- description: `It would seem you have already registered. If you believe this is an error, please contact ${c.contactEmail}.`,
- });
- break;
- case "email_already_exists":
- setError({
- title: "Email Already Exists",
- description: `${form.getValues().email} There is already an account with that email address. This could mean you have already registered, or that you have a legacy portal account that needs to be connected. If you belive this is an error, please contact ${c.contactEmail}.`,
- });
- break;
- case "university_id_already_exists":
- setError({
- title: `${c.universityID.name} Already Exists`,
- description: `There is already an account with the ${c.universityID.name} of ${form.getValues().universityID.toLowerCase()}. This could mean you have already registered, or that you have a legacy portal account that needs to be connected. If you belive this is an error, please contact ${c.contactEmail}.`,
- });
- break;
- default:
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- break;
- }
- resetAction();
- return;
- }
- toast.success("Registration successful!", {
- description: "You'll be redirected shortly.",
- });
- setTimeout(() => {
- router.push("/dash");
- }, 1500);
- },
- onError: async (error) => {
- toast.dismiss();
- toast.error(
- `An unknown error occurred. Please try again or contact ${c.contactEmail}.`,
- );
- console.log("error: ", error);
- resetAction();
- },
- });
-
- useEffect(() => {
- if (Object.keys(form.formState.errors).length > 0) {
- console.log("Errors: ", form.formState.errors);
- }
- }, [form.formState]);
-
- async function onSubmit(values: z.infer) {
- toast.loading("Creating Registration...");
- if (resume) {
- const resumeBlob = await put(
- staticUploads.bucketResumeBaseUploadUrl,
- resume,
- {
- presignHandlerUrl: "/api/upload/resume",
- },
- );
- console.log("resumeBlob: ", resumeBlob);
- values.data.resume = resumeBlob;
- }
- runCreateRegistration(values);
- }
-
- function validateAndSetResume(event: React.ChangeEvent) {
- const file = event.target.files?.[0];
- if (!file) {
- setResume(null);
- return false;
- }
- if (file.size > c.maxResumeSizeInBytes) {
- form.setError("data.resume", {
- message: "Resume file is too large.",
- });
- setResume(null);
- return false;
- }
- if (!c.acceptedResumeMimeTypes.includes(file.type)) {
- form.setError("data.resume", {
- message: "Resume file must be a .pdf or .docx file.",
- });
- setResume(null);
- return false;
- }
- setResume(file);
- return true;
- }
-
- return (
- <>
-
-
-
- {error?.title}
-
- {error?.description}
-
-
-
- setError(null)}>
- Ok
-
-
-
-
-
-
-
-
-
-
-
-
-
(
-
-
- {`${c.universityID.name} *`}
-
-
-
-
-
-
- )}
- />
- (
-
- Major *
-
-
-
-
- {field.value
- ? majors.find(
- (
- major,
- ) =>
- major ===
- field.value,
- )
- : "Select a Major"}
-
-
-
-
-
-
-
-
- No major found.
-
-
-
- {majors.map(
- (major) => {
- return (
- {
- form.setValue(
- "data.major",
- value as MajorType,
- );
- form.clearErrors(
- "data.major",
- );
- }}
- className="cursor-pointer "
- >
-
- {
- major
- }
-
- );
- },
- )}
-
-
-
-
-
-
-
- )}
- />
- (
-
-
- Classification *
-
-
-
-
-
-
-
-
- {c.userIdentityOptions.classification.map(
- (
- classification,
- index,
- ) => (
-
- {classification}
-
- ),
- )}
-
-
-
-
- )}
- />
- (
-
-
- Graduation Month *
-
-
-
-
-
-
-
-
-
- January
-
-
- February
-
-
- March
-
-
- April
-
-
- May
-
-
- June
-
-
- July
-
-
- August
-
-
- September
-
-
- October
-
-
- November
-
-
- December
-
-
-
-
-
- )}
- />
- (
-
-
- Graduation Year *
-
-
-
-
-
-
-
-
- {range(
- new Date().getFullYear(),
- new Date().getFullYear() +
- 5,
- ).map((year) => (
-
- {year}
-
- ))}
-
-
-
-
- )}
- />
-
-
-
-
-
(
-
- Birthday
-
-
-
-
- {field.value ? (
- format(
- field.value,
- "PPP",
- )
- ) : (
-
- Pick a Date
-
- )}
-
-
-
-
-
-
- date > new Date() ||
- date <
- new Date(
- "1900-01-01",
- )
- }
- fromYear={
- new Date().getFullYear() -
- 100
- }
- toYear={new Date().getFullYear()}
- initialFocus
- />
-
-
-
-
- )}
- />
- {
- return (
-
- Gender *
-
-
-
-
-
-
- {c.userIdentityOptions.gender.map(
- (
- value,
- index,
- ) => (
-
- {value}
-
- ),
- )}
-
-
-
-
-
- );
- }}
- />
-
- {
- return (
-
-
- Ethnicity *
-
-
-
-
-
-
-
- {c.userIdentityOptions.ethnicity.map(
- (
- value,
- index,
- ) => (
-
- {value}
-
- ),
- )}
-
-
-
-
-
- );
- }}
- />
-
-
-
-
- (
-
- Shirt Size *
-
-
-
-
-
-
-
- {c.userIdentityOptions.shirtSize.map(
- (size) => (
-
- {size}
-
- ),
- )}
-
-
-
-
- )}
- />
- (
-
- Shirt Type *
-
-
-
-
-
-
-
- {c.userIdentityOptions.shirtType.map(
- (type) => (
-
- {type}
-
- ),
- )}
-
-
-
-
- )}
- />
- (
-
- Resume
-
- {
- const success =
- validateAndSetResume(
- event,
- );
- if (!success) {
- event.target.value =
- "";
- }
- }}
- />
-
-
-
- )}
- />
-
-
-
-
- Submit
-
-
-
-
- >
- );
-}
diff --git a/apps/web/src/components/settings/forms/account-settings-form.tsx b/apps/web/src/components/settings/forms/account-settings-form.tsx
deleted file mode 100644
index a0e32c2c..00000000
--- a/apps/web/src/components/settings/forms/account-settings-form.tsx
+++ /dev/null
@@ -1,290 +0,0 @@
-"use client";
-
-import { Input } from "@/components/ui/input";
-import { editAccountSettings } from "@/actions/settings/edit";
-import { editAccountSettingsSchema } from "@/validators/settings";
-import { Button } from "@/components/ui/button";
-import { useAction } from "next-safe-action/hooks";
-import { toast } from "sonner";
-import { LoaderCircle } from "lucide-react";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import {
- MultiSelector,
- MultiSelectorContent,
- MultiSelectorInput,
- MultiSelectorItem,
- MultiSelectorList,
- MultiSelectorTrigger,
-} from "@/components/ui/MultiSelect";
-import { PopoverCalendar } from "@/components/shared/popover-calendar";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import c from "config";
-import { useState, useCallback } from "react";
-import { GenderType, EthnicityType } from "@/lib/types/shared";
-import { useRouter } from "next/navigation";
-
-interface AccountInfoProps {
- firstName: string;
- lastName: string;
- gender: GenderType[];
- ethnicity: EthnicityType[];
- birthday: Date | undefined;
-}
-
-export function AccountSettingsForm({
- firstName,
- lastName,
- gender,
- ethnicity,
- birthday,
-}: AccountInfoProps) {
- const [submitting, setSubmitting] = useState(false);
-
- const form = useForm>({
- resolver: zodResolver(editAccountSettingsSchema),
- defaultValues: {
- firstName,
- lastName,
- gender,
- ethnicity,
- birthday,
- },
- });
- const { refresh } = useRouter();
- const { execute } = useAction(editAccountSettings, {
- onExecute: () => setSubmitting(true),
- onSettled: () => setSubmitting(false),
- onSuccess: ({ input }) => {
- toast.success("Account settings updated successfully");
- form.reset(input);
- refresh();
- },
- onError: (error) => {
- toast.error("Failed to update name");
- console.error(error);
- },
- });
-
- const handleSubmit = useCallback(
- (data: z.infer) => {
- if (!form.formState.isDirty) {
- toast.error("No changes detected!", {
- description:
- "Try making some changes to your account settings before submitting",
- classNames: { title: "font-bold text-md" },
- });
- return;
- }
-
- execute(data);
- },
- [form.formState.isDirty, execute],
- );
-
- return (
-
-
-
-
-
- (
-
-
- First Name
-
-
-
-
-
- )}
- />
- (
-
-
- Last Name
-
-
-
-
-
- )}
- />
-
-
- {
- return (
-
-
- Gender
-
-
-
-
-
-
-
-
- Male
-
-
- Female
-
-
- Non-Binary
-
-
- Transgender
-
-
- Intersex
-
-
- Other
-
-
- I prefer not to say
-
-
-
-
-
-
- );
- }}
- />
-
-
- {
- return (
-
-
- Ethnicity
-
-
-
-
-
-
-
- {c.userIdentityOptions.ethnicity.map(
- (value) => (
-
- {value}
-
- ),
- )}
-
-
-
-
-
- );
- }}
- />
-
-
(
-
-
- Birthday
-
-
-
-
-
-
- )}
- />
-
- {submitting ? (
-
- ) : (
- "Update"
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/settings/forms/change-profile-picture-form.tsx b/apps/web/src/components/settings/forms/change-profile-picture-form.tsx
deleted file mode 100644
index e328d56f..00000000
--- a/apps/web/src/components/settings/forms/change-profile-picture-form.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-"use client";
-
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { LoaderCircle } from "lucide-react";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useState, useCallback } from "react";
-import { useUser } from "@clerk/nextjs";
-import { editProfilePictureSchema } from "@/validators/settings";
-import { redirect, useRouter } from "next/navigation";
-import { toast } from "sonner";
-import { Skeleton } from "@/components/ui/skeleton";
-import { FileInput } from "@/components/shared/file-input";
-import { Button } from "@/components/ui/button";
-
-interface ChangeProfilePictureFormProps {
- profilePicture?: string;
-}
-
-export function ChangeProfilePictureForm({
- profilePicture,
-}: ChangeProfilePictureFormProps) {
- const [submitting, setSubmitting] = useState(false);
- const router = useRouter();
- const { user, isLoaded } = useUser();
-
- if (!user && isLoaded) return redirect("/sign-up");
-
- const form = useForm>({
- resolver: zodResolver(editProfilePictureSchema),
- defaultValues: {
- profilePicture: undefined,
- },
- });
-
- const onSubmit = useCallback(
- async (data: z.infer) => {
- setSubmitting(true);
-
- if (!form.formState.isDirty) {
- setSubmitting(false);
- toast.error("No changes detected!", {
- description:
- "Try making some changes to your profile picture before submitting",
- classNames: { title: "font-bold text-md" },
- });
- return;
- }
-
- if (data.profilePicture) {
- if (data.profilePicture.size > 10 * 1024 * 1024) {
- toast.error(
- "Profile picture must be less than 10MB, please upload a smaller image",
- );
- setSubmitting(false);
- return;
- }
- try {
- const setProfileImageResult = await user?.setProfileImage({
- file: data.profilePicture,
- });
- if (!setProfileImageResult) {
- toast.error("Failed to upload profile picture");
- } else {
- toast.success("Profile picture updated successfully");
- }
- } catch (e) {
- toast.error("Failed to upload profile picture");
- console.error(e);
- }
-
- setSubmitting(false);
- form.reset({ profilePicture: undefined });
- router.refresh();
- }
- },
- [form.formState.isDirty],
- );
-
- return (
-
-
- (
-
-
- Profile Picture
-
-
- profilePicture &&
- router.push(profilePicture)
- }
- >
-
-
-
-
-
-
-
- form.setValue("profilePicture", file, {
- shouldDirty: true,
- })
- }
- />
-
-
-
- )}
- />
-
- {submitting ? (
-
- ) : (
- "Update"
- )}
-
-
-
- );
-}
diff --git a/apps/web/src/components/settings/forms/change-resume-form.tsx b/apps/web/src/components/settings/forms/change-resume-form.tsx
deleted file mode 100644
index 3b6b7492..00000000
--- a/apps/web/src/components/settings/forms/change-resume-form.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-"use client";
-
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { LoaderCircle } from "lucide-react";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useState, useCallback } from "react";
-import { editResumeFormSchema } from "@/validators/settings";
-import { editResumeUrl } from "@/actions/settings/edit";
-import { useAction } from "next-safe-action/hooks";
-import { toast } from "sonner";
-import { FileInput } from "@/components/shared/file-input";
-import { Button } from "@/components/ui/button";
-import c, { bucketBaseUrl } from "config";
-import { formatBlobUrl } from "@/lib/utils";
-import { staticUploads } from "config";
-import { put } from "@/lib/client/file-upload";
-import { useRouter } from "next/navigation";
-
-interface ChangeResumeFormProps {
- resume?: string;
-}
-
-export function ChangeResumeForm({ resume }: ChangeResumeFormProps) {
- const [submitting, setSubmitting] = useState(false);
-
- const form = useForm>({
- resolver: zodResolver(editResumeFormSchema),
- defaultValues: {
- resume: undefined,
- },
- });
- const { refresh } = useRouter();
- const { execute } = useAction(editResumeUrl, {
- onSettled: () => setSubmitting(false),
- onSuccess: () => {
- toast.success("Account settings updated successfully");
- form.reset({ resume: undefined });
- refresh();
- },
- onError: (error) => {
- toast.error("Failed to update name");
- console.error(error);
- },
- });
-
- const onSubmit = useCallback(
- async (data: z.infer) => {
- setSubmitting(true);
- if (!form.formState.isDirty) {
- setSubmitting(false);
- toast.error("No changes detected!", {
- description:
- "Try making some changes to your resume before submitting",
- classNames: { title: "font-bold text-md" },
- });
- return;
- }
-
- if (data.resume) {
- if (data.resume.size > c.maxResumeSizeInBytes) {
- toast.error("Resume size is too large");
- return;
- }
-
- try {
- const uploadedFileUrl = await put(
- staticUploads.bucketResumeBaseUploadUrl,
- data.resume,
- {
- presignHandlerUrl: "/api/upload/resume",
- },
- );
-
- execute({ resume: uploadedFileUrl, oldResume: resume });
- } catch (e) {
- toast.error("Failed to upload resume");
- console.error(e);
- }
- } else if (data.resume === null) {
- execute({ resume: "", oldResume: resume });
- } else {
- setSubmitting(false);
- }
- },
- [form.formState.isDirty, execute],
- );
-
- return (
-
-
- (
-
- Resume
-
- {
- form.setValue("resume", null, {
- shouldDirty: true,
- });
- }}
- showCurrent
- currentFileName={
- resume
- ? formatBlobUrl(resume)
- : undefined
- }
- currentLink={resume}
- fileValue={form.getValues("resume")}
- accept={c.acceptedResumeMimeTypes.toLocaleString()}
- onChange={(file) =>
- form.setValue("resume", file, {
- shouldDirty:
- file?.name !==
- (resume &&
- formatBlobUrl(resume)),
- })
- }
- />
-
-
-
- )}
- />
-
- {submitting ? (
-
- ) : (
- "Update"
- )}
-
-
-
- );
-}
diff --git a/apps/web/src/components/settings/forms/club-settings-form.tsx b/apps/web/src/components/settings/forms/club-settings-form.tsx
deleted file mode 100644
index 1f8c48f2..00000000
--- a/apps/web/src/components/settings/forms/club-settings-form.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-"use client";
-
-import { editClubSettingsSchema } from "@/validators/settings";
-import { editClubSettings } from "@/actions/settings/edit";
-import { Button } from "@/components/ui/button";
-import { useAction } from "next-safe-action/hooks";
-import { toast } from "sonner";
-import { LoaderCircle } from "lucide-react";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useState, useCallback } from "react";
-import { PopoverSelect } from "@/components/shared/popover-select";
-import { ShirtSizeType, ShirtType } from "@/lib/types/shared";
-import c from "config";
-import { useRouter } from "next/navigation";
-interface OrganizationSettingsPageProps {
- shirtType: ShirtType;
- shirtSize: ShirtSizeType;
-}
-
-// TODO: Figure out what types of interested events users can select. Fix padding on interested event types.
-export function ClubSettingsForm({
- shirtSize,
- shirtType,
-}: OrganizationSettingsPageProps) {
- const [submitting, setSubmitting] = useState(false);
- const { refresh } = useRouter();
- const form = useForm>({
- resolver: zodResolver(editClubSettingsSchema),
- defaultValues: {
- shirtSize,
- shirtType,
- },
- });
-
- const { execute } = useAction(editClubSettings, {
- onSettled: () => setSubmitting(false),
- onExecute: () => setSubmitting(true),
- onSuccess: () => {
- toast.success("Organization settings updated");
- form.reset(form.getValues());
- refresh();
- },
- onError: (error) => {
- toast.error("Failed to update organization settings");
- console.error(error);
- },
- });
-
- const handleSubmit = useCallback(
- (data: z.infer) => {
- if (!form.formState.isDirty) {
- toast.error("No changes detected!", {
- description:
- "Try making some changes to your club settings before submitting",
- classNames: { title: "font-bold text-md" },
- });
- return;
- }
-
- execute(data);
- },
- [form.formState.isDirty, execute],
- );
-
- return (
-
-
-
-
-
(
-
-
- Shirt Size
-
-
-
-
-
-
- )}
- />
- (
-
-
- Shirt Type
-
-
-
-
-
-
- )}
- />
-
- {submitting ? (
-
- ) : (
- "Update"
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/settings/forms/school-settings-form.tsx b/apps/web/src/components/settings/forms/school-settings-form.tsx
deleted file mode 100644
index 47ff3be3..00000000
--- a/apps/web/src/components/settings/forms/school-settings-form.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-"use client";
-
-import { editAcademicSettings } from "@/actions/settings/edit";
-import { editAcademicSettingsSchema } from "@/validators/settings";
-import { Button } from "@/components/ui/button";
-import { useAction } from "next-safe-action/hooks";
-import { toast } from "sonner";
-import { range } from "@/lib/utils";
-import { LoaderCircle } from "lucide-react";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useState, useCallback } from "react";
-import { majors } from "config";
-import { PopoverCommand } from "@/components/shared/popover-command";
-import { PopoverSelect } from "@/components/shared/popover-select";
-import { MajorType, ClassificationType } from "@/lib/types/shared";
-import c from "config";
-import { useRouter } from "next/navigation";
-
-interface SchoolSettingsFormProps {
- major: MajorType;
- classification: ClassificationType;
- graduationMonth: number;
- graduationYear: number;
-}
-
-const months = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
-];
-
-export function SchoolSettingsForm({
- graduationMonth,
- graduationYear,
- classification,
- major,
-}: SchoolSettingsFormProps) {
- const [submitting, setSubmitting] = useState(false);
- const { refresh } = useRouter();
- const form = useForm>({
- resolver: zodResolver(editAcademicSettingsSchema),
- defaultValues: {
- classification,
- graduationMonth,
- graduationYear,
- major,
- },
- });
-
- const { execute } = useAction(editAcademicSettings, {
- onExecute: () => setSubmitting(true),
- onSettled: () => setSubmitting(false),
- onSuccess: () => {
- toast.success("Academic information updated successfully");
- form.reset(form.getValues());
- refresh();
- },
- onError: (error) => {
- toast.error("Failed to update academic information");
- console.error(error);
- },
- });
-
- const handleSubmit = useCallback(
- (data: z.infer) => {
- if (!form.formState.isDirty) {
- toast.error("No changes detected!", {
- description:
- "Try making some changes to your school settings before submitting",
- classNames: { title: "font-bold text-md" },
- });
- return;
- }
-
- execute(data);
- },
- [form.formState.isDirty, execute],
- );
-
- return (
-
-
-
-
-
(
-
-
- Major
-
-
-
-
-
-
- )}
- />
- (
-
-
- Classification
-
-
-
-
-
-
- )}
- />
-
-
(
-
-
- Graduation Month
-
-
-
- field.onChange(
- parseInt(value),
- )
- }
- options={range(1, 13).map(
- String,
- )}
- optionNames={months}
- topic="month"
- value={field.value.toString()}
- />
-
-
-
- )}
- />
-
-
-
(
-
-
- Graduation Year
-
-
-
- field.onChange(
- parseInt(value),
- )
- }
- options={range(
- new Date().getFullYear(),
- new Date().getFullYear() +
- 5,
- ).map((year) =>
- year.toString(),
- )}
- topic="year"
- value={field.value.toString()}
- />
-
-
-
- )}
- />
-
-
- {submitting ? (
-
- ) : (
- "Update"
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/settings/settings-dropdown.tsx b/apps/web/src/components/settings/settings-dropdown.tsx
deleted file mode 100644
index 622ac0f1..00000000
--- a/apps/web/src/components/settings/settings-dropdown.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from "@/components/ui/accordion";
-import Link from "next/link";
-import { cn } from "@/lib/utils";
-import { Settings } from "lucide-react";
-
-interface SettingsDropdownProps {
- items: { href: string; title: string }[];
- className?: string;
-}
-
-export function SettingsDropdown({ items, className }: SettingsDropdownProps) {
- return (
-
-
-
-
-
-
- {items.map(({ title, href }) => (
-
-
- {title}
-
-
- ))}
-
-
-
- );
-}
diff --git a/apps/web/src/components/settings/settings-nav.tsx b/apps/web/src/components/settings/settings-nav.tsx
deleted file mode 100644
index fef49461..00000000
--- a/apps/web/src/components/settings/settings-nav.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { HTMLAttributes } from "react";
-import { cn } from "@/lib/utils";
-import Link from "next/link";
-import { buttonVariants } from "../ui/button";
-
-interface SettingsNavProps extends HTMLAttributes {
- className?: string;
- items: {
- href: string;
- title: string;
- }[];
-}
-
-export function SettingsNav({ items, className, ...props }: SettingsNavProps) {
- return (
-
- {items.map(({ href, title }) => (
-
- {title}
-
- ))}
-
- );
-}
diff --git a/apps/web/src/components/shared/footer.tsx b/apps/web/src/components/shared/footer.tsx
new file mode 100644
index 00000000..5fceda1a
--- /dev/null
+++ b/apps/web/src/components/shared/footer.tsx
@@ -0,0 +1,127 @@
+import Image from "next/image";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+
+export default function Footer() {
+ return (
+
+
+
+
+ The Association for Computing
+
+ Machinery
+
+ at UTSA
+
+
+
+
+
+
+
+ Resources &
+ Important Links
+
+
+
+
+
+
+
+ Suborgs &
+ Hackathons
+
+
+
+
+
+
+
+
+
+
+
+ Social
+
+ Media
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function FooterLink({
+ text,
+ href,
+ target,
+}: {
+ text: string;
+ href: string;
+ target?: string;
+}) {
+ return (
+
+
+ {text}
+
+
+ );
+}
diff --git a/apps/web/src/components/shared/navbar.tsx b/apps/web/src/components/shared/navbar.tsx
index 29a3ab7d..f796bcc5 100644
--- a/apps/web/src/components/shared/navbar.tsx
+++ b/apps/web/src/components/shared/navbar.tsx
@@ -1,22 +1,10 @@
import Image from "next/image";
import Link from "next/link";
-import { auth, currentUser } from "@clerk/nextjs/server";
-import { db } from "db";
-import { users } from "db/schema";
-import { eq } from "db/drizzle";
-import ProfileButton from "@/components/shared/profile-button";
-import { Button } from "@/components/ui/button";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetHeader,
- SheetTitle,
- SheetTrigger,
-} from "@/components/ui/sheet";
+import { Button, variantItems } from "@/components/ui/button";
import c from "config";
import { Menu } from "lucide-react";
+import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
type NavbarProps = {
siteRegion?: string;
@@ -24,17 +12,6 @@ type NavbarProps = {
};
export default async function Navbar({ siteRegion, showBorder }: NavbarProps) {
- const clerkAuth = await auth();
- const clerkUser = await currentUser();
- const { userId } = clerkAuth;
- const user = userId
- ? await db.query.users.findFirst({
- where: eq(users.clerkID, userId),
- with: { data: true },
- })
- : null;
-
- const registrationComplete = user != null;
return (
- {user ? (
- <>
-
-
- {registrationComplete
- ? "Dashboard"
- : "Complete Registration"}
-
-
-
-
Events
-
- {(user.role === "admin" ||
- user.role === "super_admin") && (
-
-
- Admin
-
-
- )}
-
- >
- ) : (
- <>
-
-
- Sign In
-
-
-
-
Register
-
- >
- )}
+
{/* Small screen navbar */}
@@ -123,69 +51,135 @@ export default async function Navbar({ siteRegion, showBorder }: NavbarProps) {
- {user ? (
- <>
- {registrationComplete && (
-
-
- Settings
-
-
- )}
-
-
- {registrationComplete
- ? "Dashboard"
- : "Complete Registration"}
-
-
-
- Events
-
- {(user.role === "admin" ||
- user.role === "super_admin") && (
-
- Admin
-
- )}
-
- >
- ) : (
-
-
-
- Sign In
-
-
-
- Register
-
-
- )}
+
);
}
+
+interface HeroVariant {
+ wrapper: string;
+ buttonVariant: keyof typeof variantItems;
+ link: string;
+ dashButton: string;
+}
+
+const variant = {
+ default: {
+ wrapper: "",
+ buttonVariant: "styleized-white-blue-text",
+ link: "text-white",
+ dashButton: "text-white",
+ },
+ blueForeground: {
+ wrapper: "bg-white",
+ buttonVariant: "styleized-blue-darker",
+ link: "text-acm-darker-blue",
+ dashButton: "hover:text-acm-darker-blue",
+ },
+} as const;
+
+export function HeroNav({
+ navVariant = "default",
+ customColor,
+}: {
+ navVariant?: keyof typeof variant;
+ customColor?: string;
+}) {
+ const linkStyles = customColor || variant[navVariant].link;
+ const dashButtonStyles = customColor || variant[navVariant].dashButton;
+
+ return (
+
+
+
+
+
+
+ Events
+
+
+ Team
+
+
+ Sub-orgs
+
+
+ Sponsor
+
+
+ Donate
+
+
+ Contact
+
+
+ Resources
+
+
+
+
+ );
+}
+
+async function PortalButton({
+ navVariant,
+ customColor,
+}: {
+ navVariant: keyof typeof variant;
+ customColor?: string;
+}) {
+ return (
+
+
+ Membership Portal
+
+
+ );
+}
+
+function NavLink({
+ href,
+ children,
+ linkStyles,
+}: {
+ href: string;
+ children: React.ReactNode;
+ linkStyles: string;
+}) {
+ // Check if linkStyles is a custom color (starts with rgb, hex, etc.)
+ const isCustomColor =
+ linkStyles.startsWith("rgb") ||
+ linkStyles.startsWith("#") ||
+ linkStyles.startsWith("hsl");
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/web/src/components/team/meet-the-team.client.tsx b/apps/web/src/components/team/meet-the-team.client.tsx
new file mode 100644
index 00000000..cbb111f9
--- /dev/null
+++ b/apps/web/src/components/team/meet-the-team.client.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import OrbitCarousel, { type OrbitPerson } from "@/components/team/orbit-carousel";
+import { TEAM_GROUPS } from "@/components/team/team.data";
+
+const TABS = [
+ "faculty sponsors",
+ "acm",
+ "acm-w",
+ "rowdy creators",
+ "coding in color",
+ "rowdyhacks",
+ "code quantum",
+ "rowdy cybercon",
+] as const;
+
+type Tab = (typeof TABS)[number];
+
+export default function MeetTheTeamClient() {
+ const [activeTab, setActiveTab] = useState("acm");
+
+ const people: OrbitPerson[] = useMemo(() => {
+ const group = TEAM_GROUPS.find((g) => g.label === activeTab);
+ if (!group) return [];
+
+ return group.members.map((m) => ({
+ id: m.id,
+ name: m.name,
+ role: m.role,
+ org: group.label,
+ imageUrl: m.imageUrl,
+
+ // ✅ THIS is the missing piece
+ socials: m.socials,
+ }));
+ }, [activeTab]);
+
+ return (
+
+
+ {TABS.map((tab, idx) => (
+ setActiveTab(tab)}
+ className={`transition ${
+ activeTab === tab
+ ? "text-acm-blue"
+ : "text-acm-darker-blue/35 hover:text-acm-darker-blue/60"
+ }`}
+ >
+ {tab}
+ {idx < TABS.length - 1 ? (
+ |
+ ) : null}
+
+ ))}
+
+
+
+
+
+ {people.length ? (
+
+ ) : (
+
+
+ No members yet
+
+
+ Add people to the{" "}
+ {activeTab} group
+ in{" "}
+ team.data.ts .
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/components/team/orbit-carousel.tsx b/apps/web/src/components/team/orbit-carousel.tsx
new file mode 100644
index 00000000..013b0892
--- /dev/null
+++ b/apps/web/src/components/team/orbit-carousel.tsx
@@ -0,0 +1,497 @@
+"use client";
+
+import Image from "next/image";
+import React, { useEffect, useMemo, useRef, useState } from "react";
+
+export type OrbitPerson = {
+ id: string;
+ name: string;
+ role?: string;
+ org?: string;
+ imageUrl: string;
+ socials?: {
+ linkedin?: string;
+ github?: string;
+ instagram?: string;
+ website?: string;
+ };
+};
+
+export type OrbitCarouselProps = {
+ people?: OrbitPerson[];
+ initialIndex?: number;
+};
+
+function clampIndex(i: number, n: number) {
+ if (n <= 0) return 0;
+ return ((i % n) + n) % n;
+}
+
+function lerp(a: number, b: number, t: number) {
+ return a + (b - a) * t;
+}
+
+function degToRad(deg: number) {
+ return (deg * Math.PI) / 180;
+}
+
+type Pt = { x: number; y: number };
+
+function ellipsePoint(cx: number, cy: number, rx: number, ry: number, theta: number): Pt {
+ return {
+ x: cx + rx * Math.cos(theta),
+ y: cy + ry * Math.sin(theta),
+ };
+}
+
+function arcPoint(arc: { cx: number; cy: number; rx: number; ry: number }, t: number) {
+ const thetaStart = degToRad(200);
+ const thetaEnd = degToRad(340);
+ const theta = lerp(thetaStart, thetaEnd, t);
+ return ellipsePoint(arc.cx, arc.cy, arc.rx, arc.ry, theta);
+}
+
+function arcPathD(arc: { cx: number; cy: number; rx: number; ry: number }) {
+ const a = arcPoint(arc, 0);
+ const b = arcPoint(arc, 1);
+ return `M ${a.x.toFixed(2)} ${a.y.toFixed(2)} A ${arc.rx} ${arc.ry} 0 0 1 ${b.x.toFixed(
+ 2
+ )} ${b.y.toFixed(2)}`;
+}
+
+function cubicPath(a: Pt, c1: Pt, c2: Pt, b: Pt) {
+ return `M ${a.x.toFixed(2)} ${a.y.toFixed(2)} C ${c1.x.toFixed(2)} ${c1.y.toFixed(
+ 2
+ )} ${c2.x.toFixed(2)} ${c2.y.toFixed(2)} ${b.x.toFixed(2)} ${b.y.toFixed(2)}`;
+}
+
+function ellipseArcD(
+ cx: number,
+ cy: number,
+ rx: number,
+ ry: number,
+ startDeg: number,
+ endDeg: number,
+ sweep: 0 | 1 = 1
+) {
+ const a = ellipsePoint(cx, cy, rx, ry, degToRad(startDeg));
+ const b = ellipsePoint(cx, cy, rx, ry, degToRad(endDeg));
+ const largeArc: 0 | 1 = Math.abs(endDeg - startDeg) >= 180 ? 1 : 0;
+ return `M ${a.x.toFixed(2)} ${a.y.toFixed(2)} A ${rx} ${ry} 0 ${largeArc} ${sweep} ${b.x.toFixed(
+ 2
+ )} ${b.y.toFixed(2)}`;
+}
+
+type GridSeg = {
+ id: string;
+ d: string;
+ opacity: number;
+ strokeW: number;
+};
+
+export default function OrbitCarousel({ people = [], initialIndex = 0 }: OrbitCarouselProps) {
+ const n = people.length;
+ const [active, setActive] = useState(() => clampIndex(initialIndex, n || 1));
+
+ useEffect(() => {
+ setActive((v) => clampIndex(v, n || 1));
+ }, [n]);
+
+ if (!people || people.length === 0) {
+ return (
+
+
+
No team members yet
+
Pass people into OrbitCarousel.
+
+
+ );
+ }
+
+ const prev = clampIndex(active - 1, n);
+ const next = clampIndex(active + 1, n);
+
+ const activePerson = people[active];
+ const prevPerson = people[prev];
+ const nextPerson = people[next];
+
+ const W = 1200;
+ const H = 700;
+
+ const arc = {
+ cx: 600,
+ cy: 640,
+ rx: 980,
+ ry: 260,
+ };
+
+ const tLeft = 0.24;
+ const tMid = 0.5;
+ const tRight = 0.76;
+
+ const pLeft = arcPoint(arc, tLeft);
+ const pMid = arcPoint(arc, tMid);
+ const pRight = arcPoint(arc, tRight);
+
+ function goPrev() {
+ setActive((v) => clampIndex(v - 1, n));
+ }
+ function goNext() {
+ setActive((v) => clampIndex(v + 1, n));
+ }
+
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === "ArrowLeft") goPrev();
+ if (e.key === "ArrowRight") goNext();
+ };
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [n]);
+
+ const grid = useMemo(() => {
+ const lat: GridSeg[] = [
+ { id: "lat-0", d: ellipseArcD(600, 735, 1200, 310, 205, 335, 1), opacity: 0.10, strokeW: 3.4 },
+ { id: "lat-1", d: ellipseArcD(585, 775, 1020, 300, 210, 330, 1), opacity: 0.08, strokeW: 3.0 },
+ { id: "lat-2", d: ellipseArcD(620, 820, 1320, 360, 215, 325, 1), opacity: 0.07, strokeW: 3.0 },
+ ];
+
+ const loops: GridSeg[] = [
+ { id: "loop-0", d: ellipseArcD(280, 835, 360, 180, 330, 150, 1), opacity: 0.08, strokeW: 3.2 },
+ { id: "loop-1", d: ellipseArcD(920, 830, 420, 190, 30, 210, 1), opacity: 0.08, strokeW: 3.2 },
+ ];
+
+ const merTs = [0.32, 0.5, 0.68];
+ const mer: GridSeg[] = merTs.map((t, i) => {
+ const end = arcPoint(arc, t); // ✅ hits main arc
+ const start: Pt = {
+ x: i === 0 ? 260 : i === 1 ? 600 : 940,
+ y: 698,
+ };
+
+ const dir = i === 0 ? -1 : i === 1 ? 1 : 1;
+ const bend = i === 1 ? 340 : 420;
+
+ const c1: Pt = { x: start.x + dir * bend, y: start.y - 190 };
+ const c2: Pt = { x: end.x - dir * bend * 0.9, y: end.y + 260 };
+
+ return { id: `mer-${i}`, d: cubicPath(start, c1, c2, end), opacity: 0.09, strokeW: 3.6 };
+ });
+
+ const all = [...lat, ...loops, ...mer];
+ return { lat, loops, mer, all };
+ }, []);
+
+ // Dots: one per grid line, animated along its own path
+ const pathRefs = useRef>({});
+ const [gridDots, setGridDots] = useState>({});
+
+ const rafRef = useRef(null);
+ const startRef = useRef(null);
+
+ useEffect(() => {
+ // make this smaller = faster overall
+ const baseDurationMs = 100;
+
+ const tick = (ts: number) => {
+ if (startRef.current == null) startRef.current = ts;
+ const elapsed = ts - startRef.current;
+
+ const nextDots: Record = {};
+
+ for (let i = 0; i < grid.all.length; i++) {
+ const seg = grid.all[i];
+ const el = pathRefs.current[seg.id];
+ if (!el) continue;
+
+ const len = el.getTotalLength();
+
+ const speed = 0.85 + (i % 3) * 0.18; // subtle variety
+ const phase = (i * 0.17) % 1;
+
+ const u = ((elapsed / (baseDurationMs / speed)) + phase * baseDurationMs) / baseDurationMs;
+ const uu = ((u % 1) + 1) % 1;
+
+ const pt = el.getPointAtLength(len * uu);
+ nextDots[seg.id] = { x: pt.x, y: pt.y };
+ }
+
+ setGridDots(nextDots);
+ rafRef.current = requestAnimationFrame(tick);
+ };
+
+ rafRef.current = requestAnimationFrame(tick);
+ return () => {
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
+ rafRef.current = null;
+ startRef.current = null;
+ };
+ }, [grid]);
+
+ const BASE = 170;
+ const sideScale = 120 / 170;
+
+ const bubbleTargets = useMemo(() => {
+ return [
+ {
+ idx: prev,
+ person: prevPerson,
+ pt: pLeft,
+ scale: sideScale,
+ emphasize: false,
+ z: 20,
+ },
+ {
+ idx: active,
+ person: activePerson,
+ pt: pMid,
+ scale: 1,
+ emphasize: true,
+ z: 30,
+ },
+ {
+ idx: next,
+ person: nextPerson,
+ pt: pRight,
+ scale: sideScale,
+ emphasize: false,
+ z: 20,
+ },
+ ];
+ }, [prev, active, next, prevPerson, activePerson, nextPerson, pLeft.x, pLeft.y, pMid.x, pMid.y, pRight.x, pRight.y]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {grid.all.map((ln) => (
+ {
+ pathRefs.current[ln.id] = el;
+ }}
+ d={ln.d}
+ stroke="white"
+ strokeOpacity={ln.opacity}
+ strokeWidth={ln.strokeW}
+ strokeLinecap="round"
+ />
+ ))}
+
+
+ {grid.all.map((ln) => {
+ const pt = gridDots[ln.id];
+ if (!pt) return null;
+ return ;
+ })}
+
+
+
+
+
+
+
+
+ {(activePerson?.org ?? "ACM").toUpperCase()}
+
+
{activePerson.name}
+ {activePerson.role ? (
+
{activePerson.role}
+ ) : null}
+
+
+
+
+
+
+ ‹
+
+
+
+ ›
+
+
+ {/* Bubbles */}
+
+ {bubbleTargets.map((v) => (
+
setActive(v.idx)}
+ />
+ ))}
+
+
+
+
{activePerson.name}
+ {activePerson.role ? (
+
{activePerson.role}
+ ) : null}
+
+
+
+
+
+ );
+}
+
+function PersonBubble({
+ person,
+ x,
+ y,
+ baseSize,
+ scale,
+ emphasize,
+ zIndex,
+ onClick,
+}: {
+ person: OrbitPerson;
+ x: number;
+ y: number;
+ baseSize: number;
+ scale: number;
+ emphasize?: boolean;
+ zIndex?: number;
+ onClick?: () => void;
+}) {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+/* ---------------- Socials (ONLY addition) ---------------- */
+
+function normalizeUrl(url: string) {
+ if (/^https?:\/\//i.test(url)) return url;
+ return `https://${url}`;
+}
+
+function SocialLinks({
+ socials,
+}: {
+ socials?: {
+ linkedin?: string;
+ github?: string;
+ instagram?: string;
+ website?: string;
+ };
+}) {
+ const items = [
+ socials?.linkedin ? { k: "linkedin", href: socials.linkedin, label: "in" } : null,
+ socials?.github ? { k: "github", href: socials.github, label: "gh" } : null,
+ socials?.instagram ? { k: "instagram", href: socials.instagram, label: "ig" } : null,
+ socials?.website ? { k: "website", href: socials.website, label: "web" } : null,
+ ].filter(Boolean) as { k: string; href: string; label: string }[];
+
+ if (items.length === 0) return null;
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/team/team.data.ts b/apps/web/src/components/team/team.data.ts
new file mode 100644
index 00000000..2f3b8a86
--- /dev/null
+++ b/apps/web/src/components/team/team.data.ts
@@ -0,0 +1,59 @@
+import type { TeamGroup } from "./types";
+
+export const TEAM_GROUPS: TeamGroup[] = [
+ {
+ key: "acm_general",
+ label: "acm",
+ members: [
+ {
+ id: "miguel-oseguera",
+ name: "Miguel Oseguera",
+ role: "Technical Officer",
+ imageUrl: "/img/officer_photos/acm_general/miguel-oseguera.jpg",
+ socials: {
+ linkedin: "www.linkedin.com/in/miguel-oseguera-0b5306281",
+ github: "https://github.com/Miguel-Oseguera",
+ instagram: "https://www.instagram.com/milo.o175/",
+ }
+ },
+ {
+ id: "josh-silva",
+ name: "Josh Silva",
+ role: "Projects",
+ imageUrl: "/img/officer_photos/acm_general/josh-silva.jpg",
+ },
+ {
+ id: "juleah-cephus",
+ name: "Juleah Cephus",
+ role: "Treasurer",
+ imageUrl: "/img/officer_photos/acm_general/juleah-cephus.jpg",
+ },
+ {
+ id: "eric-lee",
+ name: "Eric Lee",
+ role: "Vice President",
+ imageUrl: "/img/officer_photos/acm_general/eric-lee.jpg",
+ },
+ {
+ id: "vivian-tran",
+ name: "Vivian Tran",
+ role: "President",
+ imageUrl: "/img/officer_photos/acm_general/vivian-tran.jpg",
+ },
+ ],
+ },
+
+ {
+ key: "acm-w",
+ label: "acm-w",
+ members: [
+ {
+ id: "reese-sylvester",
+ name: "Reese Sylvester",
+ role: "President",
+ imageUrl: "/img/officer_photos/acm_w/reese-sylvester.jpg",
+ },
+ ],
+ },
+];
+
diff --git a/apps/web/src/components/team/types.ts b/apps/web/src/components/team/types.ts
new file mode 100644
index 00000000..a97806b6
--- /dev/null
+++ b/apps/web/src/components/team/types.ts
@@ -0,0 +1,31 @@
+export type TeamKey =
+ | "faculty_sponsor"
+ | "acm_general"
+ | "acm-w"
+ | "rowdy_creators"
+ | "coding_in_color"
+ | "rowdy_hacks"
+ | "code_quantum"
+ | "rowdy_cybercon";
+
+export type TeamMember = {
+ id: string;
+ name: string;
+ role?: string;
+ imageUrl: string;
+
+ socials?: {
+ linkedin?: string;
+ github?: string;
+ instagram?: string;
+ website?: string;
+ };
+
+ links?: { label: string; href: string }[];
+};
+
+export type TeamGroup = {
+ key: TeamKey;
+ label: string;
+ members: TeamMember[];
+};
diff --git a/apps/web/src/components/ui/accordion.tsx b/apps/web/src/components/ui/accordion.tsx
index 86da12f4..42d5b5fb 100644
--- a/apps/web/src/components/ui/accordion.tsx
+++ b/apps/web/src/components/ui/accordion.tsx
@@ -1,58 +1,56 @@
-"use client";
+"use client"
-import * as React from "react";
-import * as AccordionPrimitive from "@radix-ui/react-accordion";
-import { ChevronDown } from "lucide-react";
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { cn } from "@/lib/utils"
+import { ChevronDownIcon } from "@radix-ui/react-icons"
-import { cn } from "@/lib/utils";
-
-const Accordion = AccordionPrimitive.Root;
+const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-));
-AccordionItem.displayName = "AccordionItem";
+
+))
+AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
-
- svg]:rotate-180",
- className,
- )}
- {...props}
- >
- {children}
-
-
-
-));
-AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
-
- {children}
-
-));
-
-AccordionContent.displayName = AccordionPrimitive.Content.displayName;
-
-export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+
+ {children}
+
+))
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/apps/web/src/components/ui/alert-dialog.tsx b/apps/web/src/components/ui/alert-dialog.tsx
index b434d63b..57760f2e 100644
--- a/apps/web/src/components/ui/alert-dialog.tsx
+++ b/apps/web/src/components/ui/alert-dialog.tsx
@@ -1,141 +1,141 @@
-"use client";
+"use client"
-import * as React from "react";
-import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
-import { cn } from "@/lib/utils";
-import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
-const AlertDialog = AlertDialogPrimitive.Root;
+const AlertDialog = AlertDialogPrimitive.Root
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
-const AlertDialogPortal = AlertDialogPrimitive.Portal;
+const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-));
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-
-
-
-));
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes) => (
-
-);
-AlertDialogHeader.displayName = "AlertDialogHeader";
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
- className,
- ...props
+ className,
+ ...props
}: React.HTMLAttributes) => (
-
-);
-AlertDialogFooter.displayName = "AlertDialogFooter";
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-));
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-));
+
+))
AlertDialogDescription.displayName =
- AlertDialogPrimitive.Description.displayName;
+ AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-));
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-));
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
- AlertDialog,
- AlertDialogPortal,
- AlertDialogOverlay,
- AlertDialogTrigger,
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogFooter,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogAction,
- AlertDialogCancel,
-};
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx
index d9f1b10c..51e507ba 100644
--- a/apps/web/src/components/ui/avatar.tsx
+++ b/apps/web/src/components/ui/avatar.tsx
@@ -1,50 +1,50 @@
-"use client";
+"use client"
-import * as React from "react";
-import * as AvatarPrimitive from "@radix-ui/react-avatar";
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-));
-Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-));
-AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
-));
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
-export { Avatar, AvatarImage, AvatarFallback };
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx
index e83a2b81..e87d62bf 100644
--- a/apps/web/src/components/ui/badge.tsx
+++ b/apps/web/src/components/ui/badge.tsx
@@ -1,36 +1,36 @@
-import * as React from "react";
-import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/utils"
const badgeVariants = cva(
- "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
- destructive:
- "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
- outline: "text-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- },
-);
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
export interface BadgeProps
- extends React.HTMLAttributes,
- VariantProps {}
+ extends React.HTMLAttributes,
+ VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
- return (
-
- );
+ return (
+
+ )
}
-export { Badge, badgeVariants };
+export { Badge, badgeVariants }
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
index ca8e747b..2fb04a2b 100644
--- a/apps/web/src/components/ui/button.tsx
+++ b/apps/web/src/components/ui/button.tsx
@@ -4,27 +4,32 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
+const variantItems = {
+ default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ "styleized-blue-darker": "text-white bg-acm-darker-blue",
+ "styleized-blue-light": "text-white bg-acm-light-blue",
+ "styleized-white-blue-text": "text-acm-darker-blue bg-white hover:white/90",
+};
+
const buttonVariants = cva(
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
- variant: {
- default:
- "bg-primary text-primary-foreground hover:bg-primary/90",
- destructive:
- "bg-destructive text-destructive-foreground hover:bg-destructive/90",
- outline:
- "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
- ghost: "hover:bg-accent hover:text-accent-foreground",
- link: "text-primary underline-offset-4 hover:underline",
- },
+ variant: variantItems,
size: {
- default: "h-10 px-4 py-2",
- sm: "h-9 rounded-md px-3",
- lg: "h-11 rounded-md px-8",
- icon: "h-10 w-10",
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ xl: "h-12 rounded-md px-10 font-bold text-lg",
+ icon: "h-9 w-9",
},
},
defaultVariants: {
@@ -54,4 +59,4 @@ const Button = React.forwardRef(
);
Button.displayName = "Button";
-export { Button, buttonVariants };
+export { Button, buttonVariants, variantItems };
diff --git a/apps/web/src/components/ui/calendar.tsx b/apps/web/src/components/ui/calendar.tsx
index da36795d..a77b2df7 100644
--- a/apps/web/src/components/ui/calendar.tsx
+++ b/apps/web/src/components/ui/calendar.tsx
@@ -1,68 +1,76 @@
-"use client";
+"use client"
-import * as React from "react";
-import { ChevronLeft, ChevronRight } from "lucide-react";
-import { DayPicker } from "react-day-picker";
+import * as React from "react"
+import { DayPicker } from "react-day-picker"
-import { cn } from "@/lib/utils";
-import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
-export type CalendarProps = React.ComponentProps;
+export type CalendarProps = React.ComponentProps
function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- ...props
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
}: CalendarProps) {
- return (
- ,
- IconRight: ({ ...props }) => (
-
- ),
- }}
- {...props}
- />
- );
+ return (
+ .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
+ : "[&:has([aria-selected])]:rounded-md"
+ ),
+ day: cn(
+ buttonVariants({ variant: "ghost" }),
+ "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
+ ),
+ day_range_start: "day-range-start",
+ day_range_end: "day-range-end",
+ day_selected:
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
+ day_today: "bg-accent text-accent-foreground",
+ day_outside:
+ "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
+ day_disabled: "text-muted-foreground opacity-50",
+ day_range_middle:
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
+ day_hidden: "invisible",
+ ...classNames,
+ }}
+ components={{
+ IconLeft: ({ className, ...props }) => (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ )
}
-Calendar.displayName = "Calendar";
+Calendar.displayName = "Calendar"
-export { Calendar };
+export { Calendar }
diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx
index 9ebb4023..cabfbfc5 100644
--- a/apps/web/src/components/ui/card.tsx
+++ b/apps/web/src/components/ui/card.tsx
@@ -1,86 +1,76 @@
-import * as React from "react";
+import * as React from "react"
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/utils"
const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
+ HTMLDivElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
-));
-Card.displayName = "Card";
+
+))
+Card.displayName = "Card"
const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
+ HTMLDivElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
-));
-CardHeader.displayName = "CardHeader";
+
+))
+CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
+ HTMLDivElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
-));
-CardTitle.displayName = "CardTitle";
+
+))
+CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
+ HTMLDivElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
-));
-CardDescription.displayName = "CardDescription";
+
+))
+CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
+ HTMLDivElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
-));
-CardContent.displayName = "CardContent";
+
+))
+CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
+ HTMLDivElement,
+ React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
-));
-CardFooter.displayName = "CardFooter";
+
+))
+CardFooter.displayName = "CardFooter"
-export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardDescription,
- CardContent,
-};
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/apps/web/src/components/ui/chart.tsx b/apps/web/src/components/ui/chart.tsx
index 81a217f8..39fba6d6 100644
--- a/apps/web/src/components/ui/chart.tsx
+++ b/apps/web/src/components/ui/chart.tsx
@@ -1,405 +1,365 @@
-"use client";
+"use client"
-import * as React from "react";
-import * as RechartsPrimitive from "recharts";
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
-import { cn } from "@/lib/utils";
+import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
-const THEMES = { light: "", dark: ".dark" } as const;
+const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
- [k in string]: {
- label?: React.ReactNode;
- icon?: React.ComponentType;
- } & (
- | { color?: string; theme?: never }
- | { color?: never; theme: Record }
- );
-};
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
type ChartContextProps = {
- config: ChartConfig;
-};
+ config: ChartConfig
+}
-const ChartContext = React.createContext(null);
+const ChartContext = React.createContext(null)
function useChart() {
- const context = React.useContext(ChartContext);
+ const context = React.useContext(ChartContext)
- if (!context) {
- throw new Error("useChart must be used within a ");
- }
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
- return context;
+ return context
}
const ChartContainer = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & {
- config: ChartConfig;
- children: React.ComponentProps<
- typeof RechartsPrimitive.ResponsiveContainer
- >["children"];
- }
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
>(({ id, className, children, config, ...props }, ref) => {
- const uniqueId = React.useId();
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
-
- return (
-
-
-
-
- {children}
-
-
-
- );
-});
-ChartContainer.displayName = "Chart";
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
- const colorConfig = Object.entries(config).filter(
- ([_, config]) => config.theme || config.color,
- );
-
- if (!colorConfig.length) {
- return null;
- }
-
- return (
-