I recommend duplicating this repository for each hunt.
Create a bare clone of this repostiory.
git clone --bare https://github.com/qiaochloe/bph-site.git
Create a new repository on GitHub. You will get an URL for this repository.
Mirror-push to the new repository.
cd bph-site.git
git push --mirror https://github.com/Brown-Puzzle-Club/NEW_REPOSITORY.git
Remove the temporary repository.
cd ..
rm -rf bph-site.git
Clone the new repository to your local machine.
git clone https://github.com/Brown-Puzzle-Club/NEW_REPOSITORY.git
Add the original repository as a remote branch public
.
git remote add public https://github.com/qiaochloe/bph-site.git
To get changes from the original repository, run
git fetch public
To merge changes from the original repository, run
git merge public/main
There is more information on the GitHub docs.
Get the puzzle name, the slug, and the answer. This is up to the puzzle-writer. The slug must be unique.
Puzzle name: "Sudoku #51"
Slug: "sudoku51"
Answer: "IMMEDIATE"
Update the puzzle table on Drizzle.
To get to Drizzle, run pnpm run db:push
and
go to https://local.drizzle.studio/
. The name
column is the name, the id
column is the slug, and the answer
column is the answer.
{
id: "sudoku51",
name: "Sudoku #51",
answer: "IMMEDIATE"
}
There are several steps to creating a puzzle page with varying levels of customizability.
After adding the puzzle to the puzzle table, you can automatically access the default look of the puzzle. This is useful for checking that the database is working correctly.
Eg. https://localhost:3000/puzzle/sudoku51
To customize the puzzle page, create a folder in src/app/(hunt)/puzzle/
. The folder must be named after the puzzle slug. Copy the contents of the src/app/(hunt)/puzzle/example
folder. Hard-code the puzzle id, the puzzle body, and the solution body inside of data.tsx. The puzzle, hint, and solution pages for this particular puzzle will be automatically updated. You can view them at:
https://localhost:3000/puzzle/sudoku51/
https://localhost:3000/puzzle/sudoku51/solution
https://localhost:3000/puzzle/sudoku51/hint
To completely customize the puzzle page, throw out the default components and edit page.tsx
directly.
All of the customizable features of the hunt structure is in hunt.config.ts
. To change how puzzles are unlocked, edit getNextPuzzleMap
in hunt.config.ts
. To change how many hints a team gets, edit getTotalHints
.
Make sure that hint.config.ts
is correct.
Before registration starts,
REGISTRATION_START_TIME
and REGISTRATION_END_TIME
HUNT_START_TIME
and HUNT_END_TIME
Before the hunt starts,
NUMBER_OF_GUESSES_PER_PUZZLE
INITIAL_PUZZLES
, getNextPuzzleMap
, and checkFinishHunt
getTotalHints
src/app/(hunt)/puzzle/[slug]
src/app/(hunt)/puzzle/example
.For admins, there is an admin
section and a hunt
section with different navbars. You can navigate using the navbar or the command palette (Cmd-K
or Ctrl-K
). This works reliably on Chrome, but you might need to refresh on Safari or other browsers.
This can be managed in the admin
section under admin/hints
and admin/errata
.
Team password resets can be made in teams/username
. Please don't change them directly in the Drizzle database. It won't work correctly because passwords need to be hashed.
.env.example
file to .env
and fill in the values. You will only need to sign up for Vercel Postgres and integrate it with Drizzle to develop locally. pnpm install
to install the dependencies.pnpm run dev
to start the development server.pnpm run db:studio
in a separate shell to open Drizzle Studio in your browser.pnpm run db:push
in a separate shell to push the schema in src/server/db/schema.ts
to the database.Make sure to check that you are reading documentation for Next.js with the App Router, not the Pages Router.
Auth.js is formerly known as NextAuth.js. Most documentation out there is still for v4, so check that you are reading documentation for v5.
This project is built using Next.js v14 using the App Router (not the Pages Router). The frontend is in the src/app
folder, and the backend is in the src/server
folder.
We use Vercel Postgres as the database and Drizzle as the ORM. All of the code for the database is in the src/server/db
folder.
Most of the client-to-server communication is currently handled by Vercel Server Actions.
Server Actions allow us to execute database queries or API calls inside of a React component without needing to explicitly define an API route.
Note that Server Actions can only be defined in Server Components marked with the use server
directive.
To make them available in Client Components marked with the use client
directive, they must be imported or passed as a prop.
Server Actions are generally located in actions.tsx
files distributed throughout src/app
.
Authentication, authorization, and session management is handled by Auth.js.
We only support username/password authentication using the Credentials
provider.
Sessions are stored in Json Web Tokens (JWTs) instead of database sessions.
The setup is in the src/server/auth
folder.
Finally, on the frontend, we are using Shadcn UI components with the Tailwind CSS framework. Components are in the src/components/ui
folder.
In terms of hunt logistics:
Teams can:
src/app/register
)src/app/login
)src/app/puzzle/components/GuessForm.tsx
)src/app/puzzle/components/PreviousGuessTable.tsx
)src/app/puzzle/components/HintForm.tsx
)src/app/puzzle/components/PreviousHintTable.tsx
)src/app/leaderboard
)Admins can:
src/app/admin/teams
)src/app/admin/guesses
)src/app/admin/hints
)src/app/admin/hints
)DNS -> Cloudflare -> Digital Ocean -> Reverse proxy -> Digital Ocean -> Vercel
The DNS can also do reverse proxying. We can also reduce cost by $20/month by using a static site.
Application Programming Interface (API) routes provide non-persistent, synchronous, bidirectional communication between the client and the server. It is synchronous because the client sends a request to the server, waits for a response, and closes the connection. There is latency from opening the connection.
There are two types of API architectures: REST and RPC. RPC APIs allow clients to call remote functions in the server as if they were local functions. REST APIs allow clients to perform specific, predefined actions using HTTP verbs (GET, POST, PUT).
Server actions uses the RPC architecture. Whenever you add use server
to a function or file, it marks it as available to the client. The client will get a URL string to that function, which they can use to send a request to the server using RPC. You never see this URL string, but that's how it works under the hood. The most significant benefit of using server actions is that you don't need to create an API route. This is good enough for our use case, which is just handling database queries and mutations.
Some caveats: server actions only work in server components, so you will have to import them or pass them as props to client components if you want to add interactivity. Server actions only support POST requests, which is primarily used for data mutations. They can be used for data-fetching, but that's not the best choice. Instead, do that directly in the server component.
Note that there might be some old code that uses the tRPC
library. That can be safely ignored; we're not using tRPC
because we don't need the extra features it provides.
Websockets provide persistent, asynchronous, bidirectional communication between the client and server. It is asynchronous because the client can send messages to the server at any time, and the server can send messages to the client at any time. This is useful for real-time applications like chat apps, multiplayer games, and collaborative tools.
It's nice to have websockets to sync data between different team members. For example, if one person makes a guess, we want to update the page of everyone else on the team immediately. But historically, websockets have been the root of all of our problems. It probably comes down to how Django handles websockets, but we're going to try to avoid them entirely.
If we really want websockets, we need to either host it on another server, use a websocket provider, or use a real-time database. Vercel does not support websockets because it is serverless.
Streaming is a persistent, asynchronous, unidirectional technique for sending data to the client in real-time. This is useful for real-time applications like chat apps, multiplayer games, and collaborative tools.
This is probably the best way for us to sync guesses between different team members. More information about how Vercel handles SSEs here. This is not high on the priority list, but it would be nice to have.
Server components handle static data fetching and rendering. For example, the leaderboard page is a server component. Client components handle user interactions such as form submissions and client-side events. The guess form is a client component. Note that these can be combined together, as in the puzzle page.