# Project export: snap & seek

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: a cross between hide and seek, a scanvenger hunt, and jet lag the game, "snap & seek"'s a whimsical outdoor games with friends!
- Devpost: https://devpost.com/software/snap-seek-ymxtkl
- GitHub: https://github.com/SuperZooper3/snap-and-seek
- Demo: https://snap-and-seek-game.vercel.app/
- Video: https://www.youtube.com/embed/aRB67gLhyjE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — SuperZooper3 (24 commits), Victor Huang (20 commits)

## Devpost submission (written by the team)

### Overview

Watch a longer version of our game demo playtest here: https://youtu.be/F_hVs6-mK4M About Snap and Seek Snap and Seek is a whimsical real-world mobile game that combines the thrill of hide and seek with the excitement of scavenger hunts and geocaching. Designed for outdoor play, it encourages players to explore their surroundings (like the amazing Stanford campus) while engaging in a fun, competitive experience with lots of running around and touching grass! We designed it esthetically to be super fun and playful, and we made it accessible to new players! It was inspired by Jet Lag: The Game, a great YouTube show. Game Concept Players split into teams, and are tasked with choosing a a hidden location. The goal of the game is to find all the other teams' hidden spots using landmark photos relative to the location, and proximity clues. The first team to match every other team's hidden photos wins! Abilities Radar: Narrow down locations with questions like "Are you within 100 meters of me?" Thermometer: By comparing two of your positions, you can see if you're colder or hotter to the location Photo Landmark: Unlock different photos taken at the hidden location to reveal more context of the location, like the tallest visible tower, or the nearest tree. Technical Features This was our first time working with Vercel! It was super fun and easy to get started with, and we're so proud to have gotten our first app deployed, because you can't make a multiplayer online game run on localhost lol. Shipping was so fast that we had multiple fun playtests with our friends throughout TreeHacks! And from there, we're proud to announce that we've just launched the full app publicly to any number of players! You can play with your own friends at https://snap-and-seek-game.vercel.app/. We explain more there. So in this sense, we're already a product ;) We see you Neo. On other technical standpoints, we did lots of optimization for GPS tracking on phones, and just making the design nice and responsive. Things also work in real time via lots of updates so that people never have to reload tabs, and we worked a lot with the google maps api to overlay your current location, hiding zones, and information gained from hints!

## README (from the GitHub repository)

# Snap and Seek

<img width="1620" height="1080" alt="MockupViews_1x_PNG_20260215_515" src="https://github.com/user-attachments/assets/11a08cad-4db7-429e-987d-0f272e103f6b" />

## About Snap and Seek

**Hide. Seek. Snap. Find them all.**

*play free at [snap-and-seek-game.vercel.app](https://snap-and-seek-game.vercel.app/)*

Snap and Seek is a whimsical real-world mobile game that combines the thrill of hide and seek with the excitement of scavenger hunts and geocaching. Designed for outdoor play, it encourages players to explore their surroundings (like the amazing Stanford campus) while engaging in a fun, competitive experience with lots of running around and touching grass!

### Game concept

Players split into teams and are tasked with choosing a hidden location. The goal is to find all the other teams' hidden spots using landmark photos relative to the location and proximity clues.

### Core mechanics / abilities

- **Radar:** Narrow down locations with questions like "Are you within 100 meters of me?"
- **Hot or Cold:** By comparing two of your positions, you can see if you're colder or hotter to the location.
- **Landmark:** Unlock different photos taken at the hidden location to reveal more context—like the tallest visible tower or the nearest tree.

### See it in action

[![Snap and Seek hype short](https://img.youtube.com/vi/aRB67gLhyjE/maxresdefault.jpg)](https://youtu.be/aRB67gLhyjE)

*Click to watch the hype short — or check the [full game walkthrough](https://youtu.be/F_hVs6-mK4M) on the website.*

## Quick start

```bash
npm install
npm run dev
```

Open [http://localhost:3000](http://localhost:3000).

## Setup

1. **Supabase** – Create a project, add the `games` table (see repo docs), then copy **Project URL** and **service_role** key into `.env.local`.
2. **Google Maps** – Enable Maps JavaScript API + Geocoding API, create an API key, add to `.env.local`.

Copy from `.env.example` and fill in:

- `NEXT_PUBLIC_SUPABASE_URL`
- `SUPABASE_SERVICE_ROLE_KEY`
- `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY`

Restart the dev server after editing env.

## Scripts

- `npm run dev` – dev server  
- `npm run build` – production build  
- `npm run start` – run production build  


## Detected evidence (automated analysis)

Indexed codebase: 115 recognized source files, 470 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Cursor — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 125)

```
.cursor/00-default.mdc
.cursor/01-memory.mdc
.cursor/debug.log
.cursor/memory-bank/activeContext.md
.cursor/memory-bank/future-plans.md
.cursor/memory-bank/game-flow.md
.cursor/memory-bank/productContext.md
.cursor/memory-bank/progress.md
.cursor/memory-bank/project-overview.md
.cursor/memory-bank/projectbrief.md
.cursor/memory-bank/systemPatterns.md
.cursor/memory-bank/tech-stack.md
.cursor/memory-bank/techContext.md
.env.example
.gitignore
app/api/games/[gameId]/game-status/route.ts
app/api/games/[gameId]/hints/[hintId]/route.ts
app/api/games/[gameId]/hints/route.ts
app/api/games/[gameId]/lock-in/route.ts
app/api/games/[gameId]/photo-locations/route.ts
app/api/games/[gameId]/photo-unlock/route.ts
app/api/games/[gameId]/pings/latest/route.ts
app/api/games/[gameId]/pings/route.ts
app/api/games/[gameId]/players/[playerId]/route.ts
app/api/games/[gameId]/players/[playerId]/withdraw/route.ts
app/api/games/[gameId]/players/route.ts
app/api/games/[gameId]/radar/route.ts
app/api/games/[gameId]/readiness/route.ts
app/api/games/[gameId]/route.ts
app/api/games/[gameId]/submissions/route.ts
app/api/games/[gameId]/thermometer/route.ts
app/api/games/route.ts
app/api/stats/route.ts
app/api/upload/route.ts
app/CreateGameForm.tsx
app/debug/DebugModeClient.tsx
app/debug/page.tsx
app/games/[gameId]/capture/page.tsx
app/games/[gameId]/GameActions.tsx
app/games/[gameId]/GamePageRefresh.tsx
app/games/[gameId]/GameZoneModal.tsx
app/games/[gameId]/god/GodMapView.tsx
app/games/[gameId]/god/GodMapWithPings.tsx
app/games/[gameId]/god/GodPhotoTray.tsx
app/games/[gameId]/god/page.tsx
app/games/[gameId]/not-found.tsx
app/games/[gameId]/page.tsx
app/games/[gameId]/PlayerList.tsx
app/games/[gameId]/seeking/CastingTimer.tsx
app/games/[gameId]/seeking/page.tsx
app/games/[gameId]/seeking/PhotoPowerup.tsx
app/games/[gameId]/seeking/PowerupTabs.tsx
app/games/[gameId]/seeking/RadarPowerup.tsx
app/games/[gameId]/seeking/SeekingLayout.tsx
app/games/[gameId]/seeking/SeekingTimer.tsx
app/games/[gameId]/seeking/ThermometerPowerup.tsx
app/games/[gameId]/setup/page.tsx
app/games/[gameId]/setup/SetupClient.tsx
app/games/[gameId]/summary/page.tsx
app/games/[gameId]/summary/SummaryGrid.tsx
app/games/[gameId]/waiting/page.tsx
app/games/[gameId]/waiting/WaitingClient.tsx
app/games/[gameId]/zone/HidingLayout.tsx
app/games/[gameId]/zone/HidingTimeRemaining.tsx
app/games/[gameId]/zone/page.tsx
app/games/[gameId]/zone/ZoneMapView.tsx
app/games/[gameId]/zone/ZoneWithLocation.tsx
app/games/new/page.tsx
app/globals.css
app/join/[gameId]/JoinForm.tsx
app/join/[gameId]/JoinWithTutorial.tsx
app/join/[gameId]/page.tsx
app/join/[gameId]/RejoinPlayerList.tsx
app/layout.tsx
app/not-found.tsx
app/page.tsx
app/robots.ts
app/sitemap.ts
app/SwRegistration.tsx
app/your-games/page.tsx
components/BackArrowIcon.tsx
components/CameraCapture.tsx
components/CameraModal.tsx
components/GameTutorial.tsx
components/HomeStats.tsx
components/ItemBar.tsx
components/TutorialModal.tsx
docs/game-management-plan.md
docs/google-maps-in-app.md
docs/security-and-design-notes.md
docs/supabase-game-zone.sql
docs/supabase-hints-table-fix.sql
docs/supabase-hints-table.sql
docs/supabase-photos-accuracy.sql
docs/supabase-pings.sql
docs/supabase-player-photos.sql
docs/supabase-players-withdrawn.sql
docs/supabase-schema-changes.sql
docs/supabase-storage-bucket-check.sql
docs/supabase-submissions.sql
docs/supabase-thermometer-threshold.sql
docs/supabase-unavailable-hint-photos.sql
docs/supabase-winner-ids.sql
eslint.config.mjs
lib/debug-location-cookie.ts
lib/game-config.ts
lib/game-win-check.ts
lib/geo.ts
lib/get-location.ts
lib/google-maps-loader.ts
lib/map-utils.ts
lib/player-cookie.ts
lib/supabase.ts
lib/tutorial-cookie.ts
lib/types.ts
lib/your-games-cookie.ts
next.config.ts
package.json
postcss.config.mjs
public/favicon/site.webmanifest
[5 more files omitted for size]
```

### Dependencies

- package.json: @react-google-maps/api@^2.20.8, @supabase/supabase-js@^2.95.3, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.1.6, next@16.1.6, react@19.2.3, react-dom@19.2.3, tailwindcss@^4, typescript@5.9.3

### Recent commits (newest first)

- final fixes
- fix build
- fix withdrawl win logic
- minor copy update
- MERGE POST EVENT AND DO ONE MORE TEST remove insecure endpoints listing games and files and stuff. (#9)
- extra copy about courtesy and location
- withdraw from game button in case someone is bugged
- gps accuracy check and enforicng them having it, and proximity check
- click on image to zoom
- fix vercel
- chore: seeking page no mroe scroll
- more tutorial links and minor styling fixes
- add number of games played and images takena
- remove google maps random stuffis
- remove thermeoter pins in map preview
- chore: blue area description
- radar behaviour consistnecy and theroemorter preview
- bisector line for thermoemter question
- update video in readme
- add gameplay example

## Key source files (fetched from GitHub, selected and truncated for size)

### snap-and-seek.md

```markdown
# Snap and Seek

**Hide. Seek. Snap. Find them all.**

A mobile-first multiplayer game that combines hide-and-seek with scavenger hunting and geocaching.

---

## Game Concept

Traditional hide-and-seek has problems:
- Sequential gameplay (wait for your turn)
- Hider gets bored sitting still
- Finding someone ends the game for them

**Snap and Seek fixes this:**
- ✅ Simultaneous gameplay - everyone plays at once
- ✅ Teams hide *locations*, not themselves
- ✅ Computer handles the "questioning" instead of the hider
- ✅ Race to find ALL spots, not just one

---

## How It Works

### Phase 1: Hide (Photo Setup)
Each team finds a spot within the game boundary and takes a photo. This photo becomes their "hidden object."

### Phase 2: Seek (Active Gameplay)
All teams can now see each other's photos and must race to physically find those locations using:
- **GPS tracking** - Walk around the game area
- **Question system** - Ask proximity questions:
  - "Am I within 100 meters?"
  - "Hotter or colder from my last position?"
  - "Show me a hint"
- **Map view** - See game boundary, your position, and found targets

### Phase 3: Victory
First team to physically reach all other teams' hidden spots wins!

---

## Core Mechanics

### 🗺️ Location & GPS
- Set game center coordinate (lat/lng)
- Define radius (e.g., 100m around a building)
- Real-time GPS tracking of all players
- Automatic detection when you reach a target location

### 📸 Photo Capture
- Take photo at your hidden spot
- GPS coordinates attached automatically
- Add optional hints for other teams
- Preview and retake before locking in

### ❓ Question System
Instead of asking the hider "am I close?", the app answers:
- **Proximity checks**: "Within 50m?", "Within 100m?"
- **Temperature feedback**: "Hotter" or "Colder" since last query
- **Hints**: Request additional photos or text clues

### 🎮 Optional Game Modes

**Player Tagging:**
- Tag opponents when within 5 meters
- Tagged players get time penalty or temporary freeze

**Hunter Mode:**
- Temporary GPS visibility between hunter/hunted teams
- Chase mechanics with cooldowns

**Power-Ups:**
- Unlocked by completing physical tasks:
  - Walk 200 meters → unlock Radar
  - Run at 2.5 m/s → unlock Speed Boost
- Examples: Extra Hint, Range Boost, Tagging Shield

---

## Technical Architecture

### Client (Mobile App)
- **Platforms**: iOS/Android via mobile web browser
- **Key screens**:
  - Auth/onboarding
  - Lobby (create/join with game code)
  - Game setup (hide your spot)
  - Active play (map + questions)
  - Results (winner + stats)

### Backend
- **Game server**: Next.js API routes
- **Real-time service**: WebSocket or polling
- **Storage**: Photos, game state, player positions

### Data Model
Core entities:
- **Users** - Player profiles (username, stats)
- **Games** - Game sessions (center, radius, status, join code)
- **Teams** - Team assignments within games
- **Targets** - Hidden spots with photos and GPS coordinates
- **PlayerStates** - Rea
[truncated — 2795 more characters]
```

### docs/security-and-design-notes.md

```markdown
# Security and design notes (publish audit)

Notes from the pre-publish security audit. Keep these in mind when changing APIs or Supabase config.

---

## Changes made (implementation summary)

- **Removed test-upload:** Deleted `app/test-upload/page.tsx` and `app/test-upload/CameraCapture.tsx`. Deleted `app/api/photos/route.ts` (GET was the only handler; test-upload was the only consumer).
- **Removed location-test:** Deleted `app/location-test/page.tsx`, `app/location-test/MapDisplay.tsx`, and `app/location-test/LocationDisplay.tsx`.
- **Removed “All games”:** Removed the GET handler from `app/api/games/route.ts` (kept POST for creating games). Deleted `app/games/page.tsx`. Updated `app/not-found.tsx` and `app/games/[gameId]/not-found.tsx` so the primary link is “Your games” with `href="/your-games"` instead of “All games” and `/games`.
- **robots.ts:** Removed `"/test-upload/"` and `"/location-test/"` from the `disallow` array.
- **Storage bucket doc:** Added `docs/supabase-storage-bucket-check.sql` with SQL to inspect policies on `storage.objects` and instructions for dropping an anon SELECT policy if one exists. Linked to it from the Storage bucket section below.
- **Your games:** Left unchanged; `app/your-games/page.tsx` and the your-games flow are still the way users see games they created or joined.

---

## Discovery model

- **No index of games.** Users only see games they created or were invited to (join link or “Your games” cookie).
- **No global photo list.** No API returns all photos; photo access is only in the context of a game and a player (e.g. photo-unlock by `gameId` + `hiderId`).
- Game IDs are the secret: without a list endpoint, they’re only known to creators and people with the share link.

---

## Supabase

### Database (games, players, photos, etc.)

- We use a magic-link / share-link model with **no user auth**. All database access is via the backend **service role** (superkey) only; we do not use RLS (intentional). Access control is “what the Next.js API exposes.”
- For the “no index” model, no DB schema change was required; removing list endpoints was enough.
- We are not planning to add RLS or direct client access; the backend uses the superkey only. Games are only visible to people who have the link.

### Storage bucket (`snap-and-seek-image`)

- **Public read by path:** Keep the bucket public so direct object URLs (from `photos.url` / `getPublicUrl`) keep working.
- **No listing:** Ensure there is **no policy** that allows anon (or authenticated) users to **list** objects in the bucket. In Supabase Dashboard → Storage → `snap-and-seek-image`, confirm only the service role can list; anon should not have list permission.
- **Paths:** Uploads use unguessable paths (`timestamp` + random string); the only way to get paths was the removed global photos API, so the bucket is not dumpable.
- **Check policies:** See [docs/supabase-storage-bucket-check.sql](supabase-storage-bucket-check.sql) for SQL to inspect storage polici
[truncated — 4199 more characters]
```

### package.json

```
{
  "name": "snap-and-seek",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@react-google-maps/api": "^2.20.8",
    "@supabase/supabase-js": "^2.95.3",
    "next": "16.1.6",
    "react": "19.2.3",
    "react-dom": "19.2.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.6",
    "tailwindcss": "^4",
    "typescript": "5.9.3"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Patrick_Hand } from "next/font/google";
import "./globals.css";
import { SwRegistration } from "./SwRegistration";

const patrickHand = Patrick_Hand({
  weight: "400",
  variable: "--font-hand",
  subsets: ["latin"],
});

const baseUrl =
  process.env.VERCEL_URL
    ? `https://${process.env.VERCEL_URL}`
    : process.env.NEXT_PUBLIC_APP_URL ?? "https://snapandseek.com";

export const metadata: Metadata = {
  metadataBase: new URL(baseUrl),
  title: {
    default: "Snap and Seek — Hide. Seek. Snap. Find them all.",
    template: "%s | Snap and Seek",
  },
  description:
    "Snap and Seek is a whimsical outdoor game with friends: a cross between hide and seek, scavenger hunt, and Jet Lag. Create a game, hide or seek, snap photos, and find everyone.",
  keywords: [
    "Snap and Seek",
    "outdoor game",
    "hide and seek",
    "scavenger hunt",
    "party game",
    "location-based game",
    "photo game",
    "Jet Lag game",
    "play with friends",
  ],
  authors: [{ name: "Snap and Seek" }],
  creator: "Snap and Seek",
  openGraph: {
    type: "website",
    locale: "en_US",
    url: "/",
    siteName: "Snap and Seek",
    title: "Snap and Seek — Hide. Seek. Snap. Find them all.",
    description:
      "A whimsical outdoor game with friends: hide and seek meets scavenger hunt. Create a game, snap photos, find everyone.",
    images: [
      {
        url: "/favicon/android-chrome-512x512.png",
        width: 512,
        height: 512,
        alt: "Snap and Seek",
      },
    ],
  },
  twitter: {
    card: "summary_large_image",
    title: "Snap and Seek — Hide. Seek. Snap. Find them all.",
    description: "Outdoor game with friends: hide and seek meets scavenger hunt. Create a game, snap photos, find everyone.",
    images: ["/favicon/android-chrome-512x512.png"],
  },
  robots: {
    index: true,
    follow: true,
    googleBot: { index: true, follow: true },
  },
  alternates: { canonical: "./" },
  icons: {
    icon: [
      { url: "/favicon/favicon.ico", sizes: "any" },
      { url: "/favicon/favicon-16x16.png", sizes: "16x16", type: "image/png" },
      { url: "/favicon/favicon-32x32.png", sizes: "32x32", type: "image/png" },
    ],
    apple: "/favicon/apple-touch-icon.png",
  },
  manifest: "/favicon/site.webmanifest",
  appleWebApp: {
    capable: true,
    statusBarStyle: "default",
    title: "Snap and Seek",
  },
  other: {
    "apple-mobile-web-app-capable": "yes",
    "apple-mobile-web-app-status-bar-style": "default",
    "apple-mobile-web-app-title": "Snap and Seek",
  },
};

export const viewport = {
  width: "device-width",
  initialScale: 1,
  maximumScale: 5,
  viewportFit: "cover" as const,
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${patrickHand.variable} font-sans antialiased overflow-x-hidden`}
      >
        <SwRegistration />
        {children}
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
import Link from "next/link";
import Image from "next/image";
import CreateGameForm from "./CreateGameForm";
import { GameTutorial } from "@/components/GameTutorial";
import { HomeStats } from "@/components/HomeStats";

const baseUrl =
  process.env.VERCEL_URL
    ? `https://${process.env.VERCEL_URL}`
    : process.env.NEXT_PUBLIC_APP_URL ?? "https://snapandseek.com";

const jsonLd = {
  "@context": "https://schema.org",
  "@type": "WebApplication",
  name: "Snap and Seek",
  description:
    "A whimsical outdoor game with friends: a cross between hide and seek, scavenger hunt, and Jet Lag. Create a game, hide or seek, snap photos, and find everyone.",
  url: baseUrl,
  applicationCategory: "Game",
  offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
  featureList: [
    "Create games and invite friends",
    "Hide and seek with photo proof",
    "Location-based play outdoors",
    "Scavenger hunt style challenges",
  ],
};

export default function Home() {
  return (
    <div className="min-h-screen font-sans" style={{ background: "var(--background)" }}>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <main className="mx-auto max-w-2xl px-4 py-8 sm:px-6 sm:py-10">
        <header className="text-center mb-8">
          <div className="flex justify-center mb-4">
            <Image
              src="/ss_f_logo.png"
              alt="Snap and Seek"
              width={200}
              height={80}
              className="h-24 w-auto sm:h-[7.5rem]"
              style={{ objectFit: "contain" }}
              priority
            />
          </div>
          <p className="text-lg sm:text-xl max-w-xl mx-auto leading-relaxed" style={{ color: "var(--pastel-ink-muted)" }}>
            A cross between hide and seek, a scavenger hunt, and Jet Lag The Game, &ldquo;Snap & Seek&rdquo; is a whimsical outdoor game with friends!
          </p>
          <HomeStats />
        </header>

        <section className="mb-8">
          <CreateGameForm />
        </section>

        <div className="mb-8 text-center">
          <Link href="/your-games" className="btn-ghost inline-flex">
            Your games
          </Link>
        </div>

        <section className="mb-8">
          <GameTutorial showTitle={true} />
        </section>

        <section className="mb-8">
          <h2 className="text-xl font-bold mb-4 text-center" style={{ color: "var(--foreground)" }}>
            What is playing like?
          </h2>
          <div className="space-y-6">
            <div>
              <div className="relative w-full rounded-xl overflow-hidden border-[3px] shadow-lg" style={{ borderColor: "var(--pastel-border)", aspectRatio: "16/9" }}>
                <iframe
                  className="absolute inset-0 w-full h-full"
                  src="https://www.youtube.com/embed/aRB67gLhyjE?rel=0"
                  title="Snap and Seek hype short"
                  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
                  allowFullScreen
                />
              </div>
              <p className="text-center mt-2 text-sm" style={{ color: "var(--pastel-ink-muted)" }}>
                Hype trailer video for the vibe!
              </p>
            </div>
            <div>
              <div className="relative w-full rounded-xl overflow-hidden border-[3px] shadow-lg" style={{ borderColor: "var(--pastel-border)", aspectRatio: "16/9" }}>
                <iframe
                  className="absolute inset-0 w-full h-full"
                  src="https://www.youtube.com/embed/F_hVs6-mK4M?rel=0"
                  title="Snap and Seek full game walkthrough"
                  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
                  allowFullScreen
                />
              </div>
              <p className="text-center mt-2 text-sm" style={{ color: "var(--pastel-ink-muted)" }}>
                A 10 minute full game walkthrough to really understand how it's played!
              </p>
            </div>
          </div>
        </section>
      </main>
    </div>
  );
}

```

### app/debug/page.tsx

```typescript
import { DebugModeClient } from "./DebugModeClient";

export default function DebugPage() {
  return (
    <div className="min-h-screen font-sans" style={{ background: "var(--background)" }}>
      <DebugModeClient />
    </div>
  );
}

```

### app/your-games/page.tsx

```typescript
import Link from "next/link";
import { cookies } from "next/headers";
import { BackArrowIcon } from "@/components/BackArrowIcon";
import { supabase } from "@/lib/supabase";
import { parseYourGamesCookie, YOUR_GAMES_COOKIE_NAME } from "@/lib/your-games-cookie";
import type { Game } from "@/lib/types";

export const metadata = {
  title: "Your Games",
  description: "View and continue your Snap and Seek games.",
};

export default async function YourGamesPage() {
  const cookieStore = await cookies();
  const raw = cookieStore.get(YOUR_GAMES_COOKIE_NAME)?.value;
  const gameIds = parseYourGamesCookie(raw);

  let games: Game[] = [];
  if (gameIds.length > 0) {
    const { data, error } = await supabase
      .from("games")
      .select("*")
      .in("id", gameIds);

    if (!error && data && data.length > 0) {
      const byId = new Map((data as Game[]).map((g) => [g.id, g]));
      games = gameIds.map((id) => byId.get(id)).filter((g): g is Game => g != null);
    }
  }

  return (
    <div className="min-h-screen font-sans" style={{ background: "var(--background)" }}>
      <main className="mx-auto max-w-2xl px-6 py-16">
        <header className="mb-10">
          <Link href="/" className="btn-ghost inline-flex items-center gap-1.5">
            <BackArrowIcon />
            Create game
          </Link>
          <h1 className="mt-4 text-3xl font-bold" style={{ color: "var(--foreground)" }}>
            Your games
          </h1>
          <p className="mt-1 text-sm" style={{ color: "var(--pastel-ink-muted)" }}>
            Games you’ve created or joined. Reopen from here to rejoin.
          </p>
        </header>

        <section className="sketch-card p-6">
          <h2 className="text-xl font-bold mb-4" style={{ color: "var(--foreground)" }}>
            Your games
          </h2>
          {games.length === 0 && (
            <p style={{ color: "var(--pastel-ink-muted)" }}>
              You haven’t joined or created any games yet. Create a game above or use a join link to get started.
            </p>
          )}
          {games.length > 0 && (
            <ul className="space-y-3">
              {games.map((game) => (
                <li key={game.id}>
                  <Link
                    href={`/games/${game.id}`}
                    className="block p-4 rounded-xl border-[3px] transition-all hover:translate-x-0.5 hover:translate-y-0.5"
                    style={{
                      background: "var(--pastel-butter)",
                      borderColor: "var(--pastel-border)",
                      color: "var(--pastel-ink)",
                      boxShadow: "3px 3px 0 var(--pastel-border-subtle)",
                    }}
                  >
                    <span className="font-bold">{game.name || "Unnamed game"}</span>
                    <span
                      className="ml-2 inline-flex rounded-full px-2.5 py-0.5 text-xs font-bold border-2"
                      style={{
                        borderColor: "var(--pastel-border)",
                        background:
                          game.status === "completed"
                            ? "var(--pastel-mint)"
                            : game.status === "hiding"
                              ? "var(--pastel-mint)"
                              : game.status === "seeking"
                                ? "var(--pastel-sky)"
                                : "var(--pastel-peach)",
                        color: "var(--pastel-ink)",
                      }}
                    >
                      {game.status ?? "lobby"}
                    </span>
                  </Link>
                </li>
              ))}
            </ul>
          )}
        </section>
      </main>
    </div>
  );
}

```

### app/games/new/page.tsx

```typescript
import { redirect } from "next/navigation";

export default function NewGamePage() {
  redirect("/");
}

```

### app/api/games/route.ts

```typescript
import { NextResponse } from "next/server";
import { supabase } from "@/lib/supabase";
import { DEFAULT_HIDING_DURATION_SECONDS, DEFAULT_POWERUP_CASTING_SECONDS, DEFAULT_THERMOMETER_THRESHOLD_METERS } from "@/lib/game-config";

export async function POST(request: Request) {
  const body = await request.json();
  const name = typeof body?.name === "string" ? body.name.trim() : "";

  const { data, error } = await supabase
    .from("games")
    .insert({
      name: name || null,
      status: "lobby",
      hiding_duration_seconds: DEFAULT_HIDING_DURATION_SECONDS,
      powerup_casting_duration_seconds: DEFAULT_POWERUP_CASTING_SECONDS,
      thermometer_threshold_meters: DEFAULT_THERMOMETER_THRESHOLD_METERS,
    })
    .select("id, name, status, created_at")
    .single();

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
  return NextResponse.json(data);
}

```

### app/api/stats/route.ts

```typescript
import { NextResponse } from "next/server";
import { supabase } from "@/lib/supabase";

/**
 * Public stats for the home page. Returns only aggregate counts;
 * does not expose games list or other data. Uses service role server-side only.
 */
export async function GET() {
  const [gamesResult, photosResult, playersResult] = await Promise.all([
    supabase.from("games").select("*", { count: "exact", head: true }),
    supabase.from("photos").select("*", { count: "exact", head: true }),
    supabase.from("players").select("*", { count: "exact", head: true }),
  ]);

  if (gamesResult.error) {
    return NextResponse.json(
      { error: gamesResult.error.message },
      { status: 500 }
    );
  }
  if (photosResult.error) {
    return NextResponse.json(
      { error: photosResult.error.message },
      { status: 500 }
    );
  }
  if (playersResult.error) {
    return NextResponse.json(
      { error: playersResult.error.message },
      { status: 500 }
    );
  }

  return NextResponse.json({
    gamesPlayed: gamesResult.count ?? 0,
    snapsTaken: photosResult.count ?? 0,
    playersJoined: playersResult.count ?? 0,
  });
}

```

[105 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]