diff --git a/.env.example b/.env.example index 68b512594..7843e4e12 100644 --- a/.env.example +++ b/.env.example @@ -19,8 +19,7 @@ DISCORD_DEV_VERIFY_CHANNEL_ID="52872856278050172988890" DISCORD_PROD_SERVER_ID="528728562780501729853789" DISCORD_PROD_VERIFY_CHANNEL_ID="5287378262780501729853789" DISCORD_SECRET_TOKEN="FHBAHJABDhbHSUeyi7398.S72ljd.HKBVAfug73gifa-74gfwiyB-BHJBDHV07hAJf83" -NODE_ENV="development" -INTERNAL_AUTH_KEY="bwgybgidbsi-4784gyfgs-475hhkdsbfs-bwgybgidbsi-4784gyfgs-475hhkdsbfs-bwgybgidbsi-4784gyfgs-475hhkdsbfs-fbnauib4783-bhfbsjfbs" +SHARED_SECRET="bwgybgidbsi-4784gyfgs-475hhkdsbfs-bwgybgidbsi-4784gyfgs-475hhkdsbfs-bwgybgidbsi-4784gyfgs-475hhkdsbfs-fbnauib4783-bhfbsjfbs" NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_YRGIBHSBIbabffjdhvbuYGI7BK" NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in" NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up" @@ -39,5 +38,5 @@ R2_BUCKET_NAME="your-org-name-userdata" R2_SECRET_ACCESS_KEY="279y45g79tgbjsbfhbsiufhbs89hg487hsiufs" TURSO_AUTH_TOKEN="dhbBDSUGFBSUYGBSIUGRBIGBSIYBS7495w8y97w.bivsb7478wGYIFGDSUGFS48y39975y3gtyugysjgs7ugu.BHFBSYUFBSV476guysfuw78gfuy3buybfshbfYUFSGVBYFIBVEIUSBVJSBANIBA74y597bvs6gfusg&gsyjgbfshgbyIGBYUS" TURSO_DATABASE_URL="libsql:hackkit-orgname.turso.io" -UPSTASH_REDIS_REST_TOKEN="BKFSHBSIY7g9347385gBJFDBSJYFGBSIYFUSIBISUYHGOSHIYtusvufdbsfiubfsibYBG" -UPSTASH_REDIS_REST_URL="https://giant-worm-78102.upstash.io" +# UPSTASH_REDIS_REST_TOKEN="BKFSHBSIY7g9347385gBJFDBSJYFGBSIYFUSIBISUYHGOSHIYtusvufdbsfiubfsibYBG" +# UPSTASH_REDIS_REST_URL="https://giant-worm-78102.upstash.io" diff --git a/apps/bot/.gitignore b/apps/bot/.gitignore new file mode 100644 index 000000000..3e2212924 --- /dev/null +++ b/apps/bot/.gitignore @@ -0,0 +1 @@ +/dist \ No newline at end of file diff --git a/apps/bot/bot.ts b/apps/bot/bot.ts index 8a39735ca..ef08d0e04 100644 --- a/apps/bot/bot.ts +++ b/apps/bot/bot.ts @@ -1,23 +1,12 @@ +import { Client, Collection, GatewayIntentBits } from "discord.js"; import { - Client, - Collection, - Events, - GatewayIntentBits, - EmbedBuilder, - ButtonBuilder, - ButtonStyle, - ActionRowBuilder, -} from "discord.js"; -import { readdirSync } from "node:fs"; -import path from "node:path"; -import { Hono } from "hono"; -import { serve } from "bun"; -import c from "config"; -import { db } from "db"; -import { eq } from "db/drizzle"; -import { discordVerification } from "db/schema"; -import { getHacker } from "db/functions"; -import { nanoid } from "nanoid"; + loadCommands, + loadEvents, + loadInteractions, + loadReceivers, +} from "./utils/loaders"; +import express from "express"; +import sharedSecretMiddleware from "./middleware"; /* DISCORD BOT */ @@ -31,224 +20,24 @@ const client = new Client({ }); client.commands = new Collection(); +(client as any).interactions = new Collection(); -const commandsPath = path.join(__dirname, "commands"); -const commandFiles = readdirSync(commandsPath).filter((file) => - file.endsWith(".ts"), -); -for (const file of commandFiles) { - console.log(`[Loading Command] ${file}`); - const filePath = path.join(commandsPath, file); - const command = require(filePath); - // Set a new item in the Collection with the key as the command name and the value as the exported module - if ("data" in command && "execute" in command) { - client.commands.set(command.data.name, command); - } else { - console.log( - `[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`, - ); - } -} -console.log(`Loaded ${client.commands.size} Commands`); +// Load commands and events and interactions from according folders. +loadCommands(client); +loadEvents(client); +loadInteractions(client); -client.on(Events.InteractionCreate, async (interaction) => { - if (interaction.isChatInputCommand()) { - const command = interaction.client.commands.get( - interaction.commandName, - ); +// Start an Express server for webhooks so other applications can communicate with the bot. +const expressApp = express(); +expressApp.use(express.json()); +expressApp.use(sharedSecretMiddleware); - if (!command) { - console.error( - `No command matching ${interaction.commandName} was found.`, - ); - return; - } +//Load receivers for bot's webhooks +loadReceivers(expressApp, client); - try { - await command.execute(interaction); - } catch (error) { - console.error(error); - if (interaction.replied || interaction.deferred) { - await interaction.followUp({ - content: "There was an error while executing this command!", - ephemeral: true, - }); - } else { - await interaction.reply({ - content: "There was an error while executing this command!", - ephemeral: true, - }); - } - } - } else if (interaction.isButton()) { - if (interaction.customId === "verify") { - console.log("Button Pressed"); - const user = interaction.member?.user; - if (!user) { - interaction.reply({ - content: "There was an error while executing this command!", - ephemeral: true, - }); - return; - } - const vCode = nanoid(20); - console.log(interaction.guildId); - const verification = await db - .insert(discordVerification) - .values({ - code: vCode, - discordName: user.username, - discordProfilePhoto: user.avatar || "", - discordUserID: user.id as string, - discordUserTag: user.discriminator as string, - status: "pending", - guild: interaction.guildId as string, - }) - .returning(); - - interaction.reply({ - content: `Please click [this link](${c.siteUrl}/discord-verify?code=${vCode}) to verify your registration!`, - ephemeral: true, - }); - } - } +const RECEIVERS_PORT = process.env.PORT ? parseInt(process.env.PORT) : 4000; +expressApp.listen(RECEIVERS_PORT, () => { + console.log(`Bot receivers listening on port ${RECEIVERS_PORT}`); }); client.login(process.env.DISCORD_SECRET_TOKEN); - -/* WEB SERVER */ - -const app = new Hono(); -app.get("/postMsgToServer", (h) => { - const internalAuthKey = h.req.query("access"); - const serverType: string | undefined | "dev" | "prod" = h.req.query("env"); - if (!internalAuthKey || internalAuthKey != process.env.INTERNAL_AUTH_KEY) { - return h.text("access denied"); - } - if (!serverType || (serverType !== "dev" && serverType !== "prod")) { - return h.text("invalid env"); - } - - const verifyBtn = new ButtonBuilder() - .setCustomId("verify") - .setLabel("Verify") - .setStyle(ButtonStyle.Primary); - - const verifyEmbed = new EmbedBuilder() - .setColor(0x0099ff) - .setTitle("Verification") - .setURL(c.siteUrl) - .setAuthor({ - name: c.botName, - iconURL: c.siteUrl + c.icon.md, - url: c.siteUrl, - }) - .setDescription( - `**Verify your registration for ${c.hackathonName} ${c.itteration} to gain access to the rest of the server!**\n\nClick the "verify" button below to begin the verification process.\n\u200B`, - ) - .setThumbnail(`${c.siteUrl}${c.icon.md}`) - .setFooter({ - text: "Questions or issues? Contact an organizer :)", - iconURL: "https://static.acmutsa.org/Info_Simple.svg.png", - }); - - const channel = client.channels.cache.get( - serverType === "dev" - ? (process.env.DISCORD_DEV_VERIFY_CHANNEL_ID as string) - : (process.env.DISCORD_PROD_VERIFY_CHANNEL_ID as string), - ); - - if (!channel || !channel.isTextBased()) { - return h.text("Invalid channel"); - } - - const row = new ActionRowBuilder().addComponents(verifyBtn); - - channel.send({ - embeds: [verifyEmbed], - components: [row], - }); - return h.text(`Posted to channel!`); -}); - -app.get("/health", (h) => { - return h.text("ok"); -}); - -app.post("/api/checkDiscordVerification", async (h) => { - const body = await h.req.json(); - const internalAuthKey = h.req.query("access"); - if (!internalAuthKey || internalAuthKey != process.env.INTERNAL_AUTH_KEY) { - console.log("denied access"); - return h.text("access denied"); - } - - if (body.code === undefined || typeof body.code !== "string") { - console.log("failed cause of malformed body"); - return h.json({ success: false }); - } - - console.log("got here 1"); - const verification = await db.query.discordVerification.findFirst({ - where: eq(discordVerification.code, body.code), - }); - console.log("got here 1 with verification ", verification); - - if (!verification || !verification.clerkID) { - console.log("failed cause of no verification or missing clerkID"); - return h.json({ success: false }); - } - console.log("got here 2"); - - const user = await getHacker(verification.clerkID); - console.log("got here 2 with user", user); - if (!user) { - console.log("failed cause of no user in db"); - return h.json({ success: false }); - } - - const { discordRole: userGroupRoleName } = ( - c.groups as Record - )[Object.keys(c.groups)[user.hackerData.group]]; - - const guild = client.guilds.cache.get(verification.guild); - if (!guild) { - console.log("failed cause of no guild on intereaction"); - return h.json({ success: false }); - } - - const role = guild.roles.cache.find( - (role) => role.name === c.botParticipantRole, - ); - const userGroupRole = guild.roles.cache.find( - (role) => role.name === userGroupRoleName, - ); - - if (!role || !userGroupRole) { - console.log( - "failed cause could not find a role, was looking for group " + - user.hackerData.group + - " called " + - userGroupRoleName, - ); - return h.json({ success: false }); - } - - const member = guild.members.cache.get(verification.discordUserID); - - if (!member) { - console.log("failed cause could not find member"); - return h.json({ success: false }); - } - - await member.roles.add(role); - await member.roles.add(userGroupRole); - await member.setNickname(user.firstName + " " + user.lastName); - - return h.json({ success: true }); -}); - -serve({ - fetch: app.fetch, - port: process.env.PORT ? parseInt(process.env.PORT) : 4000, -}); diff --git a/apps/bot/commands/postVerify.ts b/apps/bot/commands/postVerify.ts new file mode 100644 index 000000000..36041f349 --- /dev/null +++ b/apps/bot/commands/postVerify.ts @@ -0,0 +1,86 @@ +import { + SlashCommandBuilder, + ChatInputCommandInteraction, + EmbedBuilder, + ButtonBuilder, + ButtonStyle, + ActionRowBuilder, + PermissionsBitField, +} from "discord.js"; +import c from "config"; + +export const data = new SlashCommandBuilder() + .setName("post-verify") + .setDescription("Post the verification embed to this channel (admin only)"); + +export const execute = async (interaction: ChatInputCommandInteraction) => { + // Ensure command is used in a guild + if (!interaction.inGuild() || !interaction.guild) { + await interaction.reply({ + content: "This command can only be used in a server.", + ephemeral: true, + }); + return; + } + + // Check for administrator permission + const hasAdmin = interaction.memberPermissions?.has( + PermissionsBitField.Flags.Administrator, + ); + if (!hasAdmin) { + await interaction.reply({ + content: + "You must have Administrator permissions to run this command.", + ephemeral: true, + }); + return; + } + + const channel = interaction.channel; + if (!channel || !channel.isTextBased()) { + await interaction.reply({ + content: "This channel cannot receive messages from the bot.", + ephemeral: true, + }); + return; + } + + const verifyBtn = new ButtonBuilder() + .setCustomId("verify") + .setLabel("Verify") + .setStyle(ButtonStyle.Primary); + + const verifyEmbed = new EmbedBuilder() + .setColor(0x0099ff) + .setTitle("Verification") + .setURL(c.siteUrl) + .setAuthor({ + name: c.botName, + iconURL: c.siteUrl + c.icon.md, + url: c.siteUrl, + }) + .setDescription( + `**Verify your registration for ${c.hackathonName} ${c.itteration} to gain access to the rest of the server!**\n\nClick the "verify" button below to begin the verification process.\n\u200B`, + ) + .setThumbnail(`${c.siteUrl}${c.icon.md}`) + .setFooter({ + text: "Questions or issues? Contact an organizer :)", + iconURL: "https://static.acmutsa.org/Info_Simple.svg.png", + }); + + const row = new ActionRowBuilder().addComponents(verifyBtn); + + try { + await channel.send({ embeds: [verifyEmbed], components: [row] }); + await interaction.reply({ + content: "Posted verification message.", + ephemeral: true, + }); + } catch (err) { + console.error("Failed to post verification message:", err); + await interaction.reply({ + content: "Failed to post verification message.", + ephemeral: true, + }); + } +}; diff --git a/apps/bot/deploy-commands.ts b/apps/bot/deploy-commands.ts deleted file mode 100644 index 32d67e8ad..000000000 --- a/apps/bot/deploy-commands.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { REST, Routes } from "discord.js"; -import fs from "node:fs"; -import path from "node:path"; - -const args = process.execArgv; - -if (!args.includes("--prod") && !args.includes("--dev")) { - console.error( - "You must specify either --prod or --dev when deploying commands.", - ); - process.exit(1); -} - -const runType: "dev" | "prod" = args.includes("--prod") ? "prod" : "dev"; - -const commands = []; -// Grab all the command folders from the commands directory you created earlier -const commandsPath = path.join(__dirname, "commands"); -const commandFiles = fs - .readdirSync(commandsPath) - .filter((file) => file.endsWith(".ts")); -// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment -for (const file of commandFiles) { - const filePath = path.join(commandsPath, file); - const command = require(filePath); - if ("data" in command && "execute" in command) { - commands.push(command.data.toJSON()); - } else { - console.log( - `[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`, - ); - } -} - -// Construct and prepare an instance of the REST module -const rest = new REST().setToken(process.env.DISCORD_SECRET_TOKEN as string); - -// and deploy your commands! -(async () => { - try { - console.log( - `Started refreshing ${commands.length} application (/) commands.`, - ); - - // The put method is used to fully refresh all commands in the guild with the current set - if (runType === "dev") { - const data = await rest.put( - Routes.applicationGuildCommands( - process.env.DISCORD_CLIENT_ID as string, - process.env.DISCORD_DEV_SERVER_ID as string, - ), - { - body: commands, - }, - ); - console.log( - `Successfully reloaded ${(data as any).length} application (/) commands.`, - ); - } else { - const data = await rest.put( - Routes.applicationGuildCommands( - process.env.DISCORD_CLIENT_ID as string, - process.env.DISCORD_PROD_SERVER_ID as string, - ), - { - body: commands, - }, - ); - await rest.put( - Routes.applicationCommands( - process.env.DISCORD_CLIENT_ID as string, - ), - { - body: commands, - }, - ); - console.log( - `Successfully reloaded ${(data as any).length} application (/) commands.`, - ); - } - } catch (error) { - // And of course, make sure you catch and log any errors! - console.error(error); - } -})(); diff --git a/apps/bot/events/interactionCreate.ts b/apps/bot/events/interactionCreate.ts new file mode 100644 index 000000000..48e0ac442 --- /dev/null +++ b/apps/bot/events/interactionCreate.ts @@ -0,0 +1,59 @@ +export const name = "interactionCreate"; +export const once = false; + +export async function execute(interaction: any) { + try { + if (interaction.isChatInputCommand()) { + const command = interaction.client.commands.get( + interaction.commandName, + ); + + if (!command) { + console.error( + `No command matching ${interaction.commandName} was found.`, + ); + return; + } + + try { + await command.execute(interaction); + } catch (error) { + console.error(error); + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ + content: + "There was an error while executing this command!", + ephemeral: true, + }); + } else { + await interaction.reply({ + content: + "There was an error while executing this command!", + ephemeral: true, + }); + } + } + } else if (interaction.isButton()) { + // Dispatch button interactions to loaded interaction handlers keyed by customId + const handler = interaction.client.interactions?.get( + interaction.customId, + ); + if (!handler) { + console.warn( + `No interaction handler for id ${interaction.customId}`, + ); + return; + } + try { + await handler.execute(interaction); + } catch (err) { + console.error( + `Error executing interaction handler ${interaction.customId}:`, + err, + ); + } + } + } catch (err) { + console.error("Error in interaction handler:", err); + } +} diff --git a/apps/bot/events/ready.ts b/apps/bot/events/ready.ts new file mode 100644 index 000000000..9d3ee8055 --- /dev/null +++ b/apps/bot/events/ready.ts @@ -0,0 +1,57 @@ +import { db } from "db"; +import { discordVerification } from "db/schema"; +import { REST, Routes } from "discord.js"; + +export const name = "ready"; +export const once = true; +export async function execute(client: any) { + try { + console.log(`Ready! Logged in as ${client.user?.tag}`); + + await db.delete(discordVerification).all(); + console.log("Cleared discord verification entries from database."); + + const token: string | undefined = process.env.DISCORD_SECRET_TOKEN; + const clientId: string | undefined = process.env.DISCORD_CLIENT_ID; + if (!token || !clientId) { + console.warn( + "Missing DISCORD_SECRET_TOKEN or DISCORD_CLIENT_ID; skipping command registration.", + ); + return; + } + + // Only deploy when the process is started with --deploy + const shouldDeploy = process.argv.includes("--deploy"); + if (!shouldDeploy) { + console.warn( + "Command deployment skipped; run the bot with --deploy to deploy application commands.", + ); + return; + } + + const commands: any[] = []; + client.commands?.forEach((cmd: any) => { + if (cmd?.data && typeof cmd.data.toJSON === "function") { + commands.push(cmd.data.toJSON()); + } + }); + + const rest = new REST({ version: "10" }).setToken(token); + + await rest.put(Routes.applicationCommands(clientId), { body: [] }); + console.log("Deleted existing application commands."); + + if (commands.length === 0) { + console.warn("No commands to register after deletion."); + return; + } + + // Register globally; propagate changes may take up to 1 hour. + await rest.put(Routes.applicationCommands(clientId), { + body: commands, + }); + console.log(`Registered ${commands.length} application commands.`); + } catch (err) { + console.error("Error in ready handler:", err); + } +} diff --git a/apps/bot/interactions/verify.ts b/apps/bot/interactions/verify.ts new file mode 100644 index 000000000..861c23af9 --- /dev/null +++ b/apps/bot/interactions/verify.ts @@ -0,0 +1,53 @@ +import c from "config"; +import { db } from "db"; +import { discordVerification } from "db/schema"; +import { nanoid } from "nanoid"; + +export const id = "verify"; + +export async function execute(interaction: any) { + try { + console.log("Verify interaction triggered"); + const user = interaction.member?.user; + if (!user) { + await interaction.reply({ + content: "There was an error while executing this interaction!", + ephemeral: true, + }); + return; + } + + const vCode = nanoid(20); + const verification = await db + .insert(discordVerification) + .values({ + code: vCode, + discordName: user.username, + discordProfilePhoto: user.avatar || "", + discordUserID: user.id as string, + discordUserTag: user.discriminator as string, + status: "pending", + guild: interaction.guildId as string, + }) + .returning(); + + await interaction.reply({ + content: `Please click [this link](${c.siteUrl}/discord-verify?code=${vCode}) to verify your registration!`, + ephemeral: true, + }); + } catch (err) { + console.error("Error in verify interaction:", err); + try { + if (!interaction.replied && !interaction.deferred) { + await interaction.reply({ + content: "Internal error", + ephemeral: true, + }); + } + } catch (e) { + console.error("Failed to reply after verify error:", e); + } + } +} + +export default { id, execute }; diff --git a/apps/bot/middleware.ts b/apps/bot/middleware.ts new file mode 100644 index 000000000..c1146cbe5 --- /dev/null +++ b/apps/bot/middleware.ts @@ -0,0 +1,27 @@ +import { Request, Response, NextFunction } from "express"; + +export function sharedSecretMiddleware( + req: Request, + res: Response, + next: NextFunction, +) { + const expected = process.env.SHARED_SECRET; + if (!expected) { + console.error("SHARED_SECRET not configured"); + process.exit(1); + return next(); + } + + const provided = (req.header("X-Shared-Secret") as string) || ""; + if (!provided || provided !== expected) { + res.status(401).json({ + success: false, + error: "invalid_shared_secret", + }); + return; + } + + return next(); +} + +export default sharedSecretMiddleware; diff --git a/apps/bot/package.json b/apps/bot/package.json index 91291941d..dfc59be5e 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -4,10 +4,9 @@ "description": "", "main": "bot.ts", "scripts": { - "dev": "bun run --env-file=../../.env --hot bot.ts", - "start": "bun run --env-file=../../.env ./bot.ts", - "deploy:dev": "bun run --env-file=../../.env --dev deploy-commands.ts", - "deploy:prod": "bun run --env-file=../../.env --prod deploy-commands.ts" + "dev": "tsx watch --env-file=../../.env bot.ts", + "build": "tsc -p tsconfig.json", + "start": "tsx --env-file=../../.env bot.ts" }, "keywords": [], "author": "", @@ -16,12 +15,15 @@ "config": "workspace:*", "db": "workspace:*", "discord.js": "^14.15.3", - "hono": "^4.5.0", + "express": "^5.2.1", "nanoid": "^5.0.7", "zod": "^3.25.67" }, "devDependencies": { - "@types/bun": "^1.1.6" + "@types/express": "^5.0.6", + "@types/node": "20.14.11", + "tsx": "^4.0.0", + "typescript": "5.5.3" }, "packageManager": "pnpm@8.3.1" } diff --git a/apps/bot/receivers/discord_verification.ts b/apps/bot/receivers/discord_verification.ts new file mode 100644 index 000000000..c8a9ba556 --- /dev/null +++ b/apps/bot/receivers/discord_verification.ts @@ -0,0 +1,86 @@ +import { Request, Response } from "express"; +import { db } from "db"; +import { eq } from "db/drizzle"; +import { discordVerification } from "db/schema"; +import { getHacker } from "db/functions"; +import c from "config"; +import { RequestWithClient } from "../utils/loaders"; + +export async function handler(req: RequestWithClient, res: Response) { + const client = req.client; + try { + const body = req.body; + if (!body || typeof body.code !== "string") { + return res + .status(400) + .json({ success: false, error: "missing_code" }); + } + + const verification = await db.query.discordVerification.findFirst({ + where: eq(discordVerification.code, body.code), + }); + + if (!verification || !verification.clerkID) { + console.log("failed cause of no verification or missing clerkID"); + return res.json({ success: false }); + } + + const user = await getHacker(verification.clerkID); + if (!user) { + console.log("failed cause of no user in db"); + return res.json({ success: false }); + } + + const userGroupRoleName = ( + c.groups as Record + )[Object.keys(c.groups)[user.hackerData.group]].discordRole; + + console.log(userGroupRoleName); + + if (!client) { + console.error( + "No client mounted on request in discordVerification handler", + ); + return res.status(500).json({ success: false }); + } + + const guild = client.guilds.cache.get(verification.guild); + if (!guild) { + console.log("failed cause of no guild on interaction"); + return res.json({ success: false }); + } + + const role = guild.roles.cache.find( + (r: any) => r.name === c.botParticipantRole, + ); + const userGroupRole = guild.roles.cache.find( + (r: any) => r.name === userGroupRoleName, + ); + + if (!role || !userGroupRole) { + console.log( + "failed cause could not find a role", + role, + userGroupRole, + ); + return res.json({ success: false }); + } + + const member = guild.members.cache.get(verification.discordUserID); + if (!member) { + console.log("failed cause could not find member"); + return res.json({ success: false }); + } + + await member.roles.add(role); + await member.roles.add(userGroupRole); + await member.setNickname(user.firstName + " " + user.lastName); + + return res.json({ success: true }); + } catch (err) { + console.error("Error in discord-verification receiver:", err); + return res.status(500).json({ success: false }); + } +} + +export default { handler }; diff --git a/apps/bot/tsconfig.json b/apps/bot/tsconfig.json index 90c53c403..261496d6f 100644 --- a/apps/bot/tsconfig.json +++ b/apps/bot/tsconfig.json @@ -1,14 +1,16 @@ { "compilerOptions": { - "typeRoots": ["src/@types", "node_modules/@types"], + "typeRoots": ["./@types", "node_modules/@types"], "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "moduleResolution": "node", "preserveWatchOutput": true, "skipLibCheck": true, - "noEmit": true, - "strict": true - }, - "exclude": ["node_modules"] + "strict": true, + "outDir": "dist", + "noEmit": false, + "target": "ES2020", + "module": "CommonJS" + } } diff --git a/apps/bot/utils/loaders.ts b/apps/bot/utils/loaders.ts new file mode 100644 index 000000000..77e7499dd --- /dev/null +++ b/apps/bot/utils/loaders.ts @@ -0,0 +1,154 @@ +import { ne } from "db"; +import { NextFunction, Request, Response } from "express"; +import fs from "node:fs"; +import path from "node:path"; + +export function loadCommands(client: any) { + const commandsPath = path.join(__dirname, "../commands"); + if (!fs.existsSync(commandsPath)) { + console.log("No commands folder found, aborting.."); + process.exit(1); + return; + } + const commandFiles = fs + .readdirSync(commandsPath) + .filter((file) => file.endsWith(".ts") || file.endsWith(".js")); + for (const file of commandFiles) { + try { + console.log(`[Loading Command] ${file}`); + const filePath = path.join(commandsPath, file); + const command = require(filePath); + if (command && "data" in command && "execute" in command) { + client.commands.set(command.data.name, command); + } else { + console.warn( + `The command at ${filePath} is missing a required "data" or "execute" property.`, + ); + } + } catch (err) { + console.error(`Failed loading command ${file}:`, err); + } + } + console.log(`Loaded ${client.commands?.size ?? 0} Commands`); +} + +export function loadEvents(client: any) { + const eventsPath = path.join(__dirname, "../events"); + if (!fs.existsSync(eventsPath)) { + console.log("No events folder found, aborting.."); + process.exit(1); + return; + } + const eventFiles = fs + .readdirSync(eventsPath) + .filter((file) => file.endsWith(".ts") || file.endsWith(".js")); + for (const file of eventFiles) { + try { + const filePath = path.join(eventsPath, file); + const event = require(filePath); + if (!event || !event.name || !event.execute) { + console.warn( + `Event at ${filePath} is missing name or execute.`, + ); + continue; + } + if (event.once) { + client.once(event.name, (...args: any[]) => + event.execute(...args), + ); + } else { + client.on(event.name, (...args: any[]) => + event.execute(...args), + ); + } + console.log(`[Loaded Event] ${event.name}`); + } catch (err) { + console.error(`Failed loading event ${file}:`, err); + } + } +} + +export function loadInteractions(client: any) { + const interactionsPath = path.join(__dirname, "../interactions"); + if (!fs.existsSync(interactionsPath)) { + console.log("No interactions folder found, aborting.."); + process.exit(1); + return; + } + const interactionFiles = fs + .readdirSync(interactionsPath) + .filter((file) => file.endsWith(".ts") || file.endsWith(".js")); + for (const file of interactionFiles) { + try { + console.log(`[Loading Interaction] ${file}`); + const filePath = path.join(interactionsPath, file); + const interaction = require(filePath); + // interaction should export `id` and `execute` or `customId` and `execute` + const key = + interaction.id || interaction.customId || interaction.name; + if (!key || !interaction.execute) { + console.warn( + `Interaction at ${filePath} is missing id/customId/name or execute.`, + ); + continue; + } + client.interactions.set(key, interaction); + } catch (err) { + console.error(`Failed loading interaction ${file}:`, err); + } + } + console.log(`Loaded ${client.interactions?.size ?? 0} Interactions`); +} + +export default { loadCommands, loadEvents, loadInteractions }; + +export type RequestWithClient = Request & { client?: any }; + +export function loadReceivers(app: any, client: any) { + const receiversPath = path.join(__dirname, "../receivers"); + if (!fs.existsSync(receiversPath)) { + console.log("No receivers folder found, aborting.."); + process.exit(1); + return; + } + const receiverFiles = fs + .readdirSync(receiversPath) + .filter((file) => file.endsWith(".ts") || file.endsWith(".js")); + for (const file of receiverFiles) { + try { + const filePath = path.join(receiversPath, file); + const receiver = require(filePath); + + const base = path.basename(file, path.extname(file)); + const kebab = base + .replace(/([a-z])([A-Z])/g, "$1-$2") + .replace(/_/g, "-") + .toLowerCase(); + const route = `/${kebab}`; + + console.log(`[Loading Receiver] ${route}`); + + if (receiver && "handler" in receiver) { + app.post( + route, + ( + req: RequestWithClient, + res: Response, + next: NextFunction, + ) => { + req.client = client; + next(); + }, + receiver.handler, + ); + } else { + console.warn( + `The receiver at ${filePath} is missing a required "handler" property.`, + ); + } + } catch (err) { + console.error(`Failed loading receiver ${file}:`, err); + } + } + console.log(`Loaded receivers: ${receiverFiles.length}`); +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 867448ea4..53f894294 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -36,4 +36,6 @@ next-env.d.ts # react-email .react-email/ -certificates \ No newline at end of file +certificates +.open-next/ +.wrangler/ \ No newline at end of file diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index f98177b11..4f154cd7a 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -21,6 +21,11 @@ const nextConfig = { ], }, experimental: { + outputFileTracingIncludes: { + "/*": [ + "../../node_modules/.pnpm/@libsql+isomorphic-ws@0.1.5/node_modules/@libsql/isomorphic-ws/**/*", + ], + }, serverActions: { allowedOrigins: ["localhost:3000"], }, diff --git a/apps/web/open-next.config.ts b/apps/web/open-next.config.ts new file mode 100644 index 000000000..ffd988785 --- /dev/null +++ b/apps/web/open-next.config.ts @@ -0,0 +1,3 @@ +import { defineCloudflareConfig } from "@opennextjs/cloudflare"; + +export default defineCloudflareConfig(); diff --git a/apps/web/package.json b/apps/web/package.json index 55eed01f9..9a6529e8f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,6 +7,10 @@ "build": "pnpm with-env next build", "start": "pnpm with-env next start", "lint": "next lint", + "preview": "opennextjs-cloudflare build --dangerouslyUseUnsupportedNextVersion && opennextjs-cloudflare preview", + "deploy": "opennextjs-cloudflare build --dangerouslyUseUnsupportedNextVersion && opennextjs-cloudflare deploy", + "upload": "opennextjs-cloudflare build --dangerouslyUseUnsupportedNextVersion && opennextjs-cloudflare upload", + "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts", "with-env": "dotenv -e ../../.env --" }, "dependencies": { @@ -15,6 +19,7 @@ "@clerk/nextjs": "^6.12.12", "@hookform/resolvers": "^3.9.0", "@internationalized/date": "^3.5.4", + "@opennextjs/cloudflare": "^1.20.0", "@planetscale/database": "^1.18.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.1.1", @@ -47,6 +52,7 @@ "embla-carousel": "8.1.7", "embla-carousel-react": "8.1.7", "lucide-react": "^0.411.0", + "motion": "^12.38.0", "nanoid": "^5.0.7", "next": "14.2.35", "next-safe-action": "^7.9.3", @@ -83,6 +89,7 @@ "postcss": "8.4.39", "tailwindcss": "3.4.6", "tailwindcss-animate": "1.0.7", - "typescript": "5.5.3" + "typescript": "5.5.3", + "wrangler": "^4.105.0" } -} +} \ No newline at end of file diff --git a/apps/web/public/img/assets/about/about.webp b/apps/web/public/img/assets/about/about.webp new file mode 100644 index 000000000..19b91989e Binary files /dev/null and b/apps/web/public/img/assets/about/about.webp differ diff --git a/apps/web/public/img/assets/about/about1.webp b/apps/web/public/img/assets/about/about1.webp new file mode 100644 index 000000000..0afca06b5 Binary files /dev/null and b/apps/web/public/img/assets/about/about1.webp differ diff --git a/apps/web/public/img/assets/about/about2.webp b/apps/web/public/img/assets/about/about2.webp new file mode 100644 index 000000000..a55928f2b Binary files /dev/null and b/apps/web/public/img/assets/about/about2.webp differ diff --git a/apps/web/public/img/assets/about/about_photos.webp b/apps/web/public/img/assets/about/about_photos.webp new file mode 100644 index 000000000..b10703b25 Binary files /dev/null and b/apps/web/public/img/assets/about/about_photos.webp differ diff --git a/apps/web/public/img/assets/about/confidential.svg b/apps/web/public/img/assets/about/confidential.svg new file mode 100644 index 000000000..3be79477c --- /dev/null +++ b/apps/web/public/img/assets/about/confidential.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/img/assets/background.webp b/apps/web/public/img/assets/background.webp new file mode 100644 index 000000000..78588958f Binary files /dev/null and b/apps/web/public/img/assets/background.webp differ diff --git a/apps/web/public/img/assets/buttons/blank-tape-stickers1.webp b/apps/web/public/img/assets/buttons/blank-tape-stickers1.webp new file mode 100644 index 000000000..a216030dd Binary files /dev/null and b/apps/web/public/img/assets/buttons/blank-tape-stickers1.webp differ diff --git a/apps/web/public/img/assets/buttons/blank-tape-stickers2.webp b/apps/web/public/img/assets/buttons/blank-tape-stickers2.webp new file mode 100644 index 000000000..69ebe0066 Binary files /dev/null and b/apps/web/public/img/assets/buttons/blank-tape-stickers2.webp differ diff --git a/apps/web/public/img/assets/dash/have-questions-background.webp b/apps/web/public/img/assets/dash/have-questions-background.webp new file mode 100644 index 000000000..65954cb1d Binary files /dev/null and b/apps/web/public/img/assets/dash/have-questions-background.webp differ diff --git a/apps/web/public/img/assets/dash/lable-vertical.webp b/apps/web/public/img/assets/dash/lable-vertical.webp new file mode 100644 index 000000000..c2d02e6b7 Binary files /dev/null and b/apps/web/public/img/assets/dash/lable-vertical.webp differ diff --git a/apps/web/public/img/assets/dash/lable.webp b/apps/web/public/img/assets/dash/lable.webp new file mode 100644 index 000000000..1d2608a15 Binary files /dev/null and b/apps/web/public/img/assets/dash/lable.webp differ diff --git a/apps/web/public/img/assets/dash/lable1.webp b/apps/web/public/img/assets/dash/lable1.webp new file mode 100644 index 000000000..b347c78a3 Binary files /dev/null and b/apps/web/public/img/assets/dash/lable1.webp differ diff --git a/apps/web/public/img/assets/dash/lable2.webp b/apps/web/public/img/assets/dash/lable2.webp new file mode 100644 index 000000000..e1e84660a Binary files /dev/null and b/apps/web/public/img/assets/dash/lable2.webp differ diff --git a/apps/web/public/img/assets/dash/time-background.webp b/apps/web/public/img/assets/dash/time-background.webp new file mode 100644 index 000000000..299aeeeb2 Binary files /dev/null and b/apps/web/public/img/assets/dash/time-background.webp differ diff --git a/apps/web/public/img/assets/faq/FAQ.svg b/apps/web/public/img/assets/faq/FAQ.svg new file mode 100644 index 000000000..d81364c37 --- /dev/null +++ b/apps/web/public/img/assets/faq/FAQ.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/img/assets/faq/classified.svg b/apps/web/public/img/assets/faq/classified.svg new file mode 100644 index 000000000..3c28ea320 --- /dev/null +++ b/apps/web/public/img/assets/faq/classified.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/img/assets/faq/finger-print.svg b/apps/web/public/img/assets/faq/finger-print.svg new file mode 100644 index 000000000..0621837d8 --- /dev/null +++ b/apps/web/public/img/assets/faq/finger-print.svg @@ -0,0 +1,3233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/img/assets/faq/marker-circle4.svg b/apps/web/public/img/assets/faq/marker-circle4.svg new file mode 100644 index 000000000..a7ffd681d --- /dev/null +++ b/apps/web/public/img/assets/faq/marker-circle4.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/apps/web/public/img/assets/footer/discord_icon.svg b/apps/web/public/img/assets/footer/discord_icon.svg new file mode 100644 index 000000000..7f9a31f02 --- /dev/null +++ b/apps/web/public/img/assets/footer/discord_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/img/assets/footer/rh-city-logo-black.svg b/apps/web/public/img/assets/footer/rh-city-logo-black.svg new file mode 100644 index 000000000..e19810763 --- /dev/null +++ b/apps/web/public/img/assets/footer/rh-city-logo-black.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/web/public/img/assets/hero/hero.webp b/apps/web/public/img/assets/hero/hero.webp new file mode 100644 index 000000000..233f3bf79 Binary files /dev/null and b/apps/web/public/img/assets/hero/hero.webp differ diff --git a/apps/web/public/img/assets/hero/logo-background.webp b/apps/web/public/img/assets/hero/logo-background.webp new file mode 100644 index 000000000..8c5252af0 Binary files /dev/null and b/apps/web/public/img/assets/hero/logo-background.webp differ diff --git a/apps/web/public/img/assets/hero/marker-circle3.svg b/apps/web/public/img/assets/hero/marker-circle3.svg new file mode 100644 index 000000000..42ee55847 --- /dev/null +++ b/apps/web/public/img/assets/hero/marker-circle3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/img/assets/hero/register/blank-tape-stickers3.webp b/apps/web/public/img/assets/hero/register/blank-tape-stickers3.webp new file mode 100644 index 000000000..1fc26a561 Binary files /dev/null and b/apps/web/public/img/assets/hero/register/blank-tape-stickers3.webp differ diff --git a/apps/web/public/img/assets/hero/rh-logo.svg b/apps/web/public/img/assets/hero/rh-logo.svg new file mode 100644 index 000000000..e19810763 --- /dev/null +++ b/apps/web/public/img/assets/hero/rh-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/web/public/img/assets/hero/top-secret.svg b/apps/web/public/img/assets/hero/top-secret.svg new file mode 100644 index 000000000..9c6497885 --- /dev/null +++ b/apps/web/public/img/assets/hero/top-secret.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/img/assets/logo_stamp.webp b/apps/web/public/img/assets/logo_stamp.webp new file mode 100644 index 000000000..c6216b40f Binary files /dev/null and b/apps/web/public/img/assets/logo_stamp.webp differ diff --git a/apps/web/public/img/assets/map/SP1.webp b/apps/web/public/img/assets/map/SP1.webp new file mode 100644 index 000000000..1256d12e6 Binary files /dev/null and b/apps/web/public/img/assets/map/SP1.webp differ diff --git a/apps/web/public/img/assets/map/main-campus.webp b/apps/web/public/img/assets/map/main-campus.webp new file mode 100644 index 000000000..ede72a3f2 Binary files /dev/null and b/apps/web/public/img/assets/map/main-campus.webp differ diff --git a/apps/web/public/img/assets/map/map-background.webp b/apps/web/public/img/assets/map/map-background.webp new file mode 100644 index 000000000..f21f2d98a Binary files /dev/null and b/apps/web/public/img/assets/map/map-background.webp differ diff --git a/apps/web/public/img/assets/map/pin4.webp b/apps/web/public/img/assets/map/pin4.webp new file mode 100644 index 000000000..6e743e0ca Binary files /dev/null and b/apps/web/public/img/assets/map/pin4.webp differ diff --git a/apps/web/public/img/assets/map/pin5.webp b/apps/web/public/img/assets/map/pin5.webp new file mode 100644 index 000000000..43c3f3988 Binary files /dev/null and b/apps/web/public/img/assets/map/pin5.webp differ diff --git a/apps/web/public/img/assets/map/red-circle1.svg b/apps/web/public/img/assets/map/red-circle1.svg new file mode 100644 index 000000000..2115c8d6c --- /dev/null +++ b/apps/web/public/img/assets/map/red-circle1.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/apps/web/public/img/assets/menu/menu.webp b/apps/web/public/img/assets/menu/menu.webp new file mode 100644 index 000000000..15773359c Binary files /dev/null and b/apps/web/public/img/assets/menu/menu.webp differ diff --git a/apps/web/public/img/assets/menu/pin1.webp b/apps/web/public/img/assets/menu/pin1.webp new file mode 100644 index 000000000..e65003604 Binary files /dev/null and b/apps/web/public/img/assets/menu/pin1.webp differ diff --git a/apps/web/public/img/assets/pass/event-pass.webp b/apps/web/public/img/assets/pass/event-pass.webp new file mode 100644 index 000000000..fc12cd023 Binary files /dev/null and b/apps/web/public/img/assets/pass/event-pass.webp differ diff --git a/apps/web/public/img/assets/profile/agency.png b/apps/web/public/img/assets/profile/agency.png new file mode 100644 index 000000000..04794b2ed Binary files /dev/null and b/apps/web/public/img/assets/profile/agency.png differ diff --git a/apps/web/public/img/assets/profile/finger-prints.png b/apps/web/public/img/assets/profile/finger-prints.png new file mode 100644 index 000000000..805066e22 Binary files /dev/null and b/apps/web/public/img/assets/profile/finger-prints.png differ diff --git a/apps/web/public/img/assets/profile/profile-picture.png b/apps/web/public/img/assets/profile/profile-picture.png new file mode 100644 index 000000000..01f1aaff8 Binary files /dev/null and b/apps/web/public/img/assets/profile/profile-picture.png differ diff --git a/apps/web/public/img/assets/red-thread.svg b/apps/web/public/img/assets/red-thread.svg new file mode 100644 index 000000000..941cb588f --- /dev/null +++ b/apps/web/public/img/assets/red-thread.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/img/assets/silver-pin.svg b/apps/web/public/img/assets/silver-pin.svg new file mode 100644 index 000000000..79e8976d7 --- /dev/null +++ b/apps/web/public/img/assets/silver-pin.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/img/assets/team/Abrar Ahmed.webp b/apps/web/public/img/assets/team/Abrar Ahmed.webp new file mode 100644 index 000000000..5aedaf95a Binary files /dev/null and b/apps/web/public/img/assets/team/Abrar Ahmed.webp differ diff --git a/apps/web/public/img/assets/team/Alekzander Brysch.webp b/apps/web/public/img/assets/team/Alekzander Brysch.webp new file mode 100644 index 000000000..a45580e6c Binary files /dev/null and b/apps/web/public/img/assets/team/Alekzander Brysch.webp differ diff --git a/apps/web/public/img/assets/team/Anam Sultana.webp b/apps/web/public/img/assets/team/Anam Sultana.webp new file mode 100644 index 000000000..1e6c49515 Binary files /dev/null and b/apps/web/public/img/assets/team/Anam Sultana.webp differ diff --git a/apps/web/public/img/assets/team/Anh Doan.webp b/apps/web/public/img/assets/team/Anh Doan.webp new file mode 100644 index 000000000..5bf2170b9 Binary files /dev/null and b/apps/web/public/img/assets/team/Anh Doan.webp differ diff --git a/apps/web/public/img/assets/team/Ash Hernandez.webp b/apps/web/public/img/assets/team/Ash Hernandez.webp new file mode 100644 index 000000000..c137f02eb Binary files /dev/null and b/apps/web/public/img/assets/team/Ash Hernandez.webp differ diff --git a/apps/web/public/img/assets/team/Blessy Kalluri.webp b/apps/web/public/img/assets/team/Blessy Kalluri.webp new file mode 100644 index 000000000..903164efe Binary files /dev/null and b/apps/web/public/img/assets/team/Blessy Kalluri.webp differ diff --git a/apps/web/public/img/assets/team/Camille Hart.webp b/apps/web/public/img/assets/team/Camille Hart.webp new file mode 100644 index 000000000..9874235c2 Binary files /dev/null and b/apps/web/public/img/assets/team/Camille Hart.webp differ diff --git a/apps/web/public/img/assets/team/Cayden Hutcheson.webp b/apps/web/public/img/assets/team/Cayden Hutcheson.webp new file mode 100644 index 000000000..3f5644f0e Binary files /dev/null and b/apps/web/public/img/assets/team/Cayden Hutcheson.webp differ diff --git a/apps/web/public/img/assets/team/Diego Medina.webp b/apps/web/public/img/assets/team/Diego Medina.webp new file mode 100644 index 000000000..6dded570b Binary files /dev/null and b/apps/web/public/img/assets/team/Diego Medina.webp differ diff --git a/apps/web/public/img/assets/team/Dyshana Torres Rivera.webp b/apps/web/public/img/assets/team/Dyshana Torres Rivera.webp new file mode 100644 index 000000000..6dded570b Binary files /dev/null and b/apps/web/public/img/assets/team/Dyshana Torres Rivera.webp differ diff --git a/apps/web/public/img/assets/team/Elisa Moran.webp b/apps/web/public/img/assets/team/Elisa Moran.webp new file mode 100644 index 000000000..6dded570b Binary files /dev/null and b/apps/web/public/img/assets/team/Elisa Moran.webp differ diff --git a/apps/web/public/img/assets/team/Eric Lee.webp b/apps/web/public/img/assets/team/Eric Lee.webp new file mode 100644 index 000000000..c478514fa Binary files /dev/null and b/apps/web/public/img/assets/team/Eric Lee.webp differ diff --git a/apps/web/public/img/assets/team/Evelynn Donaldson.webp b/apps/web/public/img/assets/team/Evelynn Donaldson.webp new file mode 100644 index 000000000..dd488412b Binary files /dev/null and b/apps/web/public/img/assets/team/Evelynn Donaldson.webp differ diff --git a/apps/web/public/img/assets/team/Francisco Epinoza.webp b/apps/web/public/img/assets/team/Francisco Epinoza.webp new file mode 100644 index 000000000..4c2279de7 Binary files /dev/null and b/apps/web/public/img/assets/team/Francisco Epinoza.webp differ diff --git a/apps/web/public/img/assets/team/Josie Sauceda.webp b/apps/web/public/img/assets/team/Josie Sauceda.webp new file mode 100644 index 000000000..df0cc998b Binary files /dev/null and b/apps/web/public/img/assets/team/Josie Sauceda.webp differ diff --git a/apps/web/public/img/assets/team/Layla Mendiola.webp b/apps/web/public/img/assets/team/Layla Mendiola.webp new file mode 100644 index 000000000..6dded570b Binary files /dev/null and b/apps/web/public/img/assets/team/Layla Mendiola.webp differ diff --git a/apps/web/public/img/assets/team/Martin Llano.webp b/apps/web/public/img/assets/team/Martin Llano.webp new file mode 100644 index 000000000..d19e39645 Binary files /dev/null and b/apps/web/public/img/assets/team/Martin Llano.webp differ diff --git a/apps/web/public/img/assets/team/Maryna Korolova.webp b/apps/web/public/img/assets/team/Maryna Korolova.webp new file mode 100644 index 000000000..9ffab9652 Binary files /dev/null and b/apps/web/public/img/assets/team/Maryna Korolova.webp differ diff --git a/apps/web/public/img/assets/team/Miguel Oseguera.webp b/apps/web/public/img/assets/team/Miguel Oseguera.webp new file mode 100644 index 000000000..9172229f6 Binary files /dev/null and b/apps/web/public/img/assets/team/Miguel Oseguera.webp differ diff --git a/apps/web/public/img/assets/team/Paula Com.webp b/apps/web/public/img/assets/team/Paula Com.webp new file mode 100644 index 000000000..0faf72410 Binary files /dev/null and b/apps/web/public/img/assets/team/Paula Com.webp differ diff --git a/apps/web/public/img/assets/team/Reese Sylvester.webp b/apps/web/public/img/assets/team/Reese Sylvester.webp new file mode 100644 index 000000000..fc0a9785b Binary files /dev/null and b/apps/web/public/img/assets/team/Reese Sylvester.webp differ diff --git a/apps/web/public/img/assets/team/Rufat Niftaliyev.webp b/apps/web/public/img/assets/team/Rufat Niftaliyev.webp new file mode 100644 index 000000000..85bfb80a3 Binary files /dev/null and b/apps/web/public/img/assets/team/Rufat Niftaliyev.webp differ diff --git a/apps/web/public/img/assets/team/Savanah Schaefer.webp b/apps/web/public/img/assets/team/Savanah Schaefer.webp new file mode 100644 index 000000000..73fbd969b Binary files /dev/null and b/apps/web/public/img/assets/team/Savanah Schaefer.webp differ diff --git a/apps/web/public/img/assets/team/Scherly Ramirez.webp b/apps/web/public/img/assets/team/Scherly Ramirez.webp new file mode 100644 index 000000000..6dded570b Binary files /dev/null and b/apps/web/public/img/assets/team/Scherly Ramirez.webp differ diff --git a/apps/web/public/img/assets/team/Shaun Philippe.webp b/apps/web/public/img/assets/team/Shaun Philippe.webp new file mode 100644 index 000000000..a9f5fdfd6 Binary files /dev/null and b/apps/web/public/img/assets/team/Shaun Philippe.webp differ diff --git a/apps/web/public/img/assets/team/Tochi Kalu.webp b/apps/web/public/img/assets/team/Tochi Kalu.webp new file mode 100644 index 000000000..6dded570b Binary files /dev/null and b/apps/web/public/img/assets/team/Tochi Kalu.webp differ diff --git a/apps/web/public/img/assets/team/Tri Nguyen.webp b/apps/web/public/img/assets/team/Tri Nguyen.webp new file mode 100644 index 000000000..0bf8c32c8 Binary files /dev/null and b/apps/web/public/img/assets/team/Tri Nguyen.webp differ diff --git a/apps/web/public/img/assets/team/Victoria Rivas.webp b/apps/web/public/img/assets/team/Victoria Rivas.webp new file mode 100644 index 000000000..6dded570b Binary files /dev/null and b/apps/web/public/img/assets/team/Victoria Rivas.webp differ diff --git a/apps/web/public/img/logo/rh-logo-black.png b/apps/web/public/img/logo/rh-logo-black.png new file mode 100644 index 000000000..61d7a6a3a Binary files /dev/null and b/apps/web/public/img/logo/rh-logo-black.png differ diff --git a/apps/web/public/img/logo/rh-logo-black.svg b/apps/web/public/img/logo/rh-logo-black.svg new file mode 100644 index 000000000..6f130b5e3 --- /dev/null +++ b/apps/web/public/img/logo/rh-logo-black.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/img/partner-logos/HEB.svg b/apps/web/public/img/partner-logos/HEB.svg new file mode 100644 index 000000000..0a43d02b7 --- /dev/null +++ b/apps/web/public/img/partner-logos/HEB.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/web/public/img/powered-by-vercel.svg b/apps/web/public/img/powered-by-vercel.svg deleted file mode 100644 index 877828684..000000000 --- a/apps/web/public/img/powered-by-vercel.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/web/public/img/sponsors/empty-paper.svg b/apps/web/public/img/sponsors/empty-paper.svg new file mode 100644 index 000000000..bd7b012cb --- /dev/null +++ b/apps/web/public/img/sponsors/empty-paper.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/img/sponsors/sponsors-background.png b/apps/web/public/img/sponsors/sponsors-background.png new file mode 100644 index 000000000..72e17df98 Binary files /dev/null and b/apps/web/public/img/sponsors/sponsors-background.png differ diff --git a/apps/web/public/img/sponsors/sponsors-background.svg b/apps/web/public/img/sponsors/sponsors-background.svg new file mode 100644 index 000000000..b1220ae65 --- /dev/null +++ b/apps/web/public/img/sponsors/sponsors-background.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/img/sponsors/sponsors-header-background.svg b/apps/web/public/img/sponsors/sponsors-header-background.svg new file mode 100644 index 000000000..35ccf396d --- /dev/null +++ b/apps/web/public/img/sponsors/sponsors-header-background.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/actions/admin/modify-nav-item.ts b/apps/web/src/actions/admin/modify-nav-item.ts deleted file mode 100644 index 354075255..000000000 --- a/apps/web/src/actions/admin/modify-nav-item.ts +++ /dev/null @@ -1,111 +0,0 @@ -"use server"; - -import { z } from "zod"; -import { adminAction } from "@/lib/safe-action"; -import { redisSAdd, redisHSet, removeNavItem } from "@/lib/utils/server/redis"; -import { revalidatePath } from "next/cache"; - -import { Redis } from "@upstash/redis"; -import { userHasPermission } from "@/lib/utils/server/admin"; -import { PermissionType } from "@/lib/constants/permission"; - -const redis = Redis.fromEnv(); - -const metadataSchema = z.object({ - name: z.string().min(1), - url: z.string().min(1), -}); - -const editMetadataSchema = metadataSchema.extend({ - existingName: z.string().min(1), - enabled: z.boolean(), -}); - -// Maybe a better way to do this for revalidation? Who knows. -const navAdminPage = "/admin/toggles/landing"; - -export const setItem = adminAction - .schema(metadataSchema) - .action(async ({ parsedInput: { name, url }, ctx: { user, userId } }) => { - if (!userHasPermission(user, PermissionType.MANAGE_NAVLINKS)) { - throw new Error( - "You do not have permission to manage navigation links.", - ); - } - - await redisSAdd("config:navitemslist", encodeURIComponent(name)); - await redisHSet(`config:navitems:${encodeURIComponent(name)}`, { - url, - name, - enabled: true, - }); - revalidatePath(navAdminPage); - return { success: true }; - }); - -export const editItem = adminAction - .schema(editMetadataSchema) - .action( - async ({ parsedInput: { name, url, existingName }, ctx: { user } }) => { - if (!userHasPermission(user, PermissionType.MANAGE_NAVLINKS)) { - throw new Error( - "You do not have permission to manage navigation links.", - ); - } - - const pipe = redis.pipeline(); - - if (existingName != name) { - pipe.srem( - "config:navitemslist", - encodeURIComponent(existingName), - ); - } - - pipe.sadd("config:navitemslist", encodeURIComponent(name)); - pipe.hset(`config:navitems:${encodeURIComponent(name)}`, { - url, - name, - enabled: true, - }); - - await pipe.exec(); - - revalidatePath(navAdminPage); - return { success: true }; - }, - ); - -export const removeItem = adminAction - .schema(z.string()) - .action(async ({ parsedInput: name, ctx: { user, userId } }) => { - if (!userHasPermission(user, PermissionType.MANAGE_NAVLINKS)) { - throw new Error( - "You do not have permission to manage navigation links.", - ); - } - await removeNavItem(name); - // await new Promise((resolve) => setTimeout(resolve, 1500)); - revalidatePath(navAdminPage); - return { success: true }; - }); - -export const toggleItem = adminAction - .schema(z.object({ name: z.string(), statusToSet: z.boolean() })) - .action( - async ({ - parsedInput: { name, statusToSet }, - ctx: { user, userId }, - }) => { - if (!userHasPermission(user, PermissionType.MANAGE_NAVLINKS)) { - throw new Error( - "You do not have permission to manage navigation links.", - ); - } - await redisHSet(`config:navitems:${encodeURIComponent(name)}`, { - enabled: statusToSet, - }); - revalidatePath(navAdminPage); - return { success: true, itemStatus: statusToSet }; - }, - ); diff --git a/apps/web/src/actions/admin/registration-actions.ts b/apps/web/src/actions/admin/registration-actions.ts deleted file mode 100644 index e62d70d4f..000000000 --- a/apps/web/src/actions/admin/registration-actions.ts +++ /dev/null @@ -1,75 +0,0 @@ -"use server"; - -import { z } from "zod"; -import { adminAction } from "@/lib/safe-action"; -import { redisSet } from "@/lib/utils/server/redis"; -import { revalidatePath } from "next/cache"; -import { userHasPermission } from "@/lib/utils/server/admin"; -import { PermissionType } from "@/lib/constants/permission"; - -const defaultRegistrationToggleSchema = z.object({ - enabled: z.boolean(), -}); - -const defaultRSVPLimitSchema = z.object({ - rsvpLimit: z.number(), -}); - -export const toggleRegistrationEnabled = adminAction - .schema(defaultRegistrationToggleSchema) - .action(async ({ parsedInput: { enabled }, ctx: { user, userId } }) => { - if (!userHasPermission(user, PermissionType.MANAGE_REGISTRATION)) { - throw new Error( - "You do not have permission to manage registration settings.", - ); - } - - await redisSet("config:registration:registrationEnabled", enabled); - revalidatePath("/admin/toggles/registration"); - return { success: true, statusSet: enabled }; - }); - -export const toggleRegistrationMessageEnabled = adminAction - .schema(defaultRegistrationToggleSchema) - .action(async ({ parsedInput: { enabled }, ctx: { user, userId } }) => { - if (!userHasPermission(user, PermissionType.MANAGE_REGISTRATION)) { - throw new Error( - "You do not have permission to manage registration settings.", - ); - } - - await redisSet( - "config:registration:registrationMessageEnabled", - enabled, - ); - revalidatePath("/admin/toggles/registration"); - return { success: true, statusSet: enabled }; - }); - -export const toggleRSVPs = adminAction - .schema(defaultRegistrationToggleSchema) - .action(async ({ parsedInput: { enabled }, ctx: { user, userId } }) => { - if (!userHasPermission(user, PermissionType.MANAGE_REGISTRATION)) { - throw new Error( - "You do not have permission to manage registration settings.", - ); - } - - await redisSet("config:registration:allowRSVPs", enabled); - revalidatePath("/admin/toggles/registration"); - return { success: true, statusSet: enabled }; - }); - -export const setRSVPLimit = adminAction - .schema(defaultRSVPLimitSchema) - .action(async ({ parsedInput: { rsvpLimit }, ctx: { user, userId } }) => { - if (!userHasPermission(user, PermissionType.MANAGE_REGISTRATION)) { - throw new Error( - "You do not have permission to manage registration settings.", - ); - } - - await redisSet("config:registration:maxRSVPs", rsvpLimit); - revalidatePath("/admin/toggles/registration"); - return { success: true, statusSet: rsvpLimit }; - }); diff --git a/apps/web/src/actions/discord-verify.ts b/apps/web/src/actions/discord-verify.ts index 970cd8c13..02019501c 100644 --- a/apps/web/src/actions/discord-verify.ts +++ b/apps/web/src/actions/discord-verify.ts @@ -26,27 +26,27 @@ export const confirmVerifyDiscord = authenticatedAction }; } - // TODO: set some kind of thing that will revert the verification if the bot api call fails - await db .update(discordVerification) .set({ status: "accepted", clerkID: userId }) .where(eq(discordVerification.code, code)); - const res = await fetch( - env.BOT_API_URL + - "/api/checkDiscordVerification?access=" + - env.INTERNAL_AUTH_KEY, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ code }), + // Call bot receivers endpoint with shared secret header + const res = await fetch(`${env.BOT_API_URL}/discord-verification`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Shared-Secret": env.SHARED_SECRET, }, - ); - let resJson = await res.json(); - console.log(resJson); + body: JSON.stringify({ code }), + }); + let resJson = {}; + try { + resJson = await res.json(); + console.log(resJson); + } catch (e) { + console.warn("discord receiver returned no JSON", e); + } return { success: true, diff --git a/apps/web/src/app/admin/layout.tsx b/apps/web/src/app/admin/layout.tsx index a28aff892..aa7f1306f 100644 --- a/apps/web/src/app/admin/layout.tsx +++ b/apps/web/src/app/admin/layout.tsx @@ -32,7 +32,7 @@ export default async function AdminLayout({ children }: AdminLayoutProps) { return ( <> - +
diff --git a/apps/web/src/app/admin/toggles/dashboard/page.tsx b/apps/web/src/app/admin/toggles/dashboard/page.tsx deleted file mode 100644 index c82873b71..000000000 --- a/apps/web/src/app/admin/toggles/dashboard/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

Coming soon

; -} diff --git a/apps/web/src/app/admin/toggles/landing/page.tsx b/apps/web/src/app/admin/toggles/landing/page.tsx deleted file mode 100644 index 0316c8a75..000000000 --- a/apps/web/src/app/admin/toggles/landing/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { - NavItemsManager, - AddNavItemDialog, -} from "@/components/admin/toggles/NavItemsManager"; -import { getAllNavItems } from "@/lib/utils/server/redis"; - -export default async function Page() { - const nav = await getAllNavItems(); - return ( -
-
-

- Navbar Items -

-
- -
-
- - a.name.localeCompare(b.name), - )} - /> -
- ); -} - -export const dynamic = "force-dynamic"; diff --git a/apps/web/src/app/admin/toggles/layout.tsx b/apps/web/src/app/admin/toggles/layout.tsx deleted file mode 100644 index 0a2eff345..000000000 --- a/apps/web/src/app/admin/toggles/layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import ToggleItem from "@/components/admin/toggles/ToggleItem"; -import Restricted from "@/components/Restricted"; -import { PermissionType } from "@/lib/constants/permission"; -import { userHasPermission } from "@/lib/utils/server/admin"; -import { getCurrentUser } from "@/lib/utils/server/user"; -import { notFound } from "next/navigation"; - -interface ToggleLayoutProps { - children: React.ReactNode; -} - -export default async function Layout({ children }: ToggleLayoutProps) { - const user = await getCurrentUser(); - - if (!userHasPermission(user, PermissionType.MANAGE_NAVLINKS)) - return notFound(); - - return ( -
-
- - - - - - -
-
{children}
-
- ); -} diff --git a/apps/web/src/app/admin/toggles/page.tsx b/apps/web/src/app/admin/toggles/page.tsx deleted file mode 100644 index be7a63f30..000000000 --- a/apps/web/src/app/admin/toggles/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export default async function Page() { - return ( -
-

Toggles

-

- Toggles allow you to control various dynamic options on the - website. If you don't see an option here, chances are it can be - changed in the hackkit.config.ts file or via - enviroment variables. Visit the HackKit docs to learn more! -

-
- ); -} diff --git a/apps/web/src/app/admin/toggles/registration/page.tsx b/apps/web/src/app/admin/toggles/registration/page.tsx deleted file mode 100644 index fbd6ecbc0..000000000 --- a/apps/web/src/app/admin/toggles/registration/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { RegistrationToggles } from "@/components/admin/toggles/RegistrationSettings"; -import { PermissionType } from "@/lib/constants/permission"; -import { userHasPermission } from "@/lib/utils/server/admin"; -import { redisMGet } from "@/lib/utils/server/redis"; -import { parseRedisBoolean, parseRedisNumber } from "@/lib/utils/server/redis"; -import { getCurrentUser } from "@/lib/utils/server/user"; -import c from "config"; -import { notFound } from "next/dist/client/components/navigation"; - -export default async function Page() { - const [defaultRegistrationEnabled, defaultRSVPsEnabled, defaultRSVPLimit]: ( - | string - | null - )[] = await redisMGet( - "config:registration:registrationEnabled", - "config:registration:allowRSVPs", - "config:registration:maxRSVPs", - ); - - const user = await getCurrentUser(); - - if (!userHasPermission(user, PermissionType.MANAGE_REGISTRATION)) - return notFound(); - - return ( -
-
-

- Registration & Sign-in -

-
- -
- ); -} diff --git a/apps/web/src/app/dash/layout.tsx b/apps/web/src/app/dash/layout.tsx index 4d4eac4a1..c7a82a9b9 100644 --- a/apps/web/src/app/dash/layout.tsx +++ b/apps/web/src/app/dash/layout.tsx @@ -1,15 +1,18 @@ import c from "config"; -import Image from "next/image"; - -import { currentUser } from "@clerk/nextjs/server"; import Link from "next/link"; import { Button } from "@/components/shadcn/ui/button"; import DashNavItem from "@/components/dash/shared/DashNavItem"; import { redirect } from "next/navigation"; import ProfileButton from "@/components/shared/ProfileButton"; import ClientToast from "@/components/shared/ClientToast"; -import { getUser } from "db/functions"; import { getCurrentUser } from "@/lib/utils/server/user"; +import { cn } from "@/lib/utils/client/cn"; +import { Shadows_Into_Light } from "next/font/google"; + +const shadow = Shadows_Into_Light({ + subsets: ["latin"], + weight: "400", +}); interface DashLayoutProps { children: React.ReactNode; @@ -29,57 +32,66 @@ export default async function DashLayout({ children }: DashLayoutProps) { return ( <> -
-
- - {c.hackathonName - +
-
-

Dashboard

-
-
- - - - - - - - - - -
-
- -
-
-
- {Object.entries(c.dashPaths.dash).map(([name, path]) => ( - - ))} +
+
+ +
+
+ {"Pin"} +
+ +
+ +
+ {Object.entries(c.dashPaths.dash).map(([name, path]) => ( + + ))} +
+
+ +
+
+ + Home + + + Survival Guide + + + Discord + +
+ + {user && } + +
+
+ {"Pin"} +
+
+
+ {children} +
); } diff --git a/apps/web/src/app/dash/page.tsx b/apps/web/src/app/dash/page.tsx index 90cdef25b..5bbfc02f1 100644 --- a/apps/web/src/app/dash/page.tsx +++ b/apps/web/src/app/dash/page.tsx @@ -7,11 +7,15 @@ import { createQRpayload } from "@/lib/utils/shared/qr"; import { Countdown } from "@/components/dash/overview/ClientBubbles"; import { Questions, - TitleBubble, + TitleTicket, QuickQR, + QRTiteleHorizintalTicket, + QRTiteleVerticalTicket } from "@/components/dash/overview/ServerBubbles"; -import { getUser } from "db/functions"; import { getCurrentUser } from "@/lib/utils/server/user"; +import LandingThread from "@/components/landing/LandingThread"; +import Pin from "@/components/landing/Pin"; + export default async function Page() { const user = await getCurrentUser(); @@ -22,22 +26,29 @@ export default async function Page() { }); return ( -
-
-

Welcome,

-

- {user.firstName} -

-
-
- - - - -
-
+ <> + +
+ + +
+ +
+ + + + + + + + +
+
+ +
+ ); } diff --git a/apps/web/src/app/dash/pass/page.tsx b/apps/web/src/app/dash/pass/page.tsx index e4cdcb7d6..fecdc5679 100644 --- a/apps/web/src/app/dash/pass/page.tsx +++ b/apps/web/src/app/dash/pass/page.tsx @@ -12,6 +12,13 @@ import { } from "@/components/shadcn/ui/drawer"; import { getHacker } from "db/functions"; import { Hacker } from "db/types"; +import { Manuale, Shadows_Into_Light } from "next/font/google"; +import Pin from "@/components/landing/Pin"; + +const manuale = Manuale({ + subsets: ["latin"], + display: "swap", +}); interface EventPassProps { user: Hacker; @@ -34,103 +41,91 @@ export default async function Page() { const guild = Object.keys(c.groups)[userDbRecord.hackerData.group]; return ( -
- - - -
+ <> +
+ + + +
+ ); } function EventPass({ qrPayload, user, clerk, guild }: EventPassProps) { return ( -
-
-
-
+
+ + + + + +
+ +
{`${user.firstName}'s -

- {user.firstName} -

-
-

- @{user.hackerTag} +

+ + +
+
+

+ {user.firstName} +

+ +

+ Tag: @{user.hackerTag}

-

- {guild} +

+ Group: {guild}

-
-
-
- {""} -
-
- {``} -

- {c.hackathonName}{" "} - - {c.itteration} - -

-
-
-

{`${format( - c.startDate, - "h:mma, MMM d, yyyy", - )}`}

-

- {c.prettyLocation} -

-
-
-
-
- - -
+ +
+ + +
+ +
+
+ -
- - - - - + + +
+ +

+ Keep this Pass +

+ +
-
+
); } diff --git a/apps/web/src/app/discord-verify/page.tsx b/apps/web/src/app/discord-verify/page.tsx index cfe818717..db7edbb84 100644 --- a/apps/web/src/app/discord-verify/page.tsx +++ b/apps/web/src/app/discord-verify/page.tsx @@ -20,8 +20,6 @@ export default async function Page({ const passedCode = searchParams?.code; if (!passedCode || typeof passedCode !== "string") { - console.log("no code"); - console.log(passedCode); return notFound(); } diff --git a/apps/web/src/app/faq/faq-client.tsx b/apps/web/src/app/faq/faq-client.tsx new file mode 100644 index 000000000..e36178988 --- /dev/null +++ b/apps/web/src/app/faq/faq-client.tsx @@ -0,0 +1,184 @@ + +"use client" +import { Oswald } from "next/font/google"; +import { Manuale } from "next/font/google"; +import faqs from "./faq.json"; +import { motion } from "motion/react"; +import Pin from "@/components/landing/Pin"; +const manuale = Manuale({ + subsets: ["latin"], + display: "swap", +}); +const oswald = Oswald({ + variable: "--font-oswald", + subsets: ["latin"], +}); + +function PaperBg() { + return ( + + ); + +} + +type Faq = { question: string; answer: string }; + +const LEFT_COUNT = 5; +const allFaqs = faqs.faq as Faq[]; +const leftFaqs = allFaqs.slice(0, LEFT_COUNT); +const rightFaqs = allFaqs.slice(LEFT_COUNT); + +function DossierPins() { + return ( + <> + + + + + + + + + + + ); +} +function ClassifiedStamp({ + wrapperClassName = "relative flex justify-end pr-[5%] pb-[5%]", +}: { + wrapperClassName?: string; +}) { + return ( +
+ +
+ ); +} + +function FaqHeader() { + return ( +
+ +

+ FAQ +

+
+ ); +} + +function FaqItem({ item }: { item: Faq }) { + return ( + +
+

+ {item.question} +

+ +
+

+ {item.answer} +

+
+ ); +} + +export default function FAQClient() { + return ( +
+ +
+ + {/* */} +
+
+ {/* Left Paper */} +
+
+ +
+ +
+ {leftFaqs.map((item, index) => ( + + ))} +
+
+ + {/* Right Paper */} +
+
+ +
+ +
+ {rightFaqs.map((item, index) => ( + + ))} +
+
+ +
+
+ +
+
+ + +
+ + +
+ + {allFaqs.map((item, index) => ( + + ))} +
+
+
+ +
+ ); +} diff --git a/apps/web/src/app/faq/faq.json b/apps/web/src/app/faq/faq.json new file mode 100644 index 000000000..37620ca5b --- /dev/null +++ b/apps/web/src/app/faq/faq.json @@ -0,0 +1,44 @@ +{ + "faq": [ + { + "question": "Who can attend?", + "answer": "All students can sign up to be a hacker. Regardless of your experience, education, or background, as long as you are excited about learning, building, and having fun, we'd love for you to attend." + }, + { + "question": "When is the deadline to apply?", + "answer": "The deadline for registration is , at 23:59." + }, + { + "question": "How much experience do I need?", + "answer": "Absolutely zero! We want you here because you have a passion for creating, not because you're the most experienced hacker on the block. We'll have lots of resources including workshops and a bunch of mentors to help beginners get started. There'll also be plenty of people to learn from and help out!" + }, + { + "question": "How much does it cost?", + "answer": "Nothing. Nada. Zilch. It's completely free for all accepted hackers. To make it even better, we'll be giving out a ton of swag to every hacker, and prizes to our winners." + }, + { + "question": "What should I bring?", + "answer": "Please bring a valid ID in addition to anything that would help you with creating your hack or making you comfortable. A laptop, charger, mouse, keyboard, hardware, light jacket, and any hygienic products should be helpful. Don't bring anything you wouldn't bring on an airplane." + }, + { + "question": "Can I stay overnight?", + "answer": "Yes, however, if you leave the building after its closing time, you will not be able to re-enter until it re-opens, so plan accordingly. The building closes at 6PM and re-opens at 6AM." + }, + { + "question": "Where can I park?", + "answer": "UTSA Students are allowed to Park at Lots D1, D2, D3 with a parking pass. For non-UTSA students you may park in the Dolorosa lot and there is paid parking on Buena Vista Street." + }, + { + "question": "How do teams work?", + "answer": "Teams are limited to 1-4 hackers. If you have a team in mind, make sure all members submit an application before the deadline. If you don't have a team and would like to be a part of one, no worries! We'll have a dedicated time for team formation after the opening ceremony. " + }, + { + "question": "What should I bring?", + "answer": "Please bring a valid ID in addition to anything that would help you with creating your hack or making you comfortable. A laptop, charger, mouse, keyboard, hardware, light jacket, and any hygienic products should be helpful. Don't bring anything you wouldn't bring on an airplane." + }, + { + "question": "What if I have a question not addressed here?", + "answer": "Please reach out to team@rowdyhacks.org, We'll be happy to answer any questions you may have" + } + ] +} diff --git a/apps/web/src/app/faq/page.tsx b/apps/web/src/app/faq/page.tsx new file mode 100644 index 000000000..a881c695a --- /dev/null +++ b/apps/web/src/app/faq/page.tsx @@ -0,0 +1,17 @@ +import Navbar from "@/components/shared/Navbar"; +import FAQClient from "./faq-client"; +import Footer from "@/components/landing/Footer"; +import LandingThread from "@/components/landing/LandingThread"; +export default function FAQPage() { + return ( + <> + +
+ + + +
+
+ + ); +} \ No newline at end of file diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 69b2adacf..9fc65f97e 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -4,101 +4,71 @@ @layer base { :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - - --nav: 255, 255, 255; - --hackathon-primary: 206 86% 52%; - - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - - --ring: 240 5% 64.9%; - - --radius: 0.5rem; - - --gradient-color-1: #668cff; - --gradient-color-2: #3366ff; - --gradient-color-3: #002db3; - --gradient-color-4: #1952cc; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - - --nav: 240 10% 3.9%; - - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 85.7% 97.3%; - - --ring: 240 3.7% 15.9%; - - --sidebar-background: 240 5.9% 10%; - - --sidebar-foreground: 240 4.8% 95.9%; - - --sidebar-primary: 224.3 76.3% 48%; - - --sidebar-primary-foreground: 0 0% 100%; - - --sidebar-accent: 240 3.7% 15.9%; - - --sidebar-accent-foreground: 240 4.8% 95.9%; - - --sidebar-border: 240 3.7% 15.9%; - - --sidebar-ring: 217.2 91.2% 59.8%; + --gradient-color-stops: var(--hackathon-primary), transparent; + --bg-image: url("/img/assets/background.webp"); + + --background: oklch(0.9404 0.0181 89.3604); + --foreground: oklch(0.145 0 0); + --card: oklch(96.263% 0.00505 67.277); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.2843 0.0195 80.4025); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.8407 0.1291 90.3073); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(1 0 0); + --border: oklch(24.228% 0.01641 79.424); + --input: oklch(0.922 0 0); + --ring: oklch(24.228% 0.01641 79.424); + --nav: #ffffff; + --hackathon-primary: #ac1703; + --chart-1: oklch(0.81 0.1 252); + --chart-2: oklch(0.62 0.19 260); + --chart-3: oklch(0.55 0.22 263); + --chart-4: oklch(0.49 0.22 264); + --chart-5: oklch(0.42 0.18 266); + --sidebar: oklch(1 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.2843 0.0195 80.4025); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.8407 0.1291 90.3073); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.5452 0.0549 80.4233); + --sidebar-ring: oklch(0.708 0 0); + --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, + serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; + --radius: 0.625rem; + --shadow-x: 0; + --shadow-y: 1px; + --shadow-blur: 3px; + --shadow-spread: 0px; + --shadow-opacity: 0.1; + --shadow-color: oklch(0 0 0); + --shadow-2xs: 0 1px 3px 0px rgb(0 0 0 / 0.05); + --shadow-xs: 0 1px 3px 0px rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 3px 0px rgb(0 0 0 / 0.1), + 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow: 0 1px 3px 0px rgb(0 0 0 / 0.1), + 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-md: 0 1px 3px 0px rgb(0 0 0 / 0.1), + 0 2px 4px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 1px 3px 0px rgb(0 0 0 / 0.1), + 0 4px 6px -1px rgb(0 0 0 / 0.1); + --shadow-xl: 0 1px 3px 0px rgb(0 0 0 / 0.1), + 0 8px 10px -1px rgb(0 0 0 / 0.1); + --shadow-2xl: 0 1px 3px 0px rgb(0 0 0 / 0.25); } } @@ -109,6 +79,10 @@ body { @apply bg-background text-foreground; + + background-image: var(--bg-image); + background-size: contain; + background-position: center; } } @@ -140,7 +114,7 @@ .pulsatingDot:before { animation: pulseRing 1.25s cubic-bezier(0.215, 0.61, 0.355, 1) infinite; /* background-color: var(--pulsating-dot, #00BEFF); */ - background-color: hsl(var(--hackathon-primary)); + background-color: var(--hackathon-primary); border-radius: 45px; content: ""; display: block; @@ -172,3 +146,7 @@ transform: scale(1.1); } } + +.pin { + z-index: 40 !important; +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index b0b86cbd0..063b61976 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -12,9 +12,7 @@ export default function RootLayout({ return ( - - {children} - + {children} ); diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 71707548c..5469b11c1 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,11 +1,13 @@ import Navbar from "@/components/shared/Navbar"; import Hero from "@/components/landing/Hero"; import About from "@/components/landing/About"; +import Map from "@/components/landing/Map"; import Partners from "@/components/landing/Partners"; import Footer from "@/components/landing/Footer"; import MLHBadge from "@/components/landing/MLHBadge"; - +import FAQ from "@/components/landing/faq"; +import LandingThread from "@/components/landing/LandingThread"; import { Oswald } from "next/font/google"; import WorkWithUs from "@/components/landing/WorkWithUs"; @@ -16,15 +18,17 @@ const oswald = Oswald({ export default function Home() { return ( -
+
+ -
+
- - + + +
diff --git a/apps/web/src/app/register/page.tsx b/apps/web/src/app/register/page.tsx index 905969afb..769681f94 100644 --- a/apps/web/src/app/register/page.tsx +++ b/apps/web/src/app/register/page.tsx @@ -1,45 +1,56 @@ import c from "config"; import RegisterForm from "@/components/registration/RegisterForm"; +import RegisterClosed from "@/components/registration/RegistretionClosed"; import { auth, currentUser } from "@clerk/nextjs/server"; import { redirect } from "next/navigation"; import Navbar from "@/components/shared/Navbar"; import Link from "next/link"; -import { redisMGet } from "@/lib/utils/server/redis"; -import { parseRedisBoolean } from "@/lib/utils/server/redis"; -import { Button } from "@/components/shadcn/ui/button"; import { getUser } from "db/functions"; +import { Manuale, Shadows_Into_Light } from "next/font/google"; + +const manuale = Manuale({ + subsets: ["latin"], + display: "swap", +}); +const shadow = Shadows_Into_Light({ + subsets: ["latin"], + weight: "400", +}); export default async function Page() { - const { userId } = await auth(); - if (!userId) return redirect("/sign-up"); - const user = await currentUser(); - if (!user) return redirect("/sign-up"); + const registrationEnabled = c.registrationAvailable; + + if (registrationEnabled) { + + const { userId } = await auth(); + if (!userId) return redirect("/sign-up"); - const registration = await getUser(userId); - if (registration) return redirect("/dash"); + const user = await currentUser(); + if (!user) return redirect("/sign-up"); - const [defaultRegistrationEnabled]: (string | null)[] = await redisMGet( - "config:registration:registrationEnabled", - ); + const registration = await getUser(userId); + if (registration) return redirect("/dash"); - if (parseRedisBoolean(defaultRegistrationEnabled, true) === true) { return ( <> -
-
-

+
+
+

+ Case File · {c.hackathonName} +

+

Register

-

+

Welcome Hacker!{" "} Please fill out the form below to complete your registration for {c.hackathonName}.

-

+

Psttt... Running into a issue? Please let us know on{" "} - + Discord ! @@ -55,32 +66,5 @@ export default async function Page() { ); } - return ( -

-
-

{c.hackathonName}

-

- Registration -

-
-

- Registration Is Currently Closed -

-

- If you believe this is a mistake or have any questions, feel - free to reach out to us at {c.issueEmail}! -

- - - - -

- Already registered? - - Sign-in. - -

-
-
- ); + return ( ); } diff --git a/apps/web/src/app/rsvp/page.tsx b/apps/web/src/app/rsvp/page.tsx index 31632a650..0510ae86d 100644 --- a/apps/web/src/app/rsvp/page.tsx +++ b/apps/web/src/app/rsvp/page.tsx @@ -7,20 +7,11 @@ import { eq } from "db/drizzle"; import { userCommonData } from "db/schema"; import ClientToast from "@/components/shared/ClientToast"; import { SignedOut, RedirectToSignIn } from "@clerk/nextjs"; -import { - parseRedisBoolean, - parseRedisNumber, - redisGet, -} from "@/lib/utils/server/redis"; import Link from "next/link"; import { Button } from "@/components/shadcn/ui/button"; import { getUser } from "db/functions"; -export default async function RsvpPage({ - searchParams, -}: { - searchParams?: { [key: string]: string | string[] | undefined }; -}) { +export default async function RsvpPage() { const { userId } = await auth(); if (!userId) { @@ -42,22 +33,12 @@ export default async function RsvpPage({ return redirect("/i/approval"); } - const rsvpEnabled = parseRedisBoolean( - (await redisGet("config:registration:allowRSVPs")) as - | string - | boolean - | null - | undefined, - true, - ); + const rsvpEnabled = c.rsvpAvailable; let isRsvpPossible = false; - if (rsvpEnabled === true) { - const rsvpLimit = parseRedisNumber( - await redisGet("config:registration:maxRSVPs"), - c.rsvpDefaultLimit, - ); + if (rsvpEnabled) { + const rsvpLimit = c.rsvpLimit; const rsvpUserCount = await db .select({ count: count() }) diff --git a/apps/web/src/app/sign-up/[[...sign-up]]/page.tsx b/apps/web/src/app/sign-up/[[...sign-up]]/page.tsx index eea682138..c09385469 100644 --- a/apps/web/src/app/sign-up/[[...sign-up]]/page.tsx +++ b/apps/web/src/app/sign-up/[[...sign-up]]/page.tsx @@ -1,16 +1,11 @@ import { SignUp } from "@clerk/nextjs"; -import { redisMGet } from "@/lib/utils/server/redis"; -import { parseRedisBoolean } from "@/lib/utils/server/redis"; import c from "config"; -import { Button } from "@/components/shadcn/ui/button"; -import Link from "next/link"; +import RegisterClosed from "@/components/registration/RegistretionClosed";; export default async function Page() { - const [defaultRegistrationEnabled]: (string | null)[] = await redisMGet( - "config:registration:registrationEnabled", - ); + const registrationEnabled = c.registrationAvailable; - if (parseRedisBoolean(defaultRegistrationEnabled, true) === true) { + if (registrationEnabled) { return (
@@ -18,32 +13,5 @@ export default async function Page() { ); } - return ( -
-
-

{c.hackathonName}

-

- Registration -

-
-

- Registration Is Currently Closed -

-

- If you believe this is a mistake or have any questions, feel - free to reach out to us at {c.issueEmail}! -

- - - - -

- Already registered?{" "} - - Sign-in. - -

-
-
- ); + return ( ); } diff --git a/apps/web/src/app/user/[tag]/page.tsx b/apps/web/src/app/user/[tag]/page.tsx index 6158812b8..2fa4fd353 100644 --- a/apps/web/src/app/user/[tag]/page.tsx +++ b/apps/web/src/app/user/[tag]/page.tsx @@ -1,12 +1,22 @@ import { notFound } from "next/navigation"; import Image from "next/image"; -import RoleBadge from "@/components/dash/shared/RoleBadge"; -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"; +function ProfileField({ + label, + value, +}: { + label: string; + value: React.ReactNode; +}) { + return ( +
+ {label}: {value} +
+ ); +} + export default async function ({ params }: { params: { tag: string } }) { if (!params.tag || params.tag.length <= 1) return notFound(); @@ -16,91 +26,84 @@ export default async function ({ params }: { params: { tag: string } }) { return ( <> -
-
-
-
-
- {`@${user.hackerTag}'s -
-

- {user.firstName} {user.lastName} -

-
-

- @{user.hackerTag} -

- +
+
+
+ +
+ +
+ Central Intelligence Agency +
+ +

+ #hdbiwefh-0390128r9uedhfc923839r823h +

+ +
+
+ {`@${user.hackerTag}'s +
+ +
+ + + + + + + + +
+
+ +
+

+ Finger Prints: +

+
+ Fingerprints +
+
+
- {user.hackerData.GitHub && - user.hackerData.GitHub.length > 0 && ( - - - {user.hackerData.GitHub} - - )} - {user.hackerData.LinkedIn && - user.hackerData.LinkedIn.length > 0 && ( - - - {user.hackerData.LinkedIn} - - )} - {user.hackerData.PersonalWebsite && - user.hackerData.PersonalWebsite.length > 0 && ( - - - {user.hackerData.PersonalWebsite.replace( - "https://", - "", - ).replace("http://", "")} - - )} -
-
-

About

-

- {user.bio} -

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

Skills

-

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

- - ) : null} +
+
-
+
); } diff --git a/apps/web/src/components/admin/toggles/NavItemsManager.tsx b/apps/web/src/components/admin/toggles/NavItemsManager.tsx deleted file mode 100644 index 237590d6e..000000000 --- a/apps/web/src/components/admin/toggles/NavItemsManager.tsx +++ /dev/null @@ -1,317 +0,0 @@ -"use client"; - -import type { NavItemToggleType } from "@/validators/shared/navitemtoggle"; -import { - Table, - TableBody, - TableCaption, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/shadcn/ui/table"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/shadcn/ui/dialog"; -import { Button } from "@/components/shadcn/ui/button"; -import { Input } from "@/components/shadcn/ui/input"; -import { Label } from "@/components/shadcn/ui/label"; -import { Plus, Loader2 } from "lucide-react"; -import { useState } from "react"; -import { useAction, useOptimisticAction } from "next-safe-action/hooks"; -import { - setItem, - removeItem, - toggleItem, - editItem, -} from "@/actions/admin/modify-nav-item"; -import { toast } from "sonner"; -import Link from "next/link"; -import { Switch } from "@/components/shadcn/ui/switch"; - -interface NavItemsManagerProps { - navItems: NavItemToggleType[]; -} - -export function NavItemsManager({ navItems }: NavItemsManagerProps) { - const { execute, result, status } = useAction(removeItem, { - onSuccess: () => { - toast.dismiss(); - toast.success("NavItem deleted successfully!"); - }, - onError: () => { - toast.dismiss(); - toast.error("Error deleting NavItem"); - }, - }); - - return ( -
- - {/* A list of your recent invoices. */} - {/* TODO: FIX MASSIVE BUG WHERE IF ENCODED IS DIFFERENT IT WILL ALL BREAK */} - - - Name - Link - Enabled - Options - - - - {navItems.map((item) => ( - - - {item.name} - - - - {item.url} - - - - {/* didToggle(item.name, checked)} - /> */} - - - - - - - - ))} - -
-
- ); -} - -function ToggleSwitch({ - itemStatus, - name, -}: { - itemStatus: boolean; - name: string; -}) { - const initialData = { itemStatus }; // Initial data matching the shape of toggleItem's return type - - const { execute, optimisticState } = useOptimisticAction(toggleItem, { - currentState: initialData, - updateFn: (state, { statusToSet }) => { - return { itemStatus: statusToSet }; - }, - onError: () => { - toast.error("Error toggling NavItem"); - }, - }); - - return ( - - execute({ name, statusToSet: checked }) - } - /> - ); -} - -export function AddNavItemDialog() { - const [name, setName] = useState(null); - const [url, setUrl] = useState(null); - const [open, setOpen] = useState(false); - - const { - execute, - result, - status: createStatus, - } = useAction(setItem, { - onSuccess: () => { - console.log("Success"); - setOpen(false); - toast.success("NavItem created successfully!"); - }, - onError: () => { - toast.error("Error creating NavItem"); - }, - }); - - const isLoading = createStatus === "executing"; - - return ( - - - - - - - New Item - - Create a item to show in the non-dashboard navbar - - -
-
- - setName(e.target.value)} - id="name" - placeholder="A Cool Hyperlink" - className="col-span-3" - /> -
-
- - setUrl(e.target.value)} - id="name" - placeholder="https://example.com/" - className="col-span-3" - /> -
-
- - - -
-
- ); -} - -interface EditNavItemDialogProps { - existingName: string; - existingUrl: string; - existingEnabled: boolean; -} - -function EditNavItemDialog({ - existingName, - existingUrl, - existingEnabled, -}: EditNavItemDialogProps) { - const [name, setName] = useState(existingName); - const [url, setUrl] = useState(existingUrl); - const [open, setOpen] = useState(false); - - const { execute, status: editStatus } = useAction(editItem, { - onSuccess: () => { - console.log("Success"); - setOpen(false); - toast.success("NavItem edited successfully!"); - }, - onError: () => { - toast.error("Error editing NavItem"); - }, - }); - const isLoading = editStatus === "executing"; - - return ( - - - - - - - Edit Item - - Edit an existing item shown in the non-dashboard navbar - - -
-
- - setName(e.target.value)} - id="name" - placeholder="A Cool Hyperlink" - className="col-span-3" - value={name} - /> -
-
- - setUrl(e.target.value)} - id="name" - placeholder="https://example.com/" - className="col-span-3" - value={url} - /> -
-
- - - -
-
- ); -} diff --git a/apps/web/src/components/admin/toggles/RegistrationSettings.tsx b/apps/web/src/components/admin/toggles/RegistrationSettings.tsx deleted file mode 100644 index 116aa1f36..000000000 --- a/apps/web/src/components/admin/toggles/RegistrationSettings.tsx +++ /dev/null @@ -1,116 +0,0 @@ -"use client"; - -import { Button } from "@/components/shadcn/ui/button"; -import { Input } from "@/components/shadcn/ui/input"; -import { Label } from "@/components/shadcn/ui/label"; -import { Switch } from "@/components/shadcn/ui/switch"; -import { useOptimisticAction } from "next-safe-action/hooks"; -import { toast } from "sonner"; -import { - toggleRegistrationEnabled, - toggleRegistrationMessageEnabled, - toggleRSVPs, - setRSVPLimit, -} from "@/actions/admin/registration-actions"; -import { UpdateItemWithConfirmation } from "./UpdateItemWithConfirmation"; - -interface RegistrationTogglesProps { - defaultRegistrationEnabled: boolean; - defaultRSVPsEnabled: boolean; - defaultRSVPLimit: number; -} - -export function RegistrationToggles({ - defaultRegistrationEnabled, - defaultRSVPsEnabled, - defaultRSVPLimit, -}: RegistrationTogglesProps) { - const { - execute: executeToggleRSVPs, - optimisticState: toggleRSVPsOptimisticData, - } = useOptimisticAction(toggleRSVPs, { - currentState: { success: true, statusSet: defaultRSVPsEnabled }, - updateFn: (state, { enabled }) => { - return { statusSet: enabled, success: true }; - }, - }); - - const { - execute: executeToggleRegistrationEnabled, - optimisticState: ToggleRegistrationEnabledOptimisticData, - } = useOptimisticAction(toggleRegistrationEnabled, { - currentState: { success: true, statusSet: defaultRegistrationEnabled }, - updateFn: (state, { enabled }) => { - return { statusSet: enabled, success: true }; - }, - }); - - const { - execute: executeSetRSVPLimit, - optimisticState: SetRSVPLimitOptimisticData, - } = useOptimisticAction(setRSVPLimit, { - currentState: { success: true, statusSet: defaultRSVPLimit }, - updateFn: (state, { rsvpLimit }) => { - return { statusSet: rsvpLimit, success: true }; - }, - }); - - return ( - <> -
-

Registration

-
-
-

New Registrations

- { - toast.success( - `Registration ${checked ? "enabled" : "disabled"} successfully!`, - ); - executeToggleRegistrationEnabled({ - enabled: checked, - }); - }} - /> -
-
-
-
-

RSVPs

-
-
-

Allow RSVPs

- { - toast.success( - `RSVPs ${checked ? "enabled" : "disabled"} successfully!`, - ); - executeToggleRSVPs({ enabled: checked }); - }} - /> -
-
-

RSVP Limit

- { - toast.success( - `Hacker RSVP limit successfully changed to ${newLimit}!`, - ); - executeSetRSVPLimit({ rsvpLimit: newLimit }); - }} - /> -
-
-
- - ); -} diff --git a/apps/web/src/components/admin/toggles/ToggleItem.tsx b/apps/web/src/components/admin/toggles/ToggleItem.tsx deleted file mode 100644 index 0c57d9e58..000000000 --- a/apps/web/src/components/admin/toggles/ToggleItem.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { usePathname } from "next/navigation"; - -interface ToggleItemProps { - name: string; - path: string; -} - -export default function ToggleItem({ name, path }: ToggleItemProps) { - const currPath = usePathname(); - return ( - -
-

{name}

-
- - ); -} diff --git a/apps/web/src/components/admin/toggles/UpdateItemWithConfirmation.tsx b/apps/web/src/components/admin/toggles/UpdateItemWithConfirmation.tsx deleted file mode 100644 index dc35d3eab..000000000 --- a/apps/web/src/components/admin/toggles/UpdateItemWithConfirmation.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useState } from "react"; -import { Input } from "@/components/shadcn/ui/input"; -import { Button } from "@/components/shadcn/ui/button"; - -interface UpdateItemWithConfirmationBaseProps { - defaultValue: T; - enabled: boolean; - onSubmit: (value: T) => void; -} - -type UpdateItemWithConfirmationProps = - | ({ type: "string" } & UpdateItemWithConfirmationBaseProps) - | ({ type: "number" } & UpdateItemWithConfirmationBaseProps); - -export function UpdateItemWithConfirmation({ - type, - defaultValue, - onSubmit, - enabled, -}: UpdateItemWithConfirmationProps) { - const [valueUpdated, setValueUpdated] = useState(false); - const [value, setValue] = useState(defaultValue.toString()); - - return ( -
- { - // Ignore the change if the value is a non numeric character. - if (type === "number" && /[^0-9]/.test(updated)) { - setValue(value); - return; - } - - setValue(updated); - - /* Avoid allowing the user to update the default value to itself. - * Also disallow the user from sending a zero length input. */ - setValueUpdated( - updated !== defaultValue.toString() && - updated.length !== 0, - ); - }} - /> - -
- ); -} diff --git a/apps/web/src/components/dash/overview/ClientBubbles.tsx b/apps/web/src/components/dash/overview/ClientBubbles.tsx index 8551b01ba..bd7f64564 100644 --- a/apps/web/src/components/dash/overview/ClientBubbles.tsx +++ b/apps/web/src/components/dash/overview/ClientBubbles.tsx @@ -1,8 +1,17 @@ "use client"; -import { useState } from "react"; import { useTimer } from "react-timer-hook"; -import { Badge } from "@/components/shadcn/ui/badge"; +import { Manuale, Shadows_Into_Light } from "next/font/google"; +import Pin from "@/components/landing/Pin"; + +const manuale = Manuale({ + subsets: ["latin"], + display: "swap", +}); +const shadow = Shadows_Into_Light({ + subsets: ["latin"], + weight: "400", +}); interface CountdownProps { title: string; @@ -13,51 +22,66 @@ export function Countdown({ title, date }: CountdownProps) { const { seconds, minutes, hours, days } = useTimer({ expiryTimestamp: date, }); + function DossierPins() { + return ( + <> + + + + + + ); + } return ( -
-
-
-

+ + +
+
+

- {days} + {String(days).padStart(2, "0")}

Days

-
+

- {hours} + {String(hours).padStart(2, "0")}

Hours

-
+

- {minutes} + {String(minutes).padStart(2, "0")}

Minutes

-
+

- {seconds} + {String(seconds).padStart(2, "0")}

Seconds

-
- +
Time To {title} -
); -} +} \ No newline at end of file diff --git a/apps/web/src/components/dash/overview/ServerBubbles.tsx b/apps/web/src/components/dash/overview/ServerBubbles.tsx index e01e995b7..a7b400ece 100644 --- a/apps/web/src/components/dash/overview/ServerBubbles.tsx +++ b/apps/web/src/components/dash/overview/ServerBubbles.tsx @@ -1,67 +1,244 @@ -import { Button } from "@/components/shadcn/ui/button"; import Link from "next/link"; +import Image from "next/image"; import c from "config"; import { format } from "date-fns"; -import GradientHero from "./GradientHero"; import QRCode from "react-qr-code"; +import { Manuale, Shadows_Into_Light } from "next/font/google"; +import { Mail } from "lucide-react"; +import Pin from "@/components/landing/Pin"; + +const manuale = Manuale({ + subsets: ["latin"], + display: "swap", +}); + +const shadow = Shadows_Into_Light({ + subsets: ["latin"], + weight: "400", +}); export function Questions() { + function DossierPins() { + return ( + <> + + + + + ); + } return ( -
+
+ +
-

Have a question?

-

+

Have a question?

+

We're here to help! Feel free to reach out to us via email or on Discord.

-
- - +
+ + Discord logo - - + +
); } -export function TitleBubble() { +function QR({ qrPayload}: { qrPayload: string; qrSize?: string }) { + return ( + <> +

Quick QR

+
+ +
+

+ Click / Tap To Open Event Pass +

+ + ); +} + +function TicketInfo({ className, firstName }: { className?: string; firstName: string }) { return ( -
- -
-

+
+
+

{c.hackathonName}

-

- {`${format(c.startDate, "h:mma, MMM d, yyyy")}`} @{" "} - {c.prettyLocation} -

+

+ 24 hours, one mission: the perfect hack +

+
+
+
+
+

Event

+

{c.hackathonName}

+
+
+

Date

+

{format(c.startDate, "MM-dd-yy")}

+
+
+

Location

+

Downtown campus | SP1 | SP2

+
+
+

Pass No.

+

RH-10-2026

+
+
+ +
+ +
+

Welcome to the crew, {firstName}

+
+
+ ); +} + +export function QRTiteleHorizintalTicket({ qrPayload, firstName }: { qrPayload: string; firstName: string }) { + function DossierPins() { + return ( + <> + + + + + + ); + } + return ( +
+ + + +
+ + + + + + +
+
+ ); +} + + +export function QRTiteleVerticalTicket({ qrPayload, firstName }: { qrPayload: string; firstName: string }) { + function DossierPins() { + return ( + <> + + + + + + ); + } + return ( +
+ + + +
+ + + + + + +
+
+ ); +} + +export function TitleTicket({ firstName }: { firstName: string }) { + function DossierPins() { + return ( + <> + + + + + + ); + } + + return ( + + ); } export function QuickQR({ qrPayload }: { qrPayload: string }) { + function DossierPins() { + return ( + <> + + + + ); + } return ( - -

Quick QR

-
- + + + +
+
-

- Click / Tap To Open Event Pass -

+ ); } diff --git a/apps/web/src/components/dash/shared/DashNavItem.tsx b/apps/web/src/components/dash/shared/DashNavItem.tsx index 5f8ccad49..d25cdf3e8 100644 --- a/apps/web/src/components/dash/shared/DashNavItem.tsx +++ b/apps/web/src/components/dash/shared/DashNavItem.tsx @@ -13,13 +13,11 @@ export default function DashNavItem({ name, path }: DashNavItemProps) { return ( - - - - - - - -
-
-
- + + ); } diff --git a/apps/web/src/components/landing/LandingThread.tsx b/apps/web/src/components/landing/LandingThread.tsx new file mode 100644 index 000000000..6ab1a71e4 --- /dev/null +++ b/apps/web/src/components/landing/LandingThread.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { useEffect } from "react"; +import { mountLandingThread } from "@/lib/utils/client/thread"; + +export default function LandingThread() { + useEffect(() => mountLandingThread(), []); + return null; +} diff --git a/apps/web/src/components/landing/MLHBadge.tsx b/apps/web/src/components/landing/MLHBadge.tsx index 7c0e49c6d..774bd30d2 100644 --- a/apps/web/src/components/landing/MLHBadge.tsx +++ b/apps/web/src/components/landing/MLHBadge.tsx @@ -4,35 +4,19 @@ import Image from "next/image"; export default function MLHBadge() { return ( <> -
+
Major League Hacking 2025 Hackathon Season - -
-
- - Major League Hacking 2025 Hackathon Season(null); + const mapImgRef = useRef(null); + const mainCampusCardRef = useRef(null); + const mainCampusImgRef = useRef(null); + const sp1CardRef = useRef(null); + const sp1CampusImgRef = useRef(null); + + const sp1PinStyle = findPosition( + mapContainerRef, + mapImgRef, + { x: 0.54, y: 0.75 }, + 20, + ); + const sp1TextStyle = findPosition( + mapContainerRef, + mapImgRef, + { x: 0.46, y: 0.82 }, + 10, + ); + const sp1CircleStyle = findPosition( + mapContainerRef, + mapImgRef, + { x: 0.54, y: 0.77 }, + 10, + ); + const sp1CardTextStyle = findPosition( + sp1CardRef, + sp1CampusImgRef, + { x: 0.3, y: 0.87 }, + 20, + ); + + const mainCampusPinStyle = findPosition( + mapContainerRef, + mapImgRef, + { x: 0.31, y: 0.22 }, + 20, + ); + const mainCampusTextStyle = findPosition( + mapContainerRef, + mapImgRef, + { x: 0.3, y: 0.29 }, + 10, + ); + const mainCampusCircleStyle = findPosition( + mapContainerRef, + mapImgRef, + { x: 0.31, y: 0.24 }, + 10, + ); + const mainCampusCardTextStyle = findPosition( + mainCampusCardRef, + mainCampusImgRef, + { x: 0.4, y: 0.87 }, + 20, + ); + + const connectedStyle = findPosition( + mapContainerRef, + mapImgRef, + { x: 0.48, y: 0.5 }, + 10, + ); + + return ( +
+
+
+ {/* Pictures */} + +
+
+
+ main campus +
+ +
+
+

+ Target 2 +

+
+
+
+ +
+
+ sp1 +
+ +
+ +
+

+ SP1 - Target 1 +

+
+
+
+
+ + +
+
+
+ ); +} diff --git a/apps/web/src/components/landing/PartnerCard.tsx b/apps/web/src/components/landing/PartnerCard.tsx index 436873b92..32a70c657 100644 --- a/apps/web/src/components/landing/PartnerCard.tsx +++ b/apps/web/src/components/landing/PartnerCard.tsx @@ -1,6 +1,8 @@ import React from "react"; import Link from "next/link"; import Image from "next/image"; +import { Manuale } from "next/font/google"; +import Pin from "./Pin"; type Partner = { name: string; @@ -9,78 +11,60 @@ type Partner = { tier: string; }; -type colorMap = { - key: string; - value: string; -}; - -// const tierBorderMap = { -// [Tier.Title]: "w-[15rem] sm:w-72 md:w-72 lg:w-80 2xl:w-[19rem]", -// [Tier.Gold]: "w-[12.75rem] sm:w-[14.75rem] md:w-[16rem] lg:w-72 2xl:w-[19rem]", -// [Tier.Silver]: "w-[11rem] sm:w-52 md:w-60 lg:w-[16rem] 2xl:w-[17rem] ", -// [Tier.Bronze]: "w-32 sm:w-40 md:w-[12rem] lg:w-[14rem] 2xl:w-[16rem]", -// [Tier.Rowdy_Partner]: "w-[7rem] sm:w-32 md:w-40 lg:w-[11rem] 2xl:w-[13rem]", -// [Tier.In_Kind_Partner]: "w-[6rem] sm:w-[7rem] md:w-32 lg:w-40 2xl:w-52", -// }; - -const tierColorMap: { [key: string]: string } = { - ["Title Sponsor"]: "text-purple-500", - ["Gold Sponsor"]: "text-yellow-600", - ["Silver Sponsor"]: "text-gray-400", - ["Bronze Sponsor"]: "text-amber-800", - ["Rowdy Partner"]: "text-blue-500", - ["Rowdy In-Kind"]: "text-red-500", -}; +const manuale = Manuale({ + weight: ["300", "400", "500", "600", "700", "800"], + subsets: ["latin"], + variable: "--font-manuale", +}); function PartnerCard({ partner, is_title, + index, }: { partner: Partner; is_title: boolean; + index: number; }) { - const text: string = is_title - ? "text-2xl sm:text-3xl xl:text-4xl 2xl:text-[3rem]" - : "text-md sm:text-lg lg:text-xl xl:text-2xl 2xl:text-3xl"; - - const height: string = is_title - ? "h-[15rem] sm:h-[15rem] md:h-[16rem] lg:h-[20rem] xl:h-[20rem] 2xl:h-[22rem]" - : "h-[9rem] sm:h-[11rem] md:h-[11rem] lg:h-[12rem] xl:h-[14rem] 2xl:h-[17rem]"; - const image: string = is_title - ? "w-[17rem] sm:w-[17rem] md:w-[18rem] xl:w-[20rem] 2xl:w-[24rem]" - : "w-[8rem] sm:w-[10rem] md:w-[14rem] lg:w-48 xl:w-[16rem]"; - + const padding: string[] = [ + "pl-[15vh] pt-[0vh] pb-[2vh] lg:pt-[0vh] xl:pt-[5vh] xl:pb-[0vh] 2xl:pl-[3vh] 2xl:pt-[7vh] 2xl:pb-[0vh]", + "pl-[1vh] pr-[30vw] pt-[0vh] pb-[2vh] lg:pt-[0vh] lg:pr-[2vw] xl:pt-[2vh] xl:pr-[2vw] 2xl:pl-[0vw] 2xl:pr-[0vw] ", + "pl-[1vh] pr-[30vw] pt-[0vh] pb-[0vh] md:pr-[2vw] lg:pr-[30vw] xl:pr-[40vw] xl:pt-[0vh] xl:pb-[2vh] 2xl:pr-[0vw] 2xl:pb-[7vh]", + "pl-[1vh] pt-[5vh] pb-[0vh] md:pt-[15vh] lg:pt-[5vh] lg:pb-[0vh] lg:pb-[4vh] xl:pr-[3vw] xl:pt-[0vh] xl:pb-[6vh] 2xl:pl-[0vw] 2xl:pr-[0vw]", + "pl-[1vh] pt-[0vh] pb-[6vh] md:pr-[4vw] lg:pt-[0vh] lg:pb-[5vh] lg:pr-[4vw] xl:pr-[4vw] xl:pt-[6vh] xl:pb-[0vh] 2xl:pr-[33vw]", + "pl-[1vh] pt-[0vh] pb-[4vh] xl:pt-[5vh] 2xl:pb-[0vh] 2xl:pt-[7vh]", + "pl-[1vh] pb-[2vh]", + ]; return ( -
- {`${partner?.name} +
+ +
+
+ {partner.logo ? ( + {`${partner.name} + ) : ( + + Logo + + )} +
+ +
+
-

- {partner?.name} -

-

- {partner?.tier} -

); } diff --git a/apps/web/src/components/landing/Partners.tsx b/apps/web/src/components/landing/Partners.tsx index 9b8bb203b..22993dc41 100644 --- a/apps/web/src/components/landing/Partners.tsx +++ b/apps/web/src/components/landing/Partners.tsx @@ -1,6 +1,19 @@ import partnerData from "./partners.json"; import PartnerCard from "./PartnerCard"; import Image from "next/image"; +import { Manuale, Shadows_Into_Light } from "next/font/google"; +import Pin from "./Pin"; + +const shadowsIntoLight = Shadows_Into_Light({ + weight: "400", + subsets: ["latin"], + variable: "--font-shadows", +}); + +const manuale = Manuale({ + subsets: ["latin"], + display: "swap", +}); type Partner = { name: string; @@ -9,41 +22,88 @@ type Partner = { tier: string; }; -export default async function Partners() { - // Christian Walker: Aware of weird bug from 1280px to 1286 px where background dissapears - const marathon: Partner = { - name: "Marathon", - logo: "marathon_logo.svg", - url: "https://www.marathonpetroleum.com/", - tier: "Title Sponsor", - }; +function DossierPins() { + return ( + <> + + + + + + ); +} + +export default async function Partners() { return ( -
-
-

- Partners Sections -

-

- { - "See the Partners Component inside components/landing/Partners for an example" - } -

+
+ + +
+ +
+ +

+ Sponsors +

+
+ +
+ +

+ Currently under investigation +

+
+ + {/*
+
+ sponsors-background +
+ +
+ {partnerData.partners.map( + (partner: Partner, index: number) => ( + + ), + )} + {Array.from({ length: 6 }, (_, index) => ( + + ))} +
+
*/}
- {/* Example Code of what our previous partner section looked like */} - {/*

- A Huge Thanks To Our Rowdyhacks Partners! -

- -
- -
- -
- {partnerData.partners.map((partner: Partner) => ( - - ))} -
*/}
); } diff --git a/apps/web/src/components/landing/Person.tsx b/apps/web/src/components/landing/Person.tsx index 83e976af1..5075fdc35 100644 --- a/apps/web/src/components/landing/Person.tsx +++ b/apps/web/src/components/landing/Person.tsx @@ -1,9 +1,9 @@ export type Person = { - fname: string; //picture file name must match name with .png + fname: string; lname: string; imgLink: string; - role: string; - linkedin: string; - website: string; - github: string; + linkedin?: string; + note?: string; + top?:string + left?:string }; diff --git a/apps/web/src/components/landing/Pin.tsx b/apps/web/src/components/landing/Pin.tsx new file mode 100644 index 000000000..0fa7ec210 --- /dev/null +++ b/apps/web/src/components/landing/Pin.tsx @@ -0,0 +1,30 @@ +import Image from "next/image"; + +export default function Pin({ + name = "/img/assets/silver-pin.svg", + size = 25, + className = "", + no_thread = false, + no_img = false, +}: { + name?: string; + size?: number; + className?: string; + no_thread?: boolean; + no_img?: boolean; + maxSize?: number; +}) { + return ( + + ); +} diff --git a/apps/web/src/components/landing/TeamMember.tsx b/apps/web/src/components/landing/TeamMember.tsx deleted file mode 100644 index 870d8eeb8..000000000 --- a/apps/web/src/components/landing/TeamMember.tsx +++ /dev/null @@ -1,145 +0,0 @@ -"use client"; - -import { Person } from "./Person"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "../shadcn/ui/card"; -import { Oswald } from "next/font/google"; -import Image from "next/image"; -import { useState } from "react"; -const oswald = Oswald({ - variable: "--font-oswald", - subsets: ["latin"], -}); - -// Using the raw svg tag is inefficient. Will need to change later -function LinkedIn({ fillColor }: { fillColor: string }) { - return ( - - - - ); -} - -function Website({ fillColor }: { fillColor: string }) { - return ( - - - - ); -} - -function Github({ fillColor }: { fillColor: string }) { - return ( - - - - ); -} - -export default function TeamMember({ person }: { person: Person }) { - // Edit the max width and height and then set the height to auto in the styling - - const [src, setSrc] = useState(person.imgLink); - const [styling, setStyling] = useState( - "max-w-[110px] md:max-w-[140px] lg:max-w-[160px] 2xl:max-w-[200px] h-auto rounded-lg", - ); - - const FallBackStyling = - "max-w-[105px] md:max-w-[132px] lg:max-w-[150px] xl:max-w-[151px] 2xl:max-w-[188px] rounded-lg"; - - return ( - -
- - -

{`${person.fname}\u00A0${person.lname}`}

-
- -

- {person.role} -

-
-
- - {/* This also needs to be fixed */} - Person Placeholder { - setSrc("/img/logo/hackkit.svg"); - setStyling(FallBackStyling); - }} - /> - - - - -
-
- ); -} diff --git a/apps/web/src/components/landing/WorkWithUs.tsx b/apps/web/src/components/landing/WorkWithUs.tsx index 842c6e243..e45bc9624 100644 --- a/apps/web/src/components/landing/WorkWithUs.tsx +++ b/apps/web/src/components/landing/WorkWithUs.tsx @@ -1,13 +1,260 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useState } from "react"; +import { Shadows_Into_Light } from "next/font/google"; +import { Person } from "./Person"; +import teamData from "./team.json"; +import Link from "next/link"; +import { Linkedin } from "lucide-react"; +import { motion } from "motion/react"; +import Pin from "./Pin"; + +const people: Person[] = teamData.team; + +const shadowsIntoLight = Shadows_Into_Light({ + weight: "400", + subsets: ["latin"], + variable: "--font-shadows", +}); + +function srcFor(p: Person) { + return p.imgLink && p.imgLink.length > 0 ? p.imgLink : `/${p.fname}.png`; +} + +// Polaroid signature: +function Polaroid({ + person, + slotIndex, +}: { + person: Person; + slotIndex: number; +}) { + const [front, setFront] = useState(person); + const [back, setBack] = useState(person); + const [showFront, setShowFront] = useState(true); + const [roration] = useState(() => Math.random() * 10 - 3); + + useEffect(() => { + if (showFront && person !== front) { + setBack(person); + setShowFront(false); + } else if (!showFront && person !== back) { + setFront(person); + setShowFront(true); + } + }, [person, front, back, showFront]); + + return ( +
+ + {/* photo */} +
+ {`${front.fname} + +
+ + {/* name + linkedin in a row */} +
+ {/* name crossfade */} +
+

+ {front.fname} {front.lname} +

+

+ {back.fname} {back.lname} +

+
+ + {/* icon crossfade — its own relative box keeps it in the row */} +
+ {front.linkedin && ( + + + + )} + {back.linkedin && ( + + + + )} +
+
+ + {(() => { + const active = showFront ? front : back; + if (!active.note) return null; + return ( +
+

+ {active.note.split("").map((char, i) => ( + + {char === " " ? "\u00A0" : char} + + ))} +

+
+ ); + })()} +
+ ); +} + +function DossierPins() { + return ( + <> + + + + + ); +} + export default function WorkWithUs() { + const [index, setIndex] = useState(0); + const len = people.length || 1; + + const [step, setStep] = useState(3); + + useEffect(() => { + const lg = window.matchMedia("(min-width: 1024px)"); // lg + const md = window.matchMedia("(min-width: 768px)"); // md + const sm = window.matchMedia("(min-width: 640px)"); // sm + + const update = () => { + if (lg.matches) setStep(5); + else if (md.matches) setStep(4); + else if (sm.matches) setStep(3); + else setStep(2); + }; + + update(); + lg.addEventListener("change", update); + md.addEventListener("change", update); + sm.addEventListener("change", update); + return () => { + lg.removeEventListener("change", update); + md.removeEventListener("change", update); + sm.removeEventListener("change", update); + }; + }, []); + + const move = (delta: number) => setIndex((i) => (i + delta + len) % len); + const slots = [0, 1, 2, 3, 4]; + return ( -
-

- Work With Us Section -

-

- Incentivize companies to monetarily support and other students - to volunteer to help out!{" "} -

+
+
+
+
+ +

+ Main Suspects +

+
+
+ + {/* carousel row */} +
+ + + {slots.map((slot) => { + const person_id = (index + slot) % len; + const person = people[person_id]; + const visibility = + slot < 2 + ? "flex" + : slot === 2 + ? "hidden sm:flex" + : slot === 3 + ? "hidden md:flex" + : "hidden lg:flex"; + return ( +
+ +
+ ); + })} + + +
+
); } diff --git a/apps/web/src/components/landing/faq.json b/apps/web/src/components/landing/faq.json new file mode 100644 index 000000000..4a2fdac3d --- /dev/null +++ b/apps/web/src/components/landing/faq.json @@ -0,0 +1,28 @@ +{ + "faq": [ + { + "question": "Who can attend?", + "answer": "All students can sign up to be a hacker. Regardless of your experience, education, or background, as long as you are excited about learning, building, and having fun, we'd love for you to attend." + }, + { + "question": "When is the deadline to apply?", + "answer": "The deadline for registration is , at 23:59." + }, + { + "question": "How much experience do I need?", + "answer": "Absolutely zero! We want you here because you have a passion for creating, not because you're the most experienced hacker on the block. We'll have lots of resources including workshops and a bunch of mentors to help beginners get started. There'll also be plenty of people to learn from and help out!" + }, + { + "question": "How much does it cost?", + "answer": "Nothing. Nada. Zilch. It's completely free for all accepted hackers. To make it even better, we'll be giving out a ton of swag to every hacker, and prizes to our winners." + }, + { + "question": "What should I bring?", + "answer": "Please bring a valid ID in addition to anything that would help you with creating your hack or making you comfortable. A laptop, charger, mouse, keyboard, hardware, light jacket, and any hygienic products should be helpful. Don't bring anything you wouldn't bring on an airplane." + }, + { + "question": "How do teams work?", + "answer": "Teams are limited to 1-4 hackers. If you have a team in mind, make sure all members submit an application before the deadline. If you don't have a team and would like to be a part of one, no worries! We'll have a dedicated time for team formation after the opening ceremony." + } + ] +} diff --git a/apps/web/src/components/landing/faq.tsx b/apps/web/src/components/landing/faq.tsx new file mode 100644 index 000000000..92b38e83a --- /dev/null +++ b/apps/web/src/components/landing/faq.tsx @@ -0,0 +1,279 @@ +"use client"; +import { Manuale } from "next/font/google"; +import faqData from "./faq.json"; +import { Shadows_Into_Light } from "next/font/google"; +import { motion } from "motion/react"; +import Pin from "./Pin"; +import Link from "next/link"; +import { useState } from "react"; +const shadow = Shadows_Into_Light({ + subsets: ["latin"], + weight: "400", +}); + +export function FaqHoverLink() { + + return ( + + +
+ + More Questions? + + + + +
+
+ + ); +} + + +export function MobileFaqHoverLink() { + + return ( + + +
+ + More Questions? + + + + +
+
+ + ); + } + +const manuale = Manuale({ + subsets: ["latin"], + display: "swap", +}); + +type Faq = { question: string; answer: string }; + +const LEFT_COUNT = 4; +const allFaqs = faqData.faq as Faq[]; +const leftFaqs = allFaqs.slice(0, LEFT_COUNT); +const rightFaqs = allFaqs.slice(LEFT_COUNT); + +function DossierPins() { + return ( + <> + + + + + ); +} + +function PaperBg() { + return ( + + ); +} + +function FaqHeader() { + return ( +
+ +

+ FAQ +

+
+ ); +} + +function ClassifiedStamp({ + wrapperClassName = "relative flex justify-end pr-[5%] pb-[5%]", +}: { + wrapperClassName?: string; +}) { + return ( +
+ +
+ ); +} + +function FaqItem({ item }: { item: Faq }) { + return ( + +
+

+ {item.question} +

+ +
+

+ {item.answer} +

+
+ ); +} + +/* ---------- Page ---------- */ + +export default function FAQ() { + return ( +
+ +
+ {/* ===== Desktop & Tablet ===== */} +
+ {/* Left paper */} +
+
+ + + {leftFaqs.map((item, index) => ( + + ))} +
+
+ + {/* Right paper */} +
+
+ + + {rightFaqs.map((item, index) => ( + + ))} + + + +
+
+
+ + {/* ===== Mobile ===== */} +
+
+ + +
+ + +
+ + {allFaqs.map((item, index) => ( + + ))} + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/landing/partners.json b/apps/web/src/components/landing/partners.json index fc654015b..17ec4b4b7 100644 --- a/apps/web/src/components/landing/partners.json +++ b/apps/web/src/components/landing/partners.json @@ -1,142 +1,28 @@ { "partners": [ { - "name": "Cymanii", - "logo": "CyManII_Logo.svg", - "url": "https://cymanii.org/", - "tier": "Gold Sponsor" - }, - { - "name": "UTSA DS Dept.", - "logo": "UTSADS.svg", - "url": "https://sds.utsa.edu/", - "tier": "Gold Sponsor" - }, - { - "name": "Swivel", - "logo": "swivel_logo.svg", - "url": "https://www.getswivel.io/", - "tier": "Silver Sponsor" - }, - { - "name": "UTSA CS Dept.", - "logo": "UTSA_CS.svg", - "url": "https://sciences.utsa.edu/computer-science/", - "tier": "Silver Sponsor" - }, - { - "name": "Frost Bank", - "logo": "FrostBank.svg", - "url": "https://www.frostbank.com/", - "tier": "Silver Sponsor" - }, - { - "name": "Valero", - "logo": "ValeroLogo.svg", - "url": "https://www.valero.com/", - "tier": "Silver Sponsor" - }, - { - "name": "Google", - "logo": "Google_Icon.svg", - "url": "https://about.google/", - "tier": "Bronze Sponsor" - }, - { - "name": "Paycom", - "logo": "PaycomLogo.svg", - "url": "https://www.paycom.com/", - "tier": "Bronze Sponsor" + "name": "H-E-B", + "logo": "HEB.svg", + "url": "https://www.heb.com/", + "tier": "title" }, { "name": "Dell", - "logo": "Dell_Tech_Logo.svg", + "logo": "dell.svg", "url": "https://www.dell.com/", - "tier": "Bronze Sponsor" - }, - { - "name": "S + S", - "logo": "Students_and_Startups.svg", - "url": "https://studentsstartups.com/", - "tier": "Bronze Sponsor" - }, - { - "name": "AFCS", - "logo": "AFCSLogo.svg", - "url": "https://afciviliancareers.com/", - "tier": "Bronze Sponsor" - }, - { - "name": "Accenture", - "logo": "Accenture-logo.svg", - "url": "https://www.accenture.com/", - "tier": "Rowdy Partner" - }, - { - "name": "UTSA COE", - "logo": "UTSA_COE.svg", - "url": "https://klesse.utsa.edu/", - "tier": "Rowdy Partner" - }, - { - "name": "UTSA Tech Store", - "logo": "rowdy_tech_logo.svg", - "url": "https://campustechnologystore.com/campustechnologystore/", - "tier": "Rowdy Partner" - }, - { - "name": "TD Synnex", - "logo": "TD_Synnex_logo.svg", - "url": "https://www.tdsynnex.com/", - "tier": "Rowdy Partner" - }, - { - "name": "Wolfram Alpha", - "logo": "wolfram_logo.svg", - "url": "https://www.wolframalpha.com/", - "tier": "Rowdy Partner" - }, - { - "name": "CodePath", - "logo": "Codepath_logo.svg", - "url": "https://www.codepath.org/", - "tier": "Rowdy Partner" + "tier": "golden" }, { - "name": "ACM", - "logo": "ACM_logo.svg", - "url": "https://www.acm.org/", - "tier": "Rowdy Partner" - }, - { - "name": "Artea", - "logo": "Artea_logo.svg", - "url": "https://www.drinkartea.com/", - "tier": "Rowdy In-Kind" - }, - { - "name": "Pho Thien An", - "logo": "Pho_logo.svg", - "url": "https://www.phothienan.com/", - "tier": "Rowdy In-Kind" - }, - { - "name": "Bunz Burgers", - "logo": "Bunz_logo.svg", - "url": "https://www.tastybunz.com/", - "tier": "Rowdy In-Kind" - }, - { - "name": "H-E-B", - "logo": "HEB.svg", - "url": "https://www.heb.com/", - "tier": "Rowdy In-Kind" + "name": "Apple", + "logo": "apple.svg", + "url": "https://www.apple.com/", + "tier": "silver" }, { - "name": "Six Flags", - "logo": "Six_Flags_logo.svg", - "url": "https://www.sixflags.com/fiestatexas", - "tier": "Rowdy In-Kind" + "name": "google", + "logo": "google.svg", + "url": "https://www.google.com/", + "tier": "bronze" } ] } diff --git a/apps/web/src/components/landing/team.json b/apps/web/src/components/landing/team.json new file mode 100644 index 000000000..86a9e9760 --- /dev/null +++ b/apps/web/src/components/landing/team.json @@ -0,0 +1,202 @@ +{ + "team": [ + { + "fname": "Evelynn", + "lname": "Donaldson", + "imgLink": "/img/assets/team/Evelynn Donaldson.webp", + "linkedin": "https://www.linkedin.com/in/evelynn-donaldson-191a00290/" + }, + { + "fname": "Alekzander", + "lname": "Brysch", + "imgLink": "/img/assets/team/Alekzander Brysch.webp", + "linkedin": "https://www.linkedin.com/in/zander-brysch/", + "note": "Director?", + "top": "87%", + "left": "10%" + }, + { + "fname": "Paula", + "lname": "Com", + "imgLink": "/img/assets/team/Paula Com.webp", + "linkedin": "https://www.linkedin.com/in/paula-com-morales/" + }, + { + "fname": "Tri", + "lname": "Nguyen", + "imgLink": "/img/assets/team/Tri Nguyen.webp", + "linkedin": "https://www.linkedin.com/in/tri-nguyen2/" + }, + { + "fname": "Rufat", + "lname": "Niftaliyev", + "imgLink": "/img/assets/team/Rufat Niftaliyev.webp", + "linkedin": "https://www.linkedin.com/in/rufat-niftaliyev/" , + "note": "Lead?", + "top": "87%", + "left": "50%" + }, + { + "fname": "Cayden", + "lname": "Hutcheson", + "imgLink": "/img/assets/team/Cayden Hutcheson.webp", + "linkedin": "https://www.linkedin.com/in/cayden-hutcheson110/", + "note": "Director?", + "top": "8%", + "left": "15%" + }, + { + "fname": "Ash", + "lname": "Hernandez", + "imgLink": "/img/assets/team/Ash Hernandez.webp", + "linkedin": "https://www.linkedin.com/in/ashley-hernandez-sanchez-b76312333/" + }, + { + "fname": "Maryna", + "lname": "Korolova", + "imgLink": "/img/assets/team/Maryna Korolova.webp", + "linkedin": "https://www.linkedin.com/in/marynakorolova", + "note": "Lead?", + "top": "83%", + "left": "50%" + }, + { + "fname": "Shaun", + "lname": "Philippe", + "imgLink": "/img/assets/team/Shaun Philippe.webp", + "linkedin": "https://www.linkedin.com/in/shaun-ph/" + }, + { + "fname": "Josie", + "lname": "Sauceda", + "imgLink": "/img/assets/team/Josie Sauceda.webp", + "linkedin": "https://www.linkedin.com/in/josie-sauceda/" + }, + { + "fname": "Miguel", + "lname": "Oseguera", + "imgLink": "/img/assets/team/Miguel Oseguera.webp", + "linkedin": "https://www.linkedin.com/in/miguel-oseguera-0b5306281/", + "note": "Lead?", + "top": "8%", + "left": "8%" + }, + { + "fname": "Blessy", + "lname": "Kalluri", + "imgLink": "/img/assets/team/Blessy Kalluri.webp", + "linkedin": "https://www.linkedin.com/in/blessykalluri/" + }, + { + "fname": "Camille", + "lname": "Hart", + "imgLink": "/img/assets/team/Camille Hart.webp", + "linkedin": "https://www.linkedin.com/in/camille-louise-rivera/" + }, + { + "fname": "Anh", + "lname": "Doan", + "imgLink": "/img/assets/team/Anh Doan.webp", + "linkedin": "https://www.linkedin.com/in/minh-anh-doan-600b94291/", + "note": "Lead?", + "top": "85%", + "left": "6%" + }, + { + "fname": "Abrar", + "lname": "Ahmed", + "imgLink": "/img/assets/team/Abrar Ahmed.webp", + "linkedin": "https://www.linkedin.com/in/abrar-ahmed1/" + }, + { + "fname": "Diego", + "lname": "Medina", + "imgLink": "/img/assets/team/Diego Medina.webp", + "linkedin": "https://www.linkedin.com/in/diegomedina12/", + "note": "Lead?", + "top": "85%", + "left": "39%" + }, + { + "fname": "Dyshana", + "lname": "Torres Rivera", + "imgLink": "/img/assets/team/Dyshana Torres Rivera.webp", + "linkedin": "https://www.linkedin.com/in/dyshana-torres/" + }, + { + "fname": "Elisa", + "lname": "Moran", + "imgLink": "/img/assets/team/Elisa Moran.webp", + "linkedin": "https://www.linkedin.com/in/elisa-moran-a6aa2b29a/" + }, + { + "fname": "Eric", + "lname": "Lee", + "imgLink": "/img/assets/team/Eric Lee.webp", + "linkedin": "https://www.linkedin.com/in/eric-lee-sunghyun/", + "note": "Lead?", + "top": "9%", + "left": "7%" + }, + { + "fname": "Francisco", + "lname": "Epinoza", + "imgLink": "/img/assets/team/Francisco Epinoza.webp", + "linkedin": "https://www.linkedin.com/in/francisco0-espinoza7/" + }, + { + "fname": "Layla", + "lname": "Mendiola", + "imgLink": "/img/assets/team/Layla Mendiola.webp", + "linkedin": "https://www.linkedin.com/in/layla-mendiola-144013381/" + }, + { + "fname": "Martin", + "lname": "Llano", + "imgLink": "/img/assets/team/Martin Llano.webp", + "linkedin": "https://www.linkedin.com/in/martin-llano-b10239291/" + }, + { + "fname": "Reese", + "lname": "Sylvester", + "imgLink": "/img/assets/team/Reese Sylvester.webp", + "linkedin": "" , + "note": "Lead?", + "top": "10%", + "left": "13%" + }, + { + "fname": "Anam", + "lname": "Sultana", + "imgLink": "/img/assets/team/Anam Sultana.webp", + "linkedin": "https://www.linkedin.com/in/anam-sult/" + }, + { + "fname": "Savanah", + "lname": "Schaefer", + "imgLink": "/img/assets/team/Savanah Schaefer.webp", + "linkedin": "https://www.linkedin.com/in/savanahsch/", + "note": "Lead?", + "top": "86%", + "left": "10%" + }, + { + "fname": "Scherly", + "lname": "Ramirez", + "imgLink": "/img/assets/team/Scherly Ramirez.webp", + "linkedin": "https://www.linkedin.com/in/scherly-ramirez-miranda-189623388/" + }, + { + "fname": "Tochi", + "lname": "Kalu", + "imgLink": "/img/assets/team/Tochi Kalu.webp", + "linkedin": "" + }, + { + "fname": "Victoria", + "lname": "Rivas", + "imgLink": "/img/assets/team/Victoria Rivas.webp", + "linkedin": "" + } + ] +} \ No newline at end of file diff --git a/apps/web/src/components/registration/FormGroupWrapper.tsx b/apps/web/src/components/registration/FormGroupWrapper.tsx index 1c9867feb..86a91947f 100644 --- a/apps/web/src/components/registration/FormGroupWrapper.tsx +++ b/apps/web/src/components/registration/FormGroupWrapper.tsx @@ -8,8 +8,8 @@ export default function FormGroupWrapper({ title, }: FormGroupWrapperProps) { return ( -
-

+

+

{title}

{children}
diff --git a/apps/web/src/components/registration/RegisterForm.tsx b/apps/web/src/components/registration/RegisterForm.tsx index 85abd141b..c435097c2 100644 --- a/apps/web/src/components/registration/RegisterForm.tsx +++ b/apps/web/src/components/registration/RegisterForm.tsx @@ -82,6 +82,12 @@ import { decodeBase64AsFile, } from "@/lib/utils/shared/files"; import { useDebouncedCallback } from "use-debounce"; +import { Manuale } from "next/font/google"; + +const manuale = Manuale({ + subsets: ["latin"], + display: "swap", +}); export default function RegisterForm({ defaultEmail, @@ -361,14 +367,14 @@ export default function RegisterForm({ isLoading={isLoading} /> ) : ( -
+
-
+
- + @@ -1292,7 +1298,7 @@ export default function RegisterForm({ {formatRegistrationField( - `Where did you hear about ${c.hackathonName}?`, + `Where did you hear about us?`, hackerRegistrationFormValidator.shape[ field.name ].isOptional(), @@ -1648,13 +1654,13 @@ export default function RegisterForm({ uploadedFile ? "" : "cursor-pointer" - } flex min-h-[200px] flex-col items-center justify-center rounded-lg border-dashed border-white`} + } flex min-h-[200px] flex-col items-center justify-center rounded-lg border border-dashed border-border`} > -

+

{uploadedFile ? `${uploadedFile.name} (${Math.round(uploadedFile.size / 1024)}kb)` : isDragActive @@ -1697,7 +1703,7 @@ export default function RegisterForm({

-
+
@
{ setSkills(newTags); field.onChange( diff --git a/apps/web/src/components/registration/RegistretionClosed.tsx b/apps/web/src/components/registration/RegistretionClosed.tsx new file mode 100644 index 000000000..0c4b1f52f --- /dev/null +++ b/apps/web/src/components/registration/RegistretionClosed.tsx @@ -0,0 +1,54 @@ +import c from "config"; +import { Button } from "@/components/shadcn/ui/button"; +import Link from "next/link"; +import { Manuale, Shadows_Into_Light } from "next/font/google"; + +const manuale = Manuale({ + subsets: ["latin"], + display: "swap", +}); +const shadow = Shadows_Into_Light({ + subsets: ["latin"], + weight: "400", +}); + +export default function RegistrationClosed() { + return ( +
+
+

+ Case File · {c.hackathonName} +

+ +

+ Registration Is
Currently Closed +

+ +

+ This operation is currently closed to new team members. If you believe + this was an error, or you've got intel for us — + reach the handlers at{" "} + + {c.issueEmail} + + . +

+ +
+ + + +
+ +

+ Already in the team? + + Sign in. + +

+
+
+ ); +} diff --git a/apps/web/src/components/shadcn/ui/button.tsx b/apps/web/src/components/shadcn/ui/button.tsx index 4006280be..06178a58c 100644 --- a/apps/web/src/components/shadcn/ui/button.tsx +++ b/apps/web/src/components/shadcn/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils/client/cn"; const buttonVariants = cva( - "inline-flex items-center justify-center 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 rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { diff --git a/apps/web/src/components/shadcn/ui/checkbox.tsx b/apps/web/src/components/shadcn/ui/checkbox.tsx index 6f1abca51..b64d0f427 100644 --- a/apps/web/src/components/shadcn/ui/checkbox.tsx +++ b/apps/web/src/components/shadcn/ui/checkbox.tsx @@ -13,7 +13,7 @@ const Checkbox = React.forwardRef< ) { {...fieldProps} ref={ref} className={cn( - "inline-flex h-10 flex-1 items-center rounded-l-md border border-r-0 border-input bg-transparent py-2 pl-3 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + "inline-flex h-10 flex-1 items-center rounded-l-md border border-r-0 border-input bg-transparent py-2 pl-3 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0", props.isDisabled ? "cursor-not-allowed opacity-50" : "", )} > diff --git a/apps/web/src/components/shadcn/ui/date-time-picker/time-field.tsx b/apps/web/src/components/shadcn/ui/date-time-picker/time-field.tsx index 7905d22af..e62a5dacc 100644 --- a/apps/web/src/components/shadcn/ui/date-time-picker/time-field.tsx +++ b/apps/web/src/components/shadcn/ui/date-time-picker/time-field.tsx @@ -29,7 +29,7 @@ function TimeField(props: AriaTimeFieldProps) { {...fieldProps} ref={ref} className={cn( - "inline-flex h-10 w-full flex-1 rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + "inline-flex h-10 w-full flex-1 rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0", props.isDisabled ? "cursor-not-allowed opacity-50" : "", )} > diff --git a/apps/web/src/components/shadcn/ui/input.tsx b/apps/web/src/components/shadcn/ui/input.tsx index e5bf081f6..001d1c6d7 100644 --- a/apps/web/src/components/shadcn/ui/input.tsx +++ b/apps/web/src/components/shadcn/ui/input.tsx @@ -11,7 +11,7 @@ const Input = React.forwardRef( (({ className, ...props }, ref) => ( ( return (