diff --git a/apps/bot/bot.ts b/apps/bot/bot.ts index 065c6419..92140193 100644 --- a/apps/bot/bot.ts +++ b/apps/bot/bot.ts @@ -15,9 +15,9 @@ import { serve } from "bun"; import c from "config"; import { db } from "db"; import { eq } from "db/drizzle"; -import { discordVerification, users } from "db/schema"; +import { discordVerification } from "db/schema"; +import { getHacker } from "db/functions"; import { nanoid } from "nanoid"; -import { z } from "zod"; /* DISCORD BOT */ @@ -199,9 +199,8 @@ app.post("/api/checkDiscordVerification", async (h) => { return h.json({ success: false }); } console.log("got here 2"); - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, verification.clerkID), - }); + + const user = await getHacker(verification.clerkID, false); console.log("got here 2 with user", user); if (!user) { console.log("failed cause of no user in db"); @@ -210,7 +209,7 @@ app.post("/api/checkDiscordVerification", async (h) => { const { discordRole: userGroupRoleName } = ( c.groups as Record - )[Object.keys(c.groups)[user.group]]; + )[Object.keys(c.groups)[user.hackerData.group]]; const guild = client.guilds.cache.get(verification.guild); if (!guild) { @@ -228,7 +227,7 @@ app.post("/api/checkDiscordVerification", async (h) => { if (!role || !userGroupRole) { console.log( "failed cause could not find a role, was looking for group " + - user.group + + user.hackerData.group + " called " + userGroupRoleName, ); diff --git a/apps/web/src/actions/admin/scanner-admin-actions.ts b/apps/web/src/actions/admin/scanner-admin-actions.ts index 16389437..2faeb2e3 100644 --- a/apps/web/src/actions/admin/scanner-admin-actions.ts +++ b/apps/web/src/actions/admin/scanner-admin-actions.ts @@ -2,8 +2,8 @@ import { adminAction } from "@/lib/safe-action"; import { z } from "zod"; -import { db } from "db"; -import { scans, users } from "db/schema"; +import { db, sql } from "db"; +import { scans, userCommonData } from "db/schema"; import { eq, and } from "db/drizzle"; export const createScan = adminAction( z.object({ @@ -46,22 +46,10 @@ export const getScan = adminAction( }, ); -export const checkInUser = adminAction( - z.string(), - async (user, { userId: adminUserID }) => { - // Check if scanner is an admin - const isAdmin = ["admin", "super_admin"].includes( - ( - await db - .select({ role: users.role }) - .from(users) - .where(eq(users.clerkID, adminUserID)) - )[0].role, - ); - // Set checkedIn to true - return await db - .update(users) - .set({ checkedIn: true }) - .where(eq(users.clerkID, user)); - }, -); +export const checkInUser = adminAction(z.string(), async (user) => { + // Set checkinTimestamp + return await db + .update(userCommonData) + .set({ checkinTimestamp: sql`now()` }) + .where(eq(userCommonData.clerkID, user)); +}); diff --git a/apps/web/src/actions/admin/user-actions.ts b/apps/web/src/actions/admin/user-actions.ts index 31e5e649..0e650697 100644 --- a/apps/web/src/actions/admin/user-actions.ts +++ b/apps/web/src/actions/admin/user-actions.ts @@ -3,7 +3,7 @@ import { adminAction } from "@/lib/safe-action"; import { z } from "zod"; import { perms } from "config"; -import { users } from "db/schema"; +import { userCommonData } from "db/schema"; import { db } from "db"; import { eq } from "db/drizzle"; import { revalidatePath } from "next/cache"; @@ -21,9 +21,9 @@ export const updateRole = adminAction( throw new Error("You are not allowed to do this"); } await db - .update(users) + .update(userCommonData) .set({ role: roleToSet }) - .where(eq(users.clerkID, userIDToUpdate)); + .where(eq(userCommonData.clerkID, userIDToUpdate)); revalidatePath(`/admin/users/${userIDToUpdate}`); return { success: true }; }, @@ -37,9 +37,9 @@ export const setUserApproval = adminAction( async ({ userIDToUpdate, approved }, { user, userId }) => { await db - .update(users) - .set({ approved }) - .where(eq(users.clerkID, userIDToUpdate)); + .update(userCommonData) + .set({ isApproved: approved }) + .where(eq(userCommonData.clerkID, userIDToUpdate)); revalidatePath(`/admin/users/${userIDToUpdate}`); return { success: true }; }, diff --git a/apps/web/src/actions/rsvp.ts b/apps/web/src/actions/rsvp.ts index 1e13fdf7..a4a30b6d 100644 --- a/apps/web/src/actions/rsvp.ts +++ b/apps/web/src/actions/rsvp.ts @@ -4,19 +4,19 @@ import { authenticatedAction } from "@/lib/safe-action"; import { z } from "zod"; import { db } from "db"; import { eq } from "db/drizzle"; -import { users } from "db/schema"; +import { userCommonData } from "db/schema"; +import { getUser } from "db/functions"; export const rsvpMyself = authenticatedAction( z.any(), async (_, { userId }) => { - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); + const user = await getUser(userId); if (!user) throw new Error("User not found"); + await db - .update(users) - .set({ rsvp: true }) - .where(eq(users.clerkID, userId)); + .update(userCommonData) + .set({ isRSVPed: true }) + .where(eq(userCommonData.clerkID, userId)); return { success: true }; }, ); diff --git a/apps/web/src/actions/teams.ts b/apps/web/src/actions/teams.ts index dff785d0..6adc78d0 100644 --- a/apps/web/src/actions/teams.ts +++ b/apps/web/src/actions/teams.ts @@ -1,30 +1,22 @@ "use server"; -// TODO: update team /api enpoints to be actions +// TODO: update team /api endpoints to be actions import { authenticatedAction } from "@/lib/safe-action"; import { z } from "zod"; import { db } from "db"; -import { users, teams, invites } from "db/schema"; +import { userHackerData, teams, invites } from "db/schema"; import { eq } from "db/drizzle"; import { revalidatePath } from "next/cache"; -import { toCalendar } from "@internationalized/date"; +import { getHacker } from "db/functions"; export const leaveTeam = authenticatedAction( z.null(), async (_, { userId }) => { - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - with: { - team: true, - }, - }); - - if (!user) { - throw new Error("User not found"); - } + const user = await getHacker(userId, false); + if (!user) throw new Error("User not found"); - if (user.team === null || user.team === undefined) { + if (!user.hackerData.teamID) { revalidatePath("/dash/team"); return { success: false, @@ -34,13 +26,17 @@ export const leaveTeam = authenticatedAction( const result = await db.transaction(async (tx) => { await tx - .update(users) + .update(userHackerData) .set({ teamID: null }) - .where(eq(users.clerkID, userId)); + .where(eq(userHackerData.clerkID, user.clerkID)); const team = await tx.query.teams.findFirst({ - where: eq(teams.id, user.team?.id as string), // Added null check for user.team. Converted to string since TS does not realise for some reason that we checked above. + where: eq(teams.id, user.hackerData.teamID as string), // Converted to string since TS does not realise for some reason that we checked above. with: { - members: true, + members: { + with: { + commonData: true, + }, + }, }, }); @@ -71,7 +67,7 @@ export const leaveTeam = authenticatedAction( revalidatePath("/dash/team"); return { success: true, - message: `Team has been left. Ownership has been transferred to ${team.members[0].firstName} ${team.members[0].lastName}.`, + message: `Team has been left. Ownership has been transferred to ${team.members[0].commonData.firstName} ${team.members[0].commonData.lastName}.`, }; } revalidatePath("/dash/team"); diff --git a/apps/web/src/actions/user-profile-mod.ts b/apps/web/src/actions/user-profile-mod.ts index 9c73942c..fb6eb032 100644 --- a/apps/web/src/actions/user-profile-mod.ts +++ b/apps/web/src/actions/user-profile-mod.ts @@ -3,11 +3,12 @@ import { authenticatedAction } from "@/lib/safe-action"; import { z } from "zod"; import { db } from "db"; -import { users, profileData, registrationData } from "db/schema"; +import { userCommonData } from "db/schema"; import { eq } from "db/drizzle"; import { put } from "@vercel/blob"; import { decodeBase64AsFile } from "@/lib/utils/shared/files"; import { revalidatePath } from "next/cache"; +import { getUser } from "db/functions"; // TODO: Add skill updating export const modifyRegistrationData = authenticatedAction( @@ -16,14 +17,13 @@ export const modifyRegistrationData = authenticatedAction( skills: z.string().max(100), }), async ({ bio, skills }, { userId }) => { - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); + const user = await getUser(userId); if (!user) throw new Error("User not found"); + await db - .update(profileData) + .update(userCommonData) .set({ bio }) - .where(eq(profileData.hackerTag, user.hackerTag)); + .where(eq(userCommonData.clerkID, user.clerkID)); return { success: true, newbio: bio }; }, ); @@ -34,14 +34,13 @@ export const modifyAccountSettings = authenticatedAction( lastName: z.string().min(1).max(50), }), async ({ firstName, lastName }, { userId }) => { - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); + const user = await getUser(userId); if (!user) throw new Error("User not found"); + await db - .update(users) + .update(userCommonData) .set({ firstName, lastName }) - .where(eq(users.clerkID, userId)); + .where(eq(userCommonData.clerkID, userId)); return { success: true, newFirstName: firstName, @@ -54,16 +53,14 @@ export const updateProfileImage = authenticatedAction( z.object({ fileBase64: z.string(), fileName: z.string() }), async ({ fileBase64, fileName }, { userId }) => { const image = await decodeBase64AsFile(fileBase64, fileName); - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); + const user = await getUser(userId); if (!user) throw new Error("User not found"); const blobUpload = await put(image.name, image, { access: "public" }); await db - .update(profileData) + .update(userCommonData) .set({ profilePhoto: blobUpload.url }) - .where(eq(profileData.hackerTag, user.hackerTag)); + .where(eq(userCommonData.clerkID, user.clerkID)); revalidatePath("/settings/profile"); return { success: true }; }, diff --git a/apps/web/src/app/admin/check-in/page.tsx b/apps/web/src/app/admin/check-in/page.tsx index 2bbdafea..0dea1ab8 100644 --- a/apps/web/src/app/admin/check-in/page.tsx +++ b/apps/web/src/app/admin/check-in/page.tsx @@ -1,83 +1,43 @@ import CheckinScanner from "@/components/admin/scanner/CheckinScanner"; -import FullScreenMessage from "@/components/shared/FullScreenMessage"; -import { db } from "db"; -import { eq, and } from "db/drizzle"; -import { events, users, scans } from "db/schema"; +import { getUser } from "db/functions"; export default async function Page({ searchParams, }: { searchParams: { [key: string]: string | undefined }; }) { - // TODO: maybe move event existant check into a layout so it holds state? - - // if (!params || !params.id || isNaN(parseInt(params.id))) { - // return ( - // - // ); - // } - - // const event = await db.query.events.findFirst({ - // where: eq(events.id, parseInt(params.id)), - // }); - - // if (!event) { - // return ( - // - // ); - // } - - // Returns only if search params exist - if (searchParams.user) { - const [isChecked, scanUser, hasRSVPed] = await db.transaction( - async (tx) => { - const scanUser = await tx.query.users.findFirst({ - where: eq(users.clerkID, searchParams.user ?? "unknown"), - }); - if (!scanUser) { - return [null, null, null]; - } - const scan = await tx - .select({ - isChecked: users.checkedIn, - hasRSVPed: users.rsvp, - }) - .from(users) - .where(eq(users.clerkID, searchParams.user!)); - if (scan) { - return [scan[0].isChecked, scanUser, scan[0].hasRSVPed]; - } else { - return [null, scanUser, null]; - } - }, + if (!searchParams.user) + return ( +
+ +
); + const scanUser = await getUser(searchParams.user); + if (!scanUser) return (
); - } - // Fall through case return (
); diff --git a/apps/web/src/app/admin/events/new/page.tsx b/apps/web/src/app/admin/events/new/page.tsx index 6a11aa99..0050480d 100644 --- a/apps/web/src/app/admin/events/new/page.tsx +++ b/apps/web/src/app/admin/events/new/page.tsx @@ -1,4 +1,4 @@ -import NewEventForm from "@/components/admin/events/NewEventForm"; +import NewEventForm from "@/components/events/admin/NewEventForm"; export default function Page() { const defaultDate = new Date(); diff --git a/apps/web/src/app/admin/events/page.tsx b/apps/web/src/app/admin/events/page.tsx index 2a9d0ae1..980d2fcf 100644 --- a/apps/web/src/app/admin/events/page.tsx +++ b/apps/web/src/app/admin/events/page.tsx @@ -1,12 +1,11 @@ -import { db } from "db"; -import { DataTable } from "@/components/admin/events/EventDataTable"; -import { columns } from "@/components/admin/events/EventColumns"; +import { EventDataTable } from "@/components/events/shared/EventDataTable"; +import { columns } from "@/components/events/shared/EventColumns"; import { Button } from "@/components/shadcn/ui/button"; import { PlusCircle } from "lucide-react"; import Link from "next/link"; - +import { getAllEvents } from "db/functions"; export default async function Page() { - const events = await db.query.events.findMany(); + const events = await getAllEvents(); return (
@@ -17,7 +16,7 @@ export default async function Page() { Events

- {events.length} Event{events.length != 1 ? "s" : ""} + {events.length} Event{events.length != 1 && "s"}

@@ -30,7 +29,7 @@ export default async function Page() { - + ); } diff --git a/apps/web/src/app/admin/layout.tsx b/apps/web/src/app/admin/layout.tsx index 9cae4ee7..177a090c 100644 --- a/apps/web/src/app/admin/layout.tsx +++ b/apps/web/src/app/admin/layout.tsx @@ -1,18 +1,15 @@ import c from "config"; import Image from "next/image"; -import { db } from "db"; import { auth } from "@clerk/nextjs"; import Link from "next/link"; import { Button } from "@/components/shadcn/ui/button"; import DashNavItem from "@/components/dash/shared/DashNavItem"; -import { eq } from "db/drizzle"; -import { users } from "db/schema"; import FullScreenMessage from "@/components/shared/FullScreenMessage"; import ProfileButton from "@/components/shared/ProfileButton"; import { Suspense } from "react"; import ClientToast from "@/components/shared/ClientToast"; import { redirect } from "next/navigation"; -import NavBarLinksGrouper from "@/components/shared/NavBarLinksGrouper"; +import { getUser } from "db/functions"; interface AdminLayoutProps { children: React.ReactNode; @@ -25,9 +22,7 @@ export default async function AdminLayout({ children }: AdminLayoutProps) { return redirect("/sign-in"); } - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); + const user = await getUser(userId); if (!user || (user.role !== "admin" && user.role !== "super_admin")) { console.log("Denying admin access to user", user); diff --git a/apps/web/src/app/admin/page.tsx b/apps/web/src/app/admin/page.tsx index a22768e0..67a62997 100644 --- a/apps/web/src/app/admin/page.tsx +++ b/apps/web/src/app/admin/page.tsx @@ -6,30 +6,17 @@ import { CardTitle, CardDescription, } from "@/components/shadcn/ui/card"; -import { db } from "db"; -import { eq, desc } from "db/drizzle"; -import { users } from "db/schema"; import { Users, UserCheck, User2, TimerReset, MailCheck } from "lucide-react"; -import type { userType } from "@/lib/utils/shared/types"; -import { unstable_cache } from "next/cache"; -import { env } from "@/env"; +import type { User } from "db/types"; import { auth } from "@clerk/nextjs"; import { notFound } from "next/navigation"; +import { getAllUsers, getUser } from "db/functions"; export default async function Page() { - // const getCachedUsers = unstable_cache(getUsers, [`global_users_${env.INTERNAL_AUTH_KEY}`], { - // revalidate: 30, - // }); - const { userId } = auth(); - if (!userId) return notFound(); - const adminUser = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - orderBy: desc(users.createdAt), - }); - + const adminUser = await getUser(userId); if ( !adminUser || (adminUser.role !== "admin" && adminUser.role !== "super_admin") @@ -37,7 +24,7 @@ export default async function Page() { return notFound(); } - const allUsers = await getUsers(); + const allUsers = (await getAllUsers()) ?? []; const { rsvpCount, checkinCount, recentSignupCount } = getRecentRegistrationData(allUsers); @@ -142,7 +129,7 @@ export default async function Page() { ); } -function getRecentRegistrationData(users: userType[]) { +function getRecentRegistrationData(users: User[]) { type DateNumberMap = { [key: string]: number }; let rsvpCount = 0; @@ -162,10 +149,10 @@ function getRecentRegistrationData(users: userType[]) { } for (const user of users) { - if (user.rsvp) rsvpCount++; - if (user.checkedIn) checkinCount++; + if (user.isRSVPed) rsvpCount++; + if (user.checkinTimestamp) checkinCount++; - const stamp = user.createdAt.toISOString().split("T")[0]; + const stamp = user.signupTime.toISOString().split("T")[0]; if (recentSignupCount[stamp] != undefined) recentSignupCount[stamp]++; } @@ -173,9 +160,4 @@ function getRecentRegistrationData(users: userType[]) { return { rsvpCount, checkinCount, recentSignupCount }; } -async function getUsers() { - const usersReq = await db.query.users.findMany(); - return usersReq; -} - export const runtime = "edge"; diff --git a/apps/web/src/app/admin/scanner/[id]/page.tsx b/apps/web/src/app/admin/scanner/[id]/page.tsx index b48b2b49..ac0958fb 100644 --- a/apps/web/src/app/admin/scanner/[id]/page.tsx +++ b/apps/web/src/app/admin/scanner/[id]/page.tsx @@ -2,7 +2,8 @@ import PassScanner from "@/components/admin/scanner/PassScanner"; import FullScreenMessage from "@/components/shared/FullScreenMessage"; import { db } from "db"; import { eq, and } from "db/drizzle"; -import { events, users, scans } from "db/schema"; +import { getHacker } from "db/functions"; +import { events, userCommonData, scans } from "db/schema"; export default async function Page({ params, @@ -35,45 +36,37 @@ export default async function Page({ ); } - if (searchParams.user) { - const [scan, scanUser] = await db.transaction(async (tx) => { - const scanUser = await tx.query.users.findFirst({ - where: eq(users.clerkID, searchParams.user!), - }); - if (!scanUser) { - return [null, null]; - } - const scan = await tx.query.scans.findFirst({ - where: and( - eq(scans.eventID, event.id), - eq(scans.userID, scanUser.clerkID), - ), - }); - if (scan) { - return [scan, scanUser]; - } else { - return [null, scanUser]; - } - }); + if (!searchParams.user) { return (
); } + const scanUser = await getHacker(searchParams.user, false); + + const scan = !scanUser + ? null + : await db.query.scans.findFirst({ + where: and( + eq(scans.eventID, event.id), + eq(scans.userID, scanUser.clerkID), + ), + }); + return (
); diff --git a/apps/web/src/app/admin/users/[slug]/page.tsx b/apps/web/src/app/admin/users/[slug]/page.tsx index b65a0622..2e775fb5 100644 --- a/apps/web/src/app/admin/users/[slug]/page.tsx +++ b/apps/web/src/app/admin/users/[slug]/page.tsx @@ -1,11 +1,7 @@ -import { db } from "db"; -import { users } from "db/schema"; -import { eq } from "db/drizzle"; import Image from "next/image"; import { Button } from "@/components/shadcn/ui/button"; import { Badge } from "@/components/shadcn/ui/badge"; import { Info } from "lucide-react"; - import Link from "next/link"; import UpdateRoleDialog from "@/components/admin/users/UpdateRoleDialog"; import { @@ -19,26 +15,17 @@ import { notFound } from "next/navigation"; import { isUserAdmin } from "@/lib/utils/server/admin"; import ApproveUserButton from "@/components/admin/users/ApproveUserButton"; import c from "config"; +import { getHacker, getUser } from "db/functions"; export default async function Page({ params }: { params: { slug: string } }) { const { userId } = auth(); if (!userId) return notFound(); - const admin = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); - + const admin = await getUser(userId); if (!admin || !isUserAdmin(admin)) return notFound(); - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, params.slug), - with: { - profileData: true, - registrationData: true, - team: true, - }, - }); + const user = await getHacker(params.slug, true); if (!user) { return

User Not Found

; @@ -70,7 +57,7 @@ export default async function Page({ params }: { params: { slug: string } }) { {(c.featureFlags.core.requireUsersApproval as boolean) && ( )} @@ -81,7 +68,7 @@ export default async function Page({ params }: { params: { slug: string } }) { {`Profile @@ -91,11 +78,11 @@ export default async function Page({ params }: { params: { slug: string } }) {

@{user.hackerTag}

- {/*

{team.bio}

*/} + {/*

{team.bio}

*/}
Joined{" "} - {user.createdAt + {user.signupTime .toDateString() .split(" ") .slice(1) diff --git a/apps/web/src/app/admin/users/page.tsx b/apps/web/src/app/admin/users/page.tsx index b1b17c23..5ea82481 100644 --- a/apps/web/src/app/admin/users/page.tsx +++ b/apps/web/src/app/admin/users/page.tsx @@ -5,9 +5,7 @@ import { Button } from "@/components/shadcn/ui/button"; import { FolderInput } from "lucide-react"; import { DefaultPagination } from "@/components/admin/users/DefaultPagination"; import SearchBar from "@/components/admin/users/SearchBar"; -import Filters from "../../../components/admin/users/Filters"; -import { users } from "db/schema"; -import { parseCheckBoxParams } from "@/lib/utils/shared/pageParams"; +import { userCommonData } from "db/schema"; export default async function Page({ searchParams, @@ -27,15 +25,12 @@ export default async function Page({ const end = maxPerPage + start; // Might want to work with cache in prod to see if this will be plausible to do - const userData = await db.query.users.findMany({ - with: { - registrationData: true, - profileData: true, - }, + const userData = await db.query.userCommonData.findMany({ + with: { hackerData: true }, where: and( or( - ilike(users.firstName, `%${user}%`), - ilike(users.lastName, `%${user}%`), + ilike(userCommonData.firstName, `%${user}%`), + ilike(userCommonData.lastName, `%${user}%`), ), ), }); diff --git a/apps/web/src/app/api/admin/events/create/route.ts b/apps/web/src/app/api/admin/events/create/route.ts index 9ca5ec1e..6c345aab 100644 --- a/apps/web/src/app/api/admin/events/create/route.ts +++ b/apps/web/src/app/api/admin/events/create/route.ts @@ -1,23 +1,21 @@ import { auth } from "@clerk/nextjs"; -import { eq } from "db/drizzle"; import { db } from "db"; -import { users, events } from "db/schema"; -import { newEventValidator } from "@/validators/shared/newEvent"; +import { events } from "db/schema"; +import { newEventFormSchema } from "@/validators/event"; import { BasicRedirValidator } from "@/validators/shared/basicRedir"; import { NextResponse } from "next/server"; import { z } from "zod"; import superjson from "superjson"; import c from "config"; +import { getUser } from "db/functions"; +// Make this a server action export async function POST(req: Request) { const { userId } = auth(); if (!userId) return new Response("Unauthorized", { status: 401 }); - const reqUserRecord = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); - + const reqUserRecord = await getUser(userId); if ( !reqUserRecord || (reqUserRecord.role !== "super_admin" && reqUserRecord.role !== "admin") @@ -25,9 +23,8 @@ export async function POST(req: Request) { return new Response("Unauthorized", { status: 401 }); } - // console.log(await req.json()); const body = superjson.parse(await req.text()); - const parsedBody = newEventValidator.safeParse(body); + const parsedBody = newEventFormSchema.safeParse(body); if (!parsedBody.success) { return new Response("Malformed request body.", { status: 400 }); diff --git a/apps/web/src/app/api/admin/export/route.ts b/apps/web/src/app/api/admin/export/route.ts index 404a1fdb..cef66fc0 100644 --- a/apps/web/src/app/api/admin/export/route.ts +++ b/apps/web/src/app/api/admin/export/route.ts @@ -1,7 +1,5 @@ -import { db } from "db"; -import { eq } from "db/drizzle"; -import { users } from "db/schema"; import { auth } from "@clerk/nextjs"; +import { getAllHackers, getUser } from "db/functions"; function escape(value: any) { if (value === null) return "None"; @@ -37,10 +35,7 @@ export async function GET() { if (!userId) return new Response("Unauthorized", { status: 401 }); - const reqUserRecord = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); - + const reqUserRecord = await getUser(userId); if ( !reqUserRecord || (reqUserRecord.role !== "super_admin" && reqUserRecord.role !== "admin") @@ -48,26 +43,19 @@ export async function GET() { return new Response("Unauthorized", { status: 401 }); } - const userTableData = await db.query.users.findMany({ - with: { - registrationData: true, - profileData: true, - }, - }); + const userTableData = (await getAllHackers()) ?? []; - const columed = userTableData.map((user) => { + const flattenedUsers = userTableData.map((user) => { // TODO: Have to use any here to avoid type errors as we reshape the data. Could be fixed with a better type definition. let toRet: any = { ...user, - ...user.registrationData, - ...user.profileData, + ...user.hackerData, }; - delete toRet.registrationData; - delete toRet.profileData; + delete toRet.hackerData; return toRet; }); - const csv = jsonToCSV(columed); + const csv = jsonToCSV(flattenedUsers); return new Response(csv, { headers: { diff --git a/apps/web/src/app/api/registration/create/route.ts b/apps/web/src/app/api/registration/create/route.ts index 048c8951..783dc742 100644 --- a/apps/web/src/app/api/registration/create/route.ts +++ b/apps/web/src/app/api/registration/create/route.ts @@ -1,12 +1,12 @@ import { currentUser, clerkClient } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import { db } from "db"; -import { eq, sql } from "db/drizzle"; -import { users, registrationData, profileData } from "db/schema"; +import { sql } from "db/drizzle"; +import { userCommonData, userHackerData } from "db/schema"; import { RegisterFormValidator } from "@/validators/shared/RegisterForm"; import c from "config"; import { z } from "zod"; -import { sendEmail } from "@/lib/utils/server/ses"; +import { getUser, getUserByTag } from "db/functions"; export async function POST(req: Request) { const rawBody = await req.json(); @@ -49,11 +49,9 @@ export async function POST(req: Request) { ); } - // TODO: Might be removable? Not sure if this is needed. In every case, the sure should have a peice of metadata that says if they are registered or not. + // TODO: Might be removable? Not sure if this is needed. In every case, the sure should have a piece of metadata that says if they are registered or not. - const lookupByUserID = await db.query.users.findFirst({ - where: eq(users.clerkID, user.id), - }); + const lookupByUserID = await getUser(user.id); if (lookupByUserID) { return NextResponse.json( @@ -65,9 +63,7 @@ export async function POST(req: Request) { ); } - const lookupByHackerTag = await db.query.users.findFirst({ - where: eq(users.hackerTag, body.hackerTag.toLowerCase()), - }); + const lookupByHackerTag = await getUserByTag(body.hackerTag.toLowerCase()); if (lookupByHackerTag) { return NextResponse.json({ @@ -78,9 +74,9 @@ export async function POST(req: Request) { const totalUserCount = await db .select({ count: sql`count(*)`.mapWith(Number) }) - .from(users); + .from(userCommonData); - if (!body.acceptsMLHCodeOfConduct || !body.shareDataWithMLH) { + if (!body.hasAcceptedMLHCoC || !body.hasSharedDataWithMLH) { return NextResponse.json({ success: false, message: @@ -89,49 +85,47 @@ export async function POST(req: Request) { } await db.transaction(async (tx) => { - await tx.insert(users).values({ + await tx.insert(userCommonData).values({ clerkID: user.id, firstName: body.firstName, lastName: body.lastName, email: body.email, hackerTag: body.hackerTag.toLowerCase(), - registrationComplete: true, - group: totalUserCount[0].count % Object.keys(c.groups).length, - hasSearchableProfile: body.profileIsSearchable, - }); - - await tx.insert(registrationData).values({ - clerkID: user.id, - acceptedMLHCodeOfConduct: body.acceptsMLHCodeOfConduct, - accommodationNote: body.accommodationNote || null, age: body.age, - dietRestrictions: body.dietaryRestrictions, - ethnicity: body.ethnicity, gender: body.gender, - hackathonsAttended: body.hackathonsAttended, - heardFrom: body.heardAboutEvent || null, - levelOfStudy: body.levelOfStudy, - major: body.major, race: body.race, - sharedDataWithMLH: body.shareDataWithMLH, + ethnicity: body.ethnicity, shirtSize: body.shirtSize, - shortID: body.shortID, - softwareExperience: body.softwareBuildingExperience, + dietRestrictions: body.dietaryRestrictions, + accommodationNote: body.accommodationNote || null, + discord: body.profileDiscordName, + pronouns: body.pronouns, + bio: body.bio, + skills: body.skills.map((v) => v.text.toLowerCase()), + profilePhoto: user.imageUrl, + isFullyRegistered: true, + phoneNumber:body.phoneNumber, + isSearchable: body.profileIsSearchable, + countryOfResidence:body.countryOfResidence, + }); + + await tx.insert(userHackerData).values({ + clerkID: user.id, university: body.university, - wantsToReceiveMLHEmails: body.wantsToReceiveMLHEmails, + major: body.major, + schoolID: body.schoolID, + levelOfStudy: body.levelOfStudy, + hackathonsAttended: body.hackathonsAttended, + softwareExperience: body.softwareBuildingExperience, + heardFrom: body.heardAboutEvent || null, GitHub: body.github, LinkedIn: body.linkedin, PersonalWebsite: body.personalWebsite, resume: body.resume, - }); - - await tx.insert(profileData).values({ - bio: body.bio, - discordUsername: body.profileDiscordName, - hackerTag: body.hackerTag.toLowerCase(), - profilePhoto: user.imageUrl, - pronouns: body.pronouns, - skills: body.skills.map((v) => v.text.toLowerCase()), + group: totalUserCount[0].count % Object.keys(c.groups).length, + hasAcceptedMLHCoC: body.hasAcceptedMLHCoC, + hasSharedDataWithMLH: body.hasSharedDataWithMLH, + isEmailable: body.isEmailable, }); }); diff --git a/apps/web/src/app/api/team/create/route.ts b/apps/web/src/app/api/team/create/route.ts index de723958..6260335c 100644 --- a/apps/web/src/app/api/team/create/route.ts +++ b/apps/web/src/app/api/team/create/route.ts @@ -2,22 +2,21 @@ import { auth } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import { db } from "db"; import { eq } from "db/drizzle"; -import { users, teams, errorLog } from "db/schema"; +import { userHackerData, teams } from "db/schema"; +import { getHacker } from "db/functions"; import { newTeamValidator } from "@/validators/shared/team"; import { nanoid } from "nanoid"; import c from "config"; import { logError } from "@/lib/utils/server/logError"; export async function POST(req: Request) { - const { userId } = await auth(); + const { userId } = auth(); if (!userId) return new Response("Unauthorized", { status: 401 }); - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); + const user = await getHacker(userId, false); if (!user) return new Response("Unauthorized", { status: 401 }); - if (user.teamID) { + if (user.hackerData.teamID) { return NextResponse.json({ success: false, message: @@ -47,11 +46,9 @@ export async function POST(req: Request) { ownerID: userId, }); await tx - .update(users) - .set({ - teamID, - }) - .where(eq(users.clerkID, userId)); + .update(userHackerData) + .set({ teamID }) + .where(eq(userHackerData.clerkID, userId)); }); return NextResponse.json({ success: true, diff --git a/apps/web/src/app/api/team/invite/accept/route.ts b/apps/web/src/app/api/team/invite/accept/route.ts index 81b8a328..e3e9fd31 100644 --- a/apps/web/src/app/api/team/invite/accept/route.ts +++ b/apps/web/src/app/api/team/invite/accept/route.ts @@ -2,7 +2,7 @@ import { serverZodResponse } from "@/lib/utils/server/types"; import { BasicServerValidator } from "@/validators/shared/basic"; import { db } from "db"; import { eq, and } from "db/drizzle"; -import { users, invites, teams } from "db/schema"; +import { userCommonData, userHackerData, invites, teams } from "db/schema"; import { auth } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import { z } from "zod"; @@ -28,18 +28,21 @@ export async function POST( }); } - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), + const user = await db.query.userCommonData.findFirst({ + where: eq(userCommonData.clerkID, userId), with: { - team: true, - invites: { - where: eq(invites.teamID, body.data.teamInviteID), + hackerData: { + with: { + invites: { + where: eq(invites.teamID, body.data.teamInviteID), + }, + }, }, }, }); if (!user) return NextResponse.json("Unauthorized", { status: 401 }); - if (user.team) { + if (user.hackerData.teamID) { return NextResponse.json({ success: false, message: "You are already on a team.", @@ -47,7 +50,7 @@ export async function POST( }); } - if (user.invites.length === 0) { + if (user.hackerData.invites.length === 0) { return NextResponse.json({ success: false, message: "You have not been invited to this team.", @@ -56,7 +59,7 @@ export async function POST( } const team = await db.query.teams.findFirst({ - where: eq(teams.id, user.invites[0].teamID), + where: eq(teams.id, user.hackerData.invites[0].teamID), with: { members: true, }, @@ -79,16 +82,17 @@ export async function POST( } await db - .update(users) - .set({ teamID: user.invites[0].teamID }) - .where(eq(users.clerkID, userId)); - // TODO: would be interesting to see if the and() could be reomved here in favor of directly looking up the composite key. + .update(userHackerData) + .set({ teamID: user.hackerData.invites[0].teamID }) + .where(eq(userHackerData.clerkID, userId)); + + // TODO: would be interesting to see if the and() could be removed here in favor of directly looking up the composite key. await db .update(invites) .set({ status: "accepted" }) .where( and( - eq(invites.teamID, user.invites[0].teamID), + eq(invites.teamID, user.hackerData.invites[0].teamID), eq(invites.inviteeID, userId), ), ); diff --git a/apps/web/src/app/api/team/invite/create/route.ts b/apps/web/src/app/api/team/invite/create/route.ts index 2577229b..c6b59277 100644 --- a/apps/web/src/app/api/team/invite/create/route.ts +++ b/apps/web/src/app/api/team/invite/create/route.ts @@ -1,25 +1,24 @@ import { auth } from "@clerk/nextjs"; import { db } from "db"; import { eq } from "db/drizzle"; -import { users } from "db/schema"; +import { userCommonData } from "db/schema"; import { NextResponse } from "next/server"; import { newInviteValidator } from "@/validators/shared/team"; import { BasicServerValidator } from "@/validators/shared/basic"; import { invites } from "db/schema"; +import { getHacker } from "db/functions"; import type { serverZodResponse } from "@/lib/utils/server/types"; export async function POST( req: Request, ): serverZodResponse { - const { userId } = await auth(); + const { userId } = auth(); if (!userId) return NextResponse.json("Unauthorized", { status: 401 }); - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - with: { team: true }, - }); + + const user = await getHacker(userId, true); if (!user) return NextResponse.json("Unauthorized", { status: 401 }); - if (!user.teamID || !user.team) { + if (!user.hackerData.teamID || !user.hackerData.team) { return NextResponse.json( { success: false, @@ -30,7 +29,7 @@ export async function POST( ); } - if (user.team.ownerID !== userId) { + if (user.hackerData.team.ownerID !== userId) { return NextResponse.json( { success: false, @@ -49,12 +48,16 @@ export async function POST( }); } - const invitee = await db.query.users.findFirst({ - where: eq(users.hackerTag, body.data.inviteeTag), + const invitee = await db.query.userCommonData.findFirst({ + where: eq(userCommonData.hackerTag, body.data.inviteeTag), with: { - team: true, - invites: { - where: eq(invites.teamID, user.teamID), + hackerData: { + with: { + team: true, + invites: { + where: eq(invites.teamID, user.hackerData.teamID), + }, + }, }, }, }); @@ -67,7 +70,7 @@ export async function POST( }); } - if (invitee.invites.length > 0) { + if (invitee.hackerData.invites.length > 0) { return NextResponse.json({ success: false, message: "That user already has an invite.", @@ -77,7 +80,7 @@ export async function POST( await db.insert(invites).values({ inviteeID: invitee.clerkID, - teamID: user.teamID, + teamID: user.hackerData.teamID, }); return NextResponse.json({ diff --git a/apps/web/src/app/api/team/invite/decline/route.ts b/apps/web/src/app/api/team/invite/decline/route.ts index 6c92487b..3f5164e8 100644 --- a/apps/web/src/app/api/team/invite/decline/route.ts +++ b/apps/web/src/app/api/team/invite/decline/route.ts @@ -2,7 +2,7 @@ import { auth } from "@clerk/nextjs"; import { NextResponse } from "next/server"; import { z } from "zod"; import { db } from "db"; -import { users, invites, teams } from "db/schema"; +import { userCommonData, invites } from "db/schema"; import { eq, and } from "db/drizzle"; const inviteDeclineValidator = z.object({ @@ -23,17 +23,23 @@ export async function POST(req: Request) { }); } - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), + // TODO(xander): adjust logic here. null check shouldnt require a join, and invite can be queried directly + const user = await db.query.userCommonData.findFirst({ + where: eq(userCommonData.clerkID, userId), with: { - invites: { - where: eq(invites.teamID, body.data.teamInviteID), + hackerData: { + with: { + invites: { + where: eq(invites.teamID, body.data.teamInviteID), + }, + }, }, }, }); if (!user) return NextResponse.json("Unauthorized", { status: 401 }); + // TODO(xander): get invite using body data here to avoid joins above await db .update(invites) .set({ @@ -41,7 +47,7 @@ export async function POST(req: Request) { }) .where( and( - eq(invites.teamID, user.invites[0].teamID), + eq(invites.teamID, user.hackerData.invites[0].teamID), eq(invites.inviteeID, userId), ), ); diff --git a/apps/web/src/app/dash/layout.tsx b/apps/web/src/app/dash/layout.tsx index 428d7a29..4b84510b 100644 --- a/apps/web/src/app/dash/layout.tsx +++ b/apps/web/src/app/dash/layout.tsx @@ -10,9 +10,7 @@ import ProfileButton from "@/components/shared/ProfileButton"; import ClientToast from "@/components/shared/ClientToast"; import { TRPCReactProvider } from "@/trpc/react"; -import { db } from "db"; -import { eq } from "db/drizzle"; -import { users } from "db/schema"; +import { getUser } from "db/functions"; interface DashLayoutProps { children: React.ReactNode; @@ -25,15 +23,12 @@ export default async function DashLayout({ children }: DashLayoutProps) { return redirect("/register"); } - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, clerkUser.id), - }); - + const user = await getUser(clerkUser.id); if (!user) return redirect("/register"); if ( (c.featureFlags.core.requireUsersApproval as boolean) === true && - user.approved === false && + user.isApproved === false && user.role === "hacker" ) { return redirect("/i/approval"); diff --git a/apps/web/src/app/dash/page.tsx b/apps/web/src/app/dash/page.tsx index b1de2cd5..e7fc5e28 100644 --- a/apps/web/src/app/dash/page.tsx +++ b/apps/web/src/app/dash/page.tsx @@ -1,13 +1,5 @@ -// import { SignOutButton } from "@clerk/nextjs"; -// import { Button } from "@/components/shadcn/ui/button"; -// import { Suspense } from "react"; -// import Loading from "@/components/shared/Loading"; import { auth } from "@clerk/nextjs"; -import { db } from "db"; -import { users } from "db/schema"; -import { eq } from "db/drizzle"; import c from "config"; -import superjson from "superjson"; import { createQRpayload } from "@/lib/utils/shared/qr"; // HackKit Bubbles @@ -18,13 +10,12 @@ import { TitleBubble, QuickQR, } from "@/components/dash/overview/ServerBubbles"; +import { getUser } from "db/functions"; export default async function Page() { const { userId } = auth(); if (!userId) return null; - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); + const user = await getUser(userId); if (!user) return null; const qrPayload = createQRpayload({ diff --git a/apps/web/src/app/dash/pass/page.tsx b/apps/web/src/app/dash/pass/page.tsx index a179da97..cf75abd6 100644 --- a/apps/web/src/app/dash/pass/page.tsx +++ b/apps/web/src/app/dash/pass/page.tsx @@ -1,9 +1,5 @@ import QRCode from "react-qr-code"; import { currentUser } from "@clerk/nextjs"; -import superjson from "superjson"; -import { db } from "db"; -import { eq, InferModel } from "db/drizzle"; -import { users } from "db/schema"; import Image from "next/image"; import c from "config"; import { format } from "date-fns"; @@ -11,17 +7,14 @@ import TiltWrapper from "@/components/dash/shared/TiltWrapper"; import { createQRpayload } from "@/lib/utils/shared/qr"; import { Drawer, - DrawerClose, DrawerContent, - DrawerDescription, - DrawerFooter, - DrawerHeader, - DrawerTitle, DrawerTrigger, } from "@/components/shadcn/ui/drawer"; +import { getHacker } from "db/functions"; +import { Hacker } from "db/types"; interface EventPassProps { - user: InferModel; + user: Hacker; clerk: NonNullable>>; qrPayload: string; guild: string; @@ -31,17 +24,14 @@ export default async function Page() { const user = await currentUser(); if (!user) return null; - const userDbRecord = await db.query.users.findFirst({ - where: eq(users.clerkID, user.id), - }); - + const userDbRecord = await getHacker(user.id, false); if (!userDbRecord) return null; const qrPayload = createQRpayload({ userID: user.id, createdAt: new Date(), }); - const guild = Object.keys(c.groups)[userDbRecord.group]; + const guild = Object.keys(c.groups)[userDbRecord.hackerData.group]; return (
diff --git a/apps/web/src/app/dash/schedule/page.tsx b/apps/web/src/app/dash/schedule/page.tsx index 3650b296..1d9b20fc 100644 --- a/apps/web/src/app/dash/schedule/page.tsx +++ b/apps/web/src/app/dash/schedule/page.tsx @@ -1,21 +1,11 @@ -import c from "config"; -import Day from "@/components/schedule/Day"; -import { db } from "db"; -import { format, compareAsc } from "date-fns"; -import { type ReactNode } from "react"; -import { formatInTimeZone } from "date-fns-tz"; -import { redirect } from "next/navigation"; - -export default async function Page() { - const events = await db.query.events.findMany(); - +import { Suspense } from "react"; +import UserScheduleView from "@/components/schedule/UserScheduleView"; +import Loading from "@/components/shared/Loading"; +export default function Page() { return ( -
-

- Bug with Scheduling was found. Fix Coming soon! -

-

- Christian

-
+ }> + + ); } diff --git a/apps/web/src/app/dash/team/page.tsx b/apps/web/src/app/dash/team/page.tsx index 302bca9e..c695aa7a 100644 --- a/apps/web/src/app/dash/team/page.tsx +++ b/apps/web/src/app/dash/team/page.tsx @@ -1,7 +1,7 @@ import c from "config"; import { auth } from "@clerk/nextjs"; import { db } from "db"; -import { users } from "db/schema"; +import { userCommonData } from "db/schema"; import { eq } from "db/drizzle"; import { Button } from "@/components/shadcn/ui/button"; import Link from "next/link"; @@ -17,19 +17,23 @@ export default async function Page() { if (!userId) return null; // TODO: make this db query not so bad - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), + const user = await db.query.userCommonData.findFirst({ + where: eq(userCommonData.clerkID, userId), with: { - invites: { + hackerData: { with: { - team: true, - }, - }, - team: { - with: { - members: { + team: { + with: { + members: { + with: { + commonData: true, + }, + }, + }, + }, + invites: { with: { - profileData: true, + team: true, }, }, }, @@ -38,7 +42,7 @@ export default async function Page() { }); if (!user) return null; - if (!user.teamID) { + if (!user.hackerData.teamID) { return (
@@ -70,8 +74,8 @@ export default async function Page() {

Invitations

- {user.invites.length > 0 ? ( - user.invites.map((invite) => ( + {user.hackerData.invites.length > 0 ? ( + user.hackerData.invites.map((invite) => (
); } else { - if (!user.team) return null; - const team = user.team; + if (!user.hackerData.team) return null; + const team = user.hackerData.team; return (
@@ -154,27 +158,34 @@ export default async function Page() { }} > {team.members.map((member) => ( - - + +
{`${member.hackerTag}'s

- {member.firstName}{" "} - {member.lastName} + { + member.commonData + .firstName + }{" "} + {member.commonData.lastName}

- @{member.hackerTag} + @ + { + member.commonData + .hackerTag + }

diff --git a/apps/web/src/app/discord-verify/linked/page.tsx b/apps/web/src/app/discord-verify/linked/page.tsx index 5c50965a..c5874538 100644 --- a/apps/web/src/app/discord-verify/linked/page.tsx +++ b/apps/web/src/app/discord-verify/linked/page.tsx @@ -19,7 +19,7 @@ export default function Page() { To unlink, go to your {c.hackathonName} account settings to unlink before linking a new one.

- +
diff --git a/apps/web/src/app/discord-verify/page.tsx b/apps/web/src/app/discord-verify/page.tsx index 64af879e..bcb94527 100644 --- a/apps/web/src/app/discord-verify/page.tsx +++ b/apps/web/src/app/discord-verify/page.tsx @@ -1,5 +1,5 @@ import { db } from "db"; -import { discordVerification, users } from "db/schema"; +import { discordVerification, userCommonData } from "db/schema"; import { eq, and, or } from "db/drizzle"; import { notFound, redirect } from "next/navigation"; import { auth } from "@clerk/nextjs"; @@ -7,7 +7,6 @@ import c from "config"; import Balancer from "react-wrap-balancer"; import { MoveRight } from "lucide-react"; import Image from "next/image"; -import { Button } from "@/components/shadcn/ui/button"; import ClientToast from "@/components/shared/ClientToast"; import DiscordVerifyButton from "@/components/settings/DiscordVerifyButton"; @@ -32,8 +31,8 @@ export default async function Page({ return redirect("/sign-in"); } - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), + const user = await db.query.userCommonData.findFirst({ + where: eq(userCommonData.clerkID, userId), with: { discordVerification: true, }, @@ -45,7 +44,7 @@ export default async function Page({ if ( (c.featureFlags.core.requireUsersApproval as boolean) === true && - user.approved === false && + user.isApproved === false && user.role === "hacker" ) { return redirect("/i/approval"); diff --git a/apps/web/src/app/register/page.tsx b/apps/web/src/app/register/page.tsx index 188ce88a..db808b98 100644 --- a/apps/web/src/app/register/page.tsx +++ b/apps/web/src/app/register/page.tsx @@ -1,32 +1,23 @@ import c from "config"; import RegisterForm from "@/components/registration/RegisterForm"; import { auth, currentUser } from "@clerk/nextjs"; -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"; import Link from "next/link"; import { kv } from "@vercel/kv"; import { parseRedisBoolean } from "@/lib/utils/server/redis"; import { Button } from "@/components/shadcn/ui/button"; +import { getUser } from "db/functions"; export default async function Page() { const { userId } = auth(); - if (!userId) return redirect("/sign-up"); const user = await currentUser(); - if (!user) return redirect("/sign-up"); - const registration = await db.query.users.findFirst({ - where: eq(users.clerkID, user.id), - }); - - if (registration) { - return redirect("/dash"); - } + const registration = await getUser(userId); + if (registration) return redirect("/dash"); const [defaultRegistrationEnabled, defaultSecretRegistrationEnabled]: ( | string diff --git a/apps/web/src/app/rsvp/page.tsx b/apps/web/src/app/rsvp/page.tsx index 975e16c4..1b0b82de 100644 --- a/apps/web/src/app/rsvp/page.tsx +++ b/apps/web/src/app/rsvp/page.tsx @@ -4,14 +4,14 @@ import { auth } from "@clerk/nextjs"; import { redirect } from "next/navigation"; import { db } from "db"; import { eq } from "db/drizzle"; -import { users } from "db/schema"; +import { userCommonData } from "db/schema"; import ClientToast from "@/components/shared/ClientToast"; import { SignedOut, RedirectToSignIn } from "@clerk/nextjs"; import { kv } from "@vercel/kv"; import { parseRedisBoolean } from "@/lib/utils/server/redis"; import Link from "next/link"; import { Button } from "@/components/shadcn/ui/button"; -import { CheckCircleIcon } from "lucide-react"; +import { getUser } from "db/functions"; export default async function RsvpPage({ searchParams, @@ -29,17 +29,12 @@ export default async function RsvpPage({ ); } - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - }); - - if (!user) { - return redirect("/register"); - } + const user = await getUser(userId); + if (!user) return redirect("/register"); if ( (c.featureFlags.core.requireUsersApproval as boolean) === true && - user.approved === false && + user.isApproved === false && user.role === "hacker" ) { return redirect("/i/approval"); @@ -53,7 +48,7 @@ export default async function RsvpPage({ rsvpEnabled as string | boolean | null | undefined, true, ) === true || - user.rsvp === true + user.isRSVPed === true ) { return ( <> @@ -66,7 +61,7 @@ export default async function RsvpPage({

RSVP

- +
); diff --git a/apps/web/src/app/schedule/[id]/page.tsx b/apps/web/src/app/schedule/[id]/page.tsx index dc45b9c6..c5c1ad74 100644 --- a/apps/web/src/app/schedule/[id]/page.tsx +++ b/apps/web/src/app/schedule/[id]/page.tsx @@ -2,7 +2,7 @@ import { db } from "db"; import { eq } from "db/drizzle"; import { events } from "db/schema"; import FullScreenMessage from "@/components/shared/FullScreenMessage"; -import EventFull from "@/components/schedule/EventFull"; +import EventDetails from "@/components/events/admin/EventDetails"; import Navbar from "@/components/shared/Navbar"; export default async function Page({ params }: { params: { id: string } }) { @@ -33,7 +33,7 @@ export default async function Page({ params }: { params: { id: string } }) { return ( <> - + ); } diff --git a/apps/web/src/app/settings/account/page.tsx b/apps/web/src/app/settings/account/page.tsx index c9cd28cf..fbfdd1e0 100644 --- a/apps/web/src/app/settings/account/page.tsx +++ b/apps/web/src/app/settings/account/page.tsx @@ -1,16 +1,13 @@ import AccountSettings from "@/components/settings/AccountSettings"; -import { users } from "db/schema"; -import { eq } from "db/drizzle"; import { auth } from "@clerk/nextjs"; -import { db } from "db"; import { redirect } from "next/navigation"; +import { getHacker } from "db/functions"; export default async function Page() { const { userId } = auth(); - const user = await db.query.users.findFirst({ - with: { registrationData: true }, - where: eq(users.clerkID, userId!), - }); + if (!userId) return redirect("/sign-in"); + + const user = await getHacker(userId, false); if (!user) return redirect("/sign-in"); return ; } diff --git a/apps/web/src/app/settings/profile/page.tsx b/apps/web/src/app/settings/profile/page.tsx index 6140f8bb..06e9a82d 100644 --- a/apps/web/src/app/settings/profile/page.tsx +++ b/apps/web/src/app/settings/profile/page.tsx @@ -1,24 +1,17 @@ import ProfileSettings from "@/components/settings/ProfileSettings"; -import { db } from "db"; -import { users } from "db/schema"; -import { eq } from "db/drizzle"; import { auth } from "@clerk/nextjs"; +import { getHacker } from "db/functions"; export default async function Page() { const { userId } = auth(); if (!userId) throw new Error("User not found"); - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - with: { - profileData: true, - registrationData: true, - }, - }); + + const user = await getHacker(userId, false); if (!user) throw new Error("User not found"); return ( ); } diff --git a/apps/web/src/app/team/[tag]/page.tsx b/apps/web/src/app/team/[tag]/page.tsx index e1a0c5ab..9bcb1912 100644 --- a/apps/web/src/app/team/[tag]/page.tsx +++ b/apps/web/src/app/team/[tag]/page.tsx @@ -14,7 +14,7 @@ export default async function Page({ params }: { params: { tag: string } }) { with: { members: { with: { - profileData: true, + commonData: true, }, }, }, @@ -41,21 +41,22 @@ export default async function Page({ params }: { params: { tag: string } }) { )}
{team.members.map((member) => ( - +
{`${member.hackerTag}'s

- {member.firstName} {member.lastName} + {member.commonData.firstName}{" "} + {member.commonData.lastName}

- @{member.hackerTag} + @{member.commonData.hackerTag}

diff --git a/apps/web/src/app/user/[tag]/page.tsx b/apps/web/src/app/user/[tag]/page.tsx index db7a3dc7..f4d0c5ed 100644 --- a/apps/web/src/app/user/[tag]/page.tsx +++ b/apps/web/src/app/user/[tag]/page.tsx @@ -1,6 +1,3 @@ -import { db } from "db"; -import { users } from "db/schema"; -import { eq } from "db/drizzle"; import { notFound } from "next/navigation"; import Image from "next/image"; import RoleBadge from "@/components/dash/shared/RoleBadge"; @@ -8,15 +5,12 @@ import { Balancer } from "react-wrap-balancer"; import Link from "next/link"; import { Github, Linkedin, Globe } from "lucide-react"; import Navbar from "@/components/shared/Navbar"; +import { getHackerByTag } from "db/functions"; export default async function ({ params }: { params: { tag: string } }) { if (!params.tag || params.tag.length <= 1) return notFound(); - const user = await db.query.users.findFirst({ - where: eq(users.hackerTag, params.tag), - with: { profileData: true, registrationData: true }, - }); - + const user = await getHackerByTag(params.tag, false); if (!user) return notFound(); return ( @@ -29,7 +23,7 @@ export default async function ({ params }: { params: { tag: string } }) {
{`@${user.hackerTag}'s @@ -43,53 +37,50 @@ export default async function ({ params }: { params: { tag: string } }) {
- {user.registrationData.GitHub && - user.registrationData.GitHub.length > 0 && ( + {user.hackerData.GitHub && + user.hackerData.GitHub.length > 0 && ( - {user.registrationData.GitHub} + {user.hackerData.GitHub} )} - {user.registrationData.LinkedIn && - user.registrationData.LinkedIn.length > 0 && ( + {user.hackerData.LinkedIn && + user.hackerData.LinkedIn.length > 0 && ( - {user.registrationData.LinkedIn} + {user.hackerData.LinkedIn} )} - {user.registrationData.PersonalWebsite && - user.registrationData.PersonalWebsite.length > - 0 && ( + {user.hackerData.PersonalWebsite && + user.hackerData.PersonalWebsite.length > 0 && ( - {user.registrationData.PersonalWebsite.replace( + {user.hackerData.PersonalWebsite.replace( "https://", "", ).replace("http://", "")} @@ -99,17 +90,12 @@ export default async function ({ params }: { params: { tag: string } }) {

About

- {user.profileData.bio} + {user.bio}

- {user.profileData.skills && - (user.profileData.skills as string[]).length > 0 ? ( + {user.skills && (user.skills as string[]).length > 0 ? ( <>

Skills

-

- {(user.profileData.skills as string[]).join( - ", ", - )} -

+

{(user.skills as string[]).join(", ")}

) : null}
diff --git a/apps/web/src/components/admin/scanner/CheckinScanner.tsx b/apps/web/src/components/admin/scanner/CheckinScanner.tsx index 57283642..b07abc5a 100644 --- a/apps/web/src/components/admin/scanner/CheckinScanner.tsx +++ b/apps/web/src/components/admin/scanner/CheckinScanner.tsx @@ -3,10 +3,10 @@ import { useState, useEffect } from "react"; import { QrScanner } from "@yudiel/react-qr-scanner"; import superjson from "superjson"; -import { getScan, checkInUser } from "@/actions/admin/scanner-admin-actions"; -import { useAction, useOptimisticAction } from "next-safe-action/hook"; +import { checkInUser } from "@/actions/admin/scanner-admin-actions"; +import { useAction } from "next-safe-action/hook"; import { type QRDataInterface } from "@/lib/utils/shared/qr"; -import type { scansType, userType, eventType } from "@/lib/utils/shared/types"; +import type { User } from "db/types"; import { Drawer, @@ -15,10 +15,8 @@ import { DrawerFooter, DrawerHeader, DrawerTitle, - DrawerTrigger, } from "@/components/shadcn/ui/drawer"; import { Button } from "@/components/shadcn/ui/button"; -import Link from "next/link"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { toast } from "sonner"; @@ -33,16 +31,13 @@ scan: the scan object that has been scanned. If they have not scanned before sca */ interface CheckinScannerProps { - // event: eventType; hasScanned: boolean; - // scan: scansType | null; checkedIn: boolean | null; - scanUser: userType | null; + scanUser: User | null; hasRSVP: boolean | null; } export default function CheckinScanner({ - // event, hasScanned, checkedIn, scanUser, @@ -113,7 +108,7 @@ export default function CheckinScanner({ }} />
- {/*
+ {/*
diff --git a/apps/web/src/components/admin/scanner/PassScanner.tsx b/apps/web/src/components/admin/scanner/PassScanner.tsx index 0edd89ca..adf5bf63 100644 --- a/apps/web/src/components/admin/scanner/PassScanner.tsx +++ b/apps/web/src/components/admin/scanner/PassScanner.tsx @@ -3,21 +3,19 @@ import { useState, useEffect } from "react"; import { QrScanner } from "@yudiel/react-qr-scanner"; import superjson from "superjson"; -import { getScan, createScan } from "@/actions/admin/scanner-admin-actions"; -import { useAction, useOptimisticAction } from "next-safe-action/hook"; +import { createScan } from "@/actions/admin/scanner-admin-actions"; +import { useAction } from "next-safe-action/hook"; import { type QRDataInterface } from "@/lib/utils/shared/qr"; -import type { scansType, userType, eventType } from "@/lib/utils/shared/types"; +import type { Scan, Event, Hacker } from "db/types"; import c from "config"; import { Drawer, - DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, - DrawerTrigger, } from "@/components/shadcn/ui/drawer"; import { Button } from "@/components/shadcn/ui/button"; import Link from "next/link"; @@ -35,10 +33,10 @@ scan: the scan object that has been scanned. If they have not scanned before sca */ interface PassScannerProps { - event: eventType; + event: Event; hasScanned: boolean; - scan: scansType | null; - scanUser: userType | null; + scan: Scan | null; + scanUser: Hacker | null; } export default function PassScanner({ @@ -60,8 +58,11 @@ export default function PassScanner({ const path = usePathname(); const router = useRouter(); - const register = scanUser?.checkedIn ? "Checked in!" : "Not Checked In"; - const guild = Object.keys(c.groups)[scanUser?.group || 0] ?? "None"; + const register = scanUser?.checkinTimestamp + ? "Checked in!" + : "Not Checked In"; + const guild = + Object.keys(c.groups)[scanUser?.hackerData.group || 0] ?? "None"; const role = scanUser?.role ? scanUser?.role : "Not Found"; function handleScanCreate() { diff --git a/apps/web/src/components/admin/users/ServerSections.tsx b/apps/web/src/components/admin/users/ServerSections.tsx index 0bb58bb0..d8911461 100644 --- a/apps/web/src/components/admin/users/ServerSections.tsx +++ b/apps/web/src/components/admin/users/ServerSections.tsx @@ -1,11 +1,11 @@ import UserInfoSection from "@/components/admin/users/UserInfoSection"; -import type { UserWithAllData } from "@/lib/utils/server/types"; +import type { Hacker } from "db/types"; import { titleCase } from "title-case"; import { Button } from "@/components/shadcn/ui/button"; import Link from "next/link"; import { clerkClient } from "@clerk/nextjs"; -export function PersonalInfo({ user }: { user: UserWithAllData }) { +export function PersonalInfo({ user }: { user: Hacker }) { return (
@@ -13,96 +13,94 @@ export function PersonalInfo({ user }: { user: UserWithAllData }) { - - - - + + + +
); } -export function ProfileInfo({ user }: { user: UserWithAllData }) { +export function ProfileInfo({ user }: { user: Hacker }) { return (
- + - +
- +
); } -export async function AccountInfo({ user }: { user: UserWithAllData }) { - const clerkUser = await clerkClient.users.getUser(user.clerkID); - if (!clerkUser) return null; - - // const signInMethods = clerkUser.externalAccounts.map((account) => - // titleCase(account.provider.split("_").slice(-1)[0]) - // ); - - // if (clerkUser.passwordEnabled) { - // signInMethods.push("Password"); - // } +export async function AccountInfo({ user }: { user: Hacker }) { + const clerkUser = await clerkClient.users + .getUser(user.clerkID) + .catch(() => {}); return (
- - - {/* 1 ? "s" : ""}`} - value={signInMethods.join(", ")} - /> */} + {clerkUser ? ( + <> + + + + ) : ( +
+ Failed to find Clerk authentication data. +
+ )}
); } -export function TeamInfo({ user }: { user: UserWithAllData }) { +export function TeamInfo({ user }: { user: Hacker }) { return (
- - {user.team ? ( + + {user.hackerData.team ? ( <> - - + + ) : null}
- {user.team ? ( - + {user.hackerData.team ? ( + ) : null} diff --git a/apps/web/src/components/admin/users/UserColumns.tsx b/apps/web/src/components/admin/users/UserColumns.tsx index 8a48f888..a65e1ec8 100644 --- a/apps/web/src/components/admin/users/UserColumns.tsx +++ b/apps/web/src/components/admin/users/UserColumns.tsx @@ -3,28 +3,19 @@ import { ColumnDef } from "@tanstack/react-table"; import { z } from "zod"; import { createSelectSchema } from "drizzle-zod"; -import { users, registrationData, profileData } from "db/schema"; +import { userCommonData } from "db/schema"; import Link from "next/link"; import { Button } from "@/components/shadcn/ui/button"; -const userValidator = createSelectSchema(users).merge( - z.object({ - registrationData: createSelectSchema(registrationData), - profileData: createSelectSchema(profileData).merge( - z.object({ - skills: z.array(z.string()), - }), - ), - }), -); +const userValidator = createSelectSchema(userCommonData); export type userValidatorType = Pick< z.infer, | "clerkID" - | "createdAt" + | "signupTime" | "firstName" | "lastName" - | "profileData" + | "hackerTag" | "email" | "role" >; @@ -40,9 +31,9 @@ export const columns: ColumnDef[] = [ header: "Email", }, { - accessorKey: "profileData.hackerTag", + accessorKey: "hackerTag", header: "Hacker Tag", - cell: ({ row }) => `@${row.original.profileData.hackerTag}`, + cell: ({ row }) => `@${row.original.hackerTag}`, }, { accessorKey: "clerkID", @@ -53,12 +44,12 @@ export const columns: ColumnDef[] = [ header: "Role", }, { - accessorKey: "createdAt", + accessorKey: "signupTime", header: "Signup Date", cell: ({ row }) => ( - {new Date(row.original.createdAt).toLocaleDateString() + " "} - {new Date(row.original.createdAt).toLocaleTimeString("en-US", { + {new Date(row.original.signupTime).toLocaleDateString() + " "} + {new Date(row.original.signupTime).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", })} diff --git a/apps/web/src/components/dash/overview/ServerBubbles.tsx b/apps/web/src/components/dash/overview/ServerBubbles.tsx index ec8c7e8a..e01e995b 100644 --- a/apps/web/src/components/dash/overview/ServerBubbles.tsx +++ b/apps/web/src/components/dash/overview/ServerBubbles.tsx @@ -3,13 +3,8 @@ import Link from "next/link"; import c from "config"; import { format } from "date-fns"; import GradientHero from "./GradientHero"; -// import { users } from "db/schema"; import QRCode from "react-qr-code"; -// interface MYInfoProps { -// user: typeof users.$inferSelect; -// } - export function Questions() { return (
diff --git a/apps/web/src/components/dash/shared/ProfileButton.tsx b/apps/web/src/components/dash/shared/ProfileButton.tsx index 5a31ce98..3b47e860 100644 --- a/apps/web/src/components/dash/shared/ProfileButton.tsx +++ b/apps/web/src/components/dash/shared/ProfileButton.tsx @@ -5,7 +5,6 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, - DropdownMenuShortcut, DropdownMenuTrigger, } from "@/components/shadcn/ui/dropdown-menu"; import { @@ -15,21 +14,16 @@ import { } from "@/components/shadcn/ui/avatar"; import { Button } from "@/components/shadcn/ui/button"; import { auth, SignOutButton } from "@clerk/nextjs"; -import { db } from "db"; -import { users } from "db/schema"; -import { eq } from "db/drizzle"; import Link from "next/link"; import { DropdownSwitcher } from "@/components/shared/ThemeSwitcher"; +import { getUser } from "db/functions"; export default async function ProfileButton() { - const clerkUser = await auth(); + const clerkUser = auth(); const { userId } = clerkUser; if (!userId) return null; - const user = await db.query.users.findFirst({ - where: eq(users.clerkID, userId), - with: { profileData: true }, - }); + const user = await getUser(userId); if (!user && !userId) return null; if (!user) { @@ -92,10 +86,7 @@ export default async function ProfileButton() { className="relative h-8 w-8 rounded-full" > - + {user.firstName.charAt(0) + user.lastName.charAt(0)} @@ -124,7 +115,7 @@ export default async function ProfileButton() { Report a Bug - + Settings diff --git a/apps/web/src/components/schedule/EventFull.tsx b/apps/web/src/components/events/admin/EventDetails.tsx similarity index 73% rename from apps/web/src/components/schedule/EventFull.tsx rename to apps/web/src/components/events/admin/EventDetails.tsx index 922fcded..1bbeed20 100644 --- a/apps/web/src/components/schedule/EventFull.tsx +++ b/apps/web/src/components/events/admin/EventDetails.tsx @@ -1,15 +1,15 @@ -import { events } from "db/schema"; -import { InferModel } from "db/drizzle"; import c from "config"; import { Badge } from "@/components/shadcn/ui/badge"; import Balancer from "react-wrap-balancer"; import { formatInTimeZone } from "date-fns-tz"; +import { Event } from "db/types"; +import { headers } from "next/headers"; +import { getClientTimeZone } from "@/lib/utils/client/shared"; +import { VERCEL_IP_TIMEZONE_HEADER_KEY } from "@/lib/constants"; +export default function EventFull({ event }: { event: Event }) { + const userTimeZoneHeaderKey = headers().get(VERCEL_IP_TIMEZONE_HEADER_KEY); -export default function EventFull({ - event, -}: { - event: InferModel; -}) { + const userTimeZone = getClientTimeZone(userTimeZoneHeaderKey); return (

{`${formatInTimeZone( event.startTime, - c.hackathonTimezone, + userTimeZone, "EEEE MMMM do", )}, ${formatInTimeZone( event.startTime, - c.hackathonTimezone, + userTimeZone, "h:mm a", - )} - ${formatInTimeZone(event.endTime, c.hackathonTimezone, "h:mm a")}`}

+ )} - ${formatInTimeZone(event.endTime, userTimeZone, "h:mm a")}`}

diff --git a/apps/web/src/components/admin/events/NewEventForm.tsx b/apps/web/src/components/events/admin/NewEventForm.tsx similarity index 72% rename from apps/web/src/components/admin/events/NewEventForm.tsx rename to apps/web/src/components/events/admin/NewEventForm.tsx index e20a7bed..290477ad 100644 --- a/apps/web/src/components/admin/events/NewEventForm.tsx +++ b/apps/web/src/components/events/admin/NewEventForm.tsx @@ -25,45 +25,40 @@ import { Textarea } from "@/components/shadcn/ui/textarea"; import c from "config"; import { DateTimePicker } from "@/components/shadcn/ui/date-time-picker/date-time-picker"; import { parseAbsolute, getLocalTimeZone } from "@internationalized/date"; -import { newEventValidator } from "@/validators/shared/newEvent"; import { zpostSafe } from "@/lib/utils/client/zfetch"; import { BasicRedirValidator } from "@/validators/shared/basicRedir"; import { useState } from "react"; -import { Shell } from "lucide-react"; +import { LoaderPinwheel } from "lucide-react"; import { useRouter } from "next/navigation"; - -interface NewEventFormProps { - defaultDate: Date; -} - -const formSchema = newEventValidator.merge( - z.object({ - type: z.enum(Object.keys(c.eventTypes) as any), - }), -); +import { ONE_HOUR_IN_MILLISECONDS } from "@/lib/constants"; +import { NewEventFormProps } from "@/lib/types/events"; +import { newEventFormSchema } from "@/validators/event"; +import { ThreeCircles } from "react-loader-spinner"; export default function NewEventForm({ defaultDate }: NewEventFormProps) { const [loading, setLoading] = useState(false); const router = useRouter(); - const form = useForm>({ - resolver: zodResolver(formSchema), + const userLocalTimeZone = getLocalTimeZone(); + + const form = useForm>({ + resolver: zodResolver(newEventFormSchema), defaultValues: { title: "", description: "", type: "" as any, host: "", startTime: defaultDate, - endTime: defaultDate, + endTime: new Date(defaultDate.getTime() + ONE_HOUR_IN_MILLISECONDS), }, }); - async function onSubmit(values: z.infer) { + async function onSubmit(values: z.infer) { setLoading(true); const res = await zpostSafe({ url: "/api/admin/events/create", body: values, superReq: true, - vReq: formSchema, + vReq: newEventFormSchema, vRes: BasicRedirValidator, }); setLoading(false); @@ -91,8 +86,7 @@ export default function NewEventForm({ defaultDate }: NewEventFormProps) { - Generally its best to keep this short and - consise + Keep title short and concise @@ -105,10 +99,7 @@ export default function NewEventForm({ defaultDate }: NewEventFormProps) { Description -