Projects

ProjectWeb Development · 2024

Movie Ticket Booking Webapp

A hackathon movie booking app on Next.js and PostgreSQL, with a TMDB catalog, trailers, showtimes and a seat picker. Payments were out of scope.

  • Next.js
  • PostgreSQL
  • Drizzle
  • Recoil
  • AWS
  • Zod
  • Auth.js
  • ShadCN
  • AWS S3
  • Docker
  • Resend
Movie Ticket Booking Webapp interface

Web Development · Project

Chapter
Web products
Year
Availability
Open source
Last commit

QuickNix is a movie booking web app built for the StarHack hackathon. It lists what is showing using the TMDB API and lets you search it. A movie page carries the poster, the overview, the vote average and a theater and showtime picker. It is a hackathon build, and it stopped where the clock did.

The README says as much in its own words. Payments and the reservation system were never finished. Everything up to checkout works, and I would rather leave that written down than imply a flow that is not there.

# catalog rows come from TMDB, not a fixtureGET /api/v1/movies?query=inter&limit=10   3 resultsGET /movies/9f2c1a   vote average 8.4, 2 genresGET /buytickets   showtime 7:30 PMseat picker: executive-D8, executive-D9selection is component state, total 560checkout: no route writes a reservation row
Movie Ticket Booking Webapp in use

How the catalog fills

An administrator pastes an IMDb id into the add-movie form, and actions/tmdb.js calls the TMDB find endpoint with external_source=imdb_id. The first match is flattened into the shape of the movies table, with poster and backdrop paths rewritten onto the w500 image host. GET /api/v1/movies then reads that table through Drizzle, pages the result, sorts by releaseDate and matches titles with ilike. It also excludes rows whose poster path resolved to the literal /None, which is what TMDB hands back for a title with no artwork. The same route mirrors documents into a Typesense movies collection faceted on genre, language and vote average.

The seat picker

Choosing a seat is the part of a ticket flow anyone actually remembers, and it is the least real part here. components/app/buyTickets/SeatSelection.jsx renders from a seatData constant compiled into the component. Premium rows L, K and J cost 300 rupees, executive rows I down to C cost 280, and rows B and A cost 260. Selection is local useState keyed by a section-rowNumber string, and the total is a reduce over the selected ids. The theaters table does carry a seats JSONB column, defaulted to an empty array. Nothing reads it. The map on screen belongs to no theater in the database.

Five tables

lib/db/schema/ holds movies, theaters, showtimes, reservation and user, all Drizzle over Postgres on Neon. A showtime joins a movie to a theater and carries ticketPrice, startTime and endTime. A reservation holds its showtime, its user, seats as JSONB, a unique orderId, a total, customer name and phone, and a status of pending, confirmed or cancelled. Theaters store both address and seats as JSONB. Keeping seats in JSONB on two tables is the decision I would revisit first. It made a seat map easy to imagine under hackathon pressure, and it makes an honest availability query awkward.

Choices under a clock

  • Server actions call the application's own REST routes over HTTP through NEXT_PUBLIC_APP_URL instead of reading the database. One access shape then serves the pages and the Bruno collection in bruno/ alike. Every page load also pays a network hop back to itself.
  • Typesense sits beside Postgres rather than replacing the ilike query. Facets on genre and language become cheap. Two stores now have to stay in step, and only the write path in /api/v1/movies does that.
  • Authentication is Auth.js, and middleware.js is one re-export of it. Sign-in, sign-up and a gated admin panel came together quickly. The middleware checks no roles, so anything role-specific has to happen in the page itself.
  • Recoil is in the stack for client state, and lib/atoms/userDetailsAtom.js is what it holds. The seat picker never reached it and stayed on component state.

Running it locally

Setup is short, and pnpm dev binds port 3001 rather than 3000.

pnpm install
pnpm dev
curl "http://localhost:3001/api/v1/movies?query=inter&limit=10"

That last request is the quickest check that both halves of the catalog path worked. The environment needs DRIZZLE_DATABASE_URL for Neon, TMDB_API_KEY, AUTH_SECRET, the four TYPESENSE_ variables and NEXT_PUBLIC_APP_URL, since the server actions use that one to reach their own routes. Schema commands are pnpm db:generate, pnpm db:migrate and pnpm db:push.

Where it stopped

The payment page renders a checkout screen with a hardcoded film, a hardcoded price and three payment method radios. No gateway is wired behind it, and no code writes a row into reservation. Both gaps are listed under known issues in the README. pnpm db:seed is stale as well, since it inserts movie columns such as director and trailerUrl that the schema no longer has. The README advertises trailers, and no page ever rendered one. What does work is the catalog, search, movie detail, cinema list and admin panel. It runs in Docker behind a Caddy container proxying to app:3001, hosted on EC2, and is MIT licensed.

Next projectSearch Infrastructure

Typesense Dashboard

View project