The nightlife-vibe web frontend for the TorontoNights knowledge engine.
It is a completely separate app from the tn/ engine — it never touches
the database. All data comes from the engine's read-only HTTP API.
Stack: Next.js 16 (App Router) · React 19 · TypeScript · Tailwind v4 · Framer Motion · Zod.
# 1. Start the tn engine (separate terminal, in the tn/ folder)
cd ..\tn
.\.venv\Scripts\Activate.ps1
tn serve # http://localhost:8000
# 2. In this folder — install and configure
cd ..\tn-web
npm install
Copy-Item .env.local.example .env.local # already committed by default
# 3. Run
npm run dev # http://localhost:3000Browser ──▶ Next.js Route Handler (/api/*) ──▶ tn engine (localhost:8000)
(server-side only)
- The browser never talks to the engine directly.
TN_API_URLlives in.env.localand is only read on the server.- Every request has a hard timeout (
TN_API_TIMEOUT_MS, default 8s) so a stalled engine cannot hang a page render. - Upstream errors are translated to safe generic messages before reaching
the browser (see
src/lib/api/http.ts).
src/
├── app/ Next.js App Router
│ ├── api/ Server-side proxy to the tn engine
│ │ └── media/images/ Streams venue photos from the engine with long-lived cache headers
│ ├── venues/[id]/ Venue detail page
│ ├── search/ Explore / filter page
│ ├── events/ Upcoming events page
│ ├── layout.tsx Root layout — theme, fonts, nav, footer
│ ├── page.tsx Home ("Where should we go?")
│ ├── loading.tsx Route-level Suspense fallback
│ ├── error.tsx Route-level error boundary
│ └── not-found.tsx
├── components/
│ ├── ui/ Primitives (Button, Card, Input, Badge, …)
│ ├── layout/ Navbar, Footer
│ ├── home/ Hero, SearchBar, LocationButton, HomeExplorer
│ ├── venues/ VenueCard, VenueImage, VenueSheet, VenueHours, VenueTrust
│ ├── nearby/ NearbyExplorer, NearbyVenueRow, RadiusSlider
│ └── map/ VenueMap (MapLibre), MapFilters
├── hooks/ useGeolocation, useVenueSheet
├── lib/
│ ├── api/ tn engine client + Route Handler helpers
│ ├── geo.ts haversine, venuesWithinRadius, formatDistance
│ ├── env.ts Zod-validated server env
│ └── utils.ts cn(), formatConfidence(), …
└── types/ Enums + wire types (mirrors tn schemas)
Every file is intentionally kept small and single-purpose.
Venue photos are stored on the engine's disk and streamed through Next:
Browser ──▶ /api/media/images/venues/<id>/<hash>.jpg ──▶ tn engine /media/images/…
(7-day cache headers, force-static)
<VenueImage> renders the first photo via next/image (unoptimized) if the
venue has one, otherwise falls back to a category-coloured gradient tile with
the category emoji. To populate the cache on the engine side:
tn images-fetch-google --max 3 --width 1600 (see the engine RUNBOOK).
- The home page auto-requests browser geolocation on mount.
- We show the "you are here" marker + radius ring only when the user granted permission and their coords fall inside the GTA bounding box.
- Anyone outside Toronto (or who denies / times out) sees a plain "Around downtown" view with every Toronto venue on the map — no radius filter, no radius slider.
- The radius slider defaults to 1 km and caps at 10 km.
- Strong headers set in
next.config.ts(CSP, HSTS, X-Frame, Referrer, Permissions). TN_API_URLis a server-only env var —src/lib/env.tsthrows if any var is malformed at startup.- API client is wrapped with
import "server-only"so it can never be bundled into the client. - Every proxy route validates query params before forwarding.
Dark-first "nightlife" theme built with Tailwind v4 tokens in
src/app/globals.css:
--neon-pink,--neon-cyan,--neon-violet,--gold.surface-glass— frosted glass surface for cards/navbar.text-neon— gradient text used sparingly on hero headlines- Motion respects
prefers-reduced-motion.