# Project export: PinTheSlug

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: CruzHacks 2026
- Tagline: A geo-navigational game that helps students explore and learn about UCSC's beautifully complex campus!
- Devpost: https://devpost.com/software/pintheslug
- GitHub: https://github.com/kdelmo1/geo_slug
- Demo: http://pinslug.vercel.app/
- Video: https://www.youtube.com/embed/kwLn-IpYOc0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — omled1 (15 commits), WenXuan (10 commits), shawtybae817 (6 commits), Matthew Cendana (3 commits)

## Devpost submission (written by the team)

### Inspiration

. UCSC is a huge campus, yet most students only know a fraction of it. We wanted to encourage students to explore and test how well they really know UCSC and have them be able to connect location names to visual images. This was the driving inspiration for PinTheSlug.

### What it does

. PinTheSlug is a UCSC-only GeoGuessr-style game. Players are either presented a random Google Street View location or image of somewhere on (or close to) campus and must pin where they think they are. Scores are based on the distance of their guess from the actual location. Players can log-in with their UCSC Google Account to record and view their game history.

### How we built it

. Frontend: React + TypeScript with Vite. Maps: Google Maps JavaScript API & Street View. Auth & Database: Supabase (Google OAuth, PostgreSQL). Deployment: Vercel. We used a polygon representing the UCSC campus boundary and randomly generated coordinates within it, validating locations using Google Maps geometry utilities. Authentication is handled via UCSC Google login, and user data plus score history are stored in Supabase.

### Challenges we ran into

. Valid location generation: Ensuring randomly generated coordinates were actually inside campus and had available Street View imagery required careful retries and geometry checks. Authentication + database sync: Users appear automatically in Supabase Auth, but we had to design logic to create matching user records in our database only when needed. Merging refactors: We restructured the project mid-hackathon, which caused merge conflicts and deployment issues that took time to resolve. TypeScript strictness: Type errors during deployment forced us to be more precise with types, especially when working with external APIs.

### Accomplishments we're proud of

. Building a playable game with real Street View and static locations. Restricting access to UCSC students only via Google OAuth. Implementing accurate geospatial logic gameplay. Successfully deploying to Vercel. Coordinating and splitting up tasks among team members under time pressure, especially given that 3/4 of us are first-time hackers.

### What we learned

. How authentication systems like Supabase Auth interact with custom database schemas. Best practices for handling merges and refactors in team projects. How to work with geospatial data and Google Maps APIs at scale. The importance of designing for deployment early, not at the end.

### What's next

. Multiplayer. Game modes (timed, images only, street-view only, etc.). Community-uploaded images and fun facts to supply database. Leaderboards. Specific maps (dependent on size of community uploads).

## README (from the GitHub repository)

# PinTheSlug

🔗 **[Live Demo (Archived)](https://pinslug.vercel.app/)** | 🎥 [Video Demo of the Game](https://youtu.be/kwLn-IpYOc0)

> **Note:** Due to Google Maps API constraints, this site is no longer playable. This README highlights the features of the game as it existed during CruzHacks 2026.

<img width="100%" alt="PinTheSlug Loading Screen" src="https://github.com/user-attachments/assets/d0d76b9c-6eef-4dd4-b4ff-746780c22b47" />

---

## Overview

PinTheSlug is a web game heavily inspired by GeoGuessr, where users guess the locations of places on the UC Santa Cruz campus. While GeoGuessr supports custom maps and even offers Santa Cruz-specific options, we wanted to create a more personalized experience for UCSC students — complete with a playful Slug-themed UI, fun campus facts after each round, and a free-to-play format designed for students new to campus.

---

## Team & Contributions

Developed for **CruzHacks 2026** by a team of 4:

| Member | Role |
|---|---|
| **Delmo** | Implemented the Google Maps and Street View API, led frontend design and implementation, and deployed the app to Vercel. |
| **Wen** | Built out the backend architecture, focusing on user authentication and populating the users table on login, with additional contributions to frontend features. |
| **Matthew** | Designed and managed the data layer, populating Supabase with campus images and map coordinates, and implementing backend functions to store and manage game data. |
| **Angelica** | Designed the site's visual aesthetic and custom sprites, and contributed to project documentation. |

---

## Tech Stack

| Layer | Technology | Details |
|---|---|---|
| **Frontend** | React 19 + TypeScript 5.9 | Component-based UI with strict type safety |
| **Build Tool** | Vite 7 | Fast HMR dev server, `tsc -b` + Vite production build pipeline |
| **Maps** | Google Maps JS API + `@react-google-maps/api` | Street View panoramas, custom polygon boundary restricting guesses to UCSC campus bounds |
| **Backend / Database** | Supabase (`@supabase/supabase-js` v2) | PostgreSQL for game logs, user records, and campus location data; Auth via Supabase OAuth |
| **Authentication** | Supabase Auth + Google OAuth | Redirect-based OAuth flow with optional `@ucsc.edu` domain restriction |
| **Linting** | ESLint 9 + `typescript-eslint` | Enforced React Hooks rules and refresh-safe component exports |
| **AI Assistance** | Claude + Google Gemini | Used during development for code refinement and debugging |

---

## Architecture

```
src/
├── components/        # React UI components (map, scoreboard, fun facts)
├── lib/               # Supabase client config and helper functions
└── main.tsx           # App entry point

public/                # Static assets and campus images
```

**Data flow:** Campus location records (image + lat/lng coordinates) are stored in Supabase and fetched on game start. After each guess, the Haversine distance between the player's pin and the true coordinates is computed client-side to derive a score, which is then written back to Supabase along with a session ID for score history.

**Map boundary:** A GeoJSON polygon covering the UCSC campus is passed to the Google Maps API to restrict Street View and pin-drop interactions to campus bounds only.

**Auth flow:** Google OAuth is handled through Supabase Auth with a redirect callback URL. An optional email domain filter (`@ucsc.edu`) can be toggled in the Supabase dashboard to restrict signups to UCSC students.

---

## Game Structure

The game follows a single-round structure modeled after GeoGuessr:

- **5 locations** per game, all on the UCSC campus
- **Sign in with Google** to save progress and view past scores
- **Perfect score: 5,000 points** — the closer your guess to the actual location, the higher your score
- **Campus fun facts** appear after each guess, inspired by the LearnableMeta GeoGuessr plugin

### Leaderboard

<img width="100%" alt="Leaderboard after signing in" src="https://github.com/user-attachments/assets/111e8147-62c3-4669-9d7c-97130be68757" />

### Guessing a Location

<img width="100%" alt="Location 1 guessing" src="https://github.com/user-attachments/assets/f2225b46-83e5-450c-9736-3dc6d70590d6" />

### Results + Fun Fact

<img width="100%" alt="Location 1 results and fun fact" src="https://github.com/user-attachments/assets/df81746f-f4dd-4cf9-8fab-774128a46150" />

### Google Street View Location

<img width="100%" alt="Google Street View location" src="https://github.com/user-attachments/assets/bd984d3d-555b-487a-8c58-f22a2cfa2e70" />

### Ending Screen

<img width="100%" alt="Ending screen" src="https://github.com/user-attachments/assets/f8459f28-b30e-4228-b9b0-44c7a7e4e86c" />

---

## Challenges

- **Getting started** was the hardest part — we initially weren't sure we'd be able to use the Google Maps API at all.
- **Persisting game data** in Supabase so users could view their score history required significant debugging.
- **Time pressure on day 2** — the final two hours were spent simultaneously finishing the Devpost submission, polishing UI, and adding more campus locations.

---

## Closing Thoughts

Although PinTheSlug is no longer live due to API cost constraints, we're proud to have shipped a fully functional game that other hackers and attendees actually played and enjoyed. This was our team's first time building and deploying something end-to-end together, which is an experience we're forever grateful for.

Shoutout to CruzHacks and GeoGuessr for the inspiration! 🐌


## Detected evidence (automated analysis)

Indexed codebase: 15 recognized source files, 63 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (20 of 20)

```
.gitignore
eslint.config.js
index.html
lib/supabaseClient.tsx
package.json
README.md
src/App.css
src/App.tsx
src/components/Auth.tsx
src/components/Home.tsx
src/components/Profile.tsx
src/game/Game.tsx
src/index.css
src/lib/supabase.ts
src/lib/supabaseClient.ts
src/main.tsx
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vite.config.ts
```

### Dependencies

- package.json: @eslint/js@^9.39.1, @googlemaps/js-api-loader@^2.0.2, @react-google-maps/api@^2.20.8, @supabase/supabase-js@^2.90.1, @types/node@^24.10.1, @types/react@^19.2.5, @types/react-dom@^19.2.3, @vitejs/plugin-react@^5.1.1, eslint@^9.39.1, eslint-plugin-react-hooks@^7.0.1, eslint-plugin-react-refresh@^0.4.24, globals@^16.5.0, react@^19.2.0, react-dom@^19.2.0, typescript@~5.9.3, typescript-eslint@^8.46.4, vite@^7.2.4

### Recent commits (newest first)

- Update README with gameplay images
- Revise README with project status and details
- Update README with install steps and configuration
- Fixed more game stuff
- Fixed more potential deployment issues
- Fixed potential deployment bugs
- Fixed minor changes
- Beta done
- Beta done
- Update README.md
- 
- Merge branch 'main' of github.com:kdelmo1/geo_slug
- 
- remove unused imports and references
- Merge branch 'main' of github.com:kdelmo1/geo_slug
- Added README
- Leaderboard fetching
- fixing score issue
- Merge branch 'main' of https://github.com/kdelmo1/geo_slug
- test

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

### package.json

```
{
  "name": "my-geoguessr",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@googlemaps/js-api-loader": "^2.0.2",
    "@react-google-maps/api": "^2.20.8",
    "@supabase/supabase-js": "^2.90.1",
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.1",
    "@types/node": "^24.10.1",
    "@types/react": "^19.2.5",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^5.1.1",
    "eslint": "^9.39.1",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-react-refresh": "^0.4.24",
    "globals": "^16.5.0",
    "typescript": "~5.9.3",
    "typescript-eslint": "^8.46.4",
    "vite": "^7.2.4"
  }
}

```

### src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### src/App.tsx

```typescript
import { useState } from "react";
import Home from "./components/Home";
import Game from "./game/Game";

export default function App() {
  const [playing, setPlaying] = useState(false);

  return (
    <>
      {playing ? (
        <Game onExit={() => setPlaying(false)} />
      ) : (
        <Home onPlay={() => setPlaying(true)} />
      )}
    </>
  );
}

```

### vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    strictPort: true
  },
})



```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/png" href="/cursors/arrow.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>PinTheSlug</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

### eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
  },
])

```

### src/App.css

```css
body, html, #root {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

/* Fix for invisible Google Maps tiles */
.gm-style img {
  max-width: none !important;
  height: auto !important;
}
```

### src/index.css

```css
@font-face {
  font-family: 'miamiwriting'; /* You can name this whatever you want */
  src: url('/fonts/miamiwriting.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
}

:root {
  font-family: 'miamiwriting', system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  background-color: #fccf04 !important;
  color: #213547;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  /* === 1. GLOBAL DEFAULT (Normal Arrow) === */
  /* Hotspot: 0 0 (Top-Left tip) */
  cursor: url('/cursors/arrow.png') 0 0, auto;
}

/* body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
  background-color: #fccf04 !important;
}

#root {
  width: 100%;
  height: 100%;
  background-color: #fccf04 !important;
} */

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
  
  /* === CUSTOM BACKGROUND SETUP === */
  /* 1. The Image */
  background-image: url('/background.jpg'); /* Put your file in public/ folder */
  
  /* 2. Scaling Behavior */
  background-size: cover;    /* Zooms image to fill screen; crops edges if needed */
  background-position: center; /* Keeps the center of the image visible */
  background-repeat: no-repeat; /* Prevents tiling */
  background-attachment: fixed; /* Optional: Keeps background still while scrolling */
}

/* Update #root so it doesn't block the background with a solid color */
#root {
  width: 100%;
  height: 100%;
  /* background-color: #fccf04 !important; <--- REMOVE OR COMMENT THIS OUT */
}

/* Fix for invisible Google Maps tiles */
.gm-style img {
  max-width: none !important;
  height: auto !important;
}

/* =========================================
   CUSTOM CURSOR CLASSES
   (Assumes 64x64px images)
   ========================================= */

/* 2. POINTER (Index Finger) */
/* Used for buttons, links, and map controls */
/* Hotspot: 22 5 (Adjust this if the finger tip is in a different spot) */
button, a, [role="button"], .pointer-cursor {
  cursor: url('/cursors/arrow.png') 6 5, pointer !important;
}

/* 3. OPEN HAND (Idle / Hovering) */
/* Used for hovering over the map or draggable headers */
/* Hotspot: 32 32 (Center) */
.grab-cursor {
  cursor: url('/cursors/arrow.png') 32 32, grab !important;
}

/* 4. CLOSED HAND (Dragging / Grabbing) */
/* Used while actively clicking and dragging */
/* Hotspot: 32 32 (Center) */
.grabbing-cursor {
  cursor: url('/cursors/closed-hand.png') 32 32, grabbing !important;
}

/* 5. FOUR ARROW (Move / Pan) */
/* Can be used for specific move indicators */
/* Hotspot: 32 32 (Center) */
.move-cursor {
  cursor: url('/cursors/four.png') 32 32, move !important;
}

/* 6. RESIZE ARROW (Diagonal Down-Right) */
/* Used for the resize handle in the corner */
/* Hotspot: 32 32 (Center) */
.resize-cursor, ::-webkit-resizer {
  cursor: url('/cursors/resize.png') 32 32, nwse-resize !important;
}

/* =========================================
   GOOGLE MAPS OVERRIDES
   (Forces the map to use your custom icons)
   ========================================= */

/* Force "Open Hand" on the Map Canvas (Idle) */
.gm-style canvas {
  cursor: url('/cursors/arrow.png') 32 32, grab !important;
}

/* Force "Closed Hand" when dragging the Map */
.gm-style canvas:active {
  cursor: url('/cursors/closed-hand.png') 32 32, grabbing !important;
}

/* Force "Pointer Finger" on Map Controls (Zoom buttons, Pegman, etc.) */
.gm-style-pbc, 
.gmnoprint, 
.gm-control-active, 
.gm-svpc {
  cursor: url('/cursors/arrow.png') 6 5, pointer !important;
}

/* === CUSTOM GUESS BUTTON === */

.guess-button-wrapper {
  position: relative; /* Allows absolute positioning of text/glow inside */
  width: 100%;
  border: none;
  background: none;
  padding: 0;
  transition: transform 0.1s ease;
  user-select: none;
  /* Use your custom pointer cursor */
  cursor: url('/cursors/arrow.png') 6 5, pointer;
}

/* Interaction: Press effect */
.guess-button-wrapper:active:not(:disabled) {
  transform: scale(0.98);
}

/* Interaction: Disabled state */
.guess-button-wrapper:disabled {
  filter: grayscale(1) opacity(0.6);
  cursor: not-allowed !important;
}

.guess-btn-img {
  width: 100%;
  display: block;
  /* Optional: Adds a subtle shadow to the button image itself */
  filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));
}

/* THE GLOW OVERLAY */
.guess-btn-glow {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  
  background-color: rgba(76, 175, 80, 0.4); /* Green tint for Play */
  background: radial-gradient(circle, rgba(100, 255, 100, 0.5) 0%, rgba(76, 175, 80, 0.4) 100%);

  opacity: 0; /* Hidden by default */
  transition: all 0.3s ease;
  pointer-events: none;
  
  /* 2. ENSURE IT COVERS CORNERS */
  /* If your button image has rounded corners, match them here. 
     Otherwise, the glow might spill out the corners. */
  border-radius: 12px; 
  z-index: 1; 
}

/* Show glow on hover (if not disabled) */
.guess-button-wrapper:hover:not(:disabled) .guess-btn-glow {
  opacity: 1;
  
  /* 3. OUTER GLOW (The "Radiating" effect) */
  /* This creates a fuzzy orange light around the outside of the button */
  box-shadow: 0 0 30px 10px rgba(76, 175, 80, 0.6); /* Green outer glow */
}

/* THE TEXT */
.guess-btn-text {
  position: absolute;
  top: 50%;
  left: 50%;
  /* Perfectly center the text */
  transform: translate(-50%, -50%);
  z-index: 2;
  
  /* Typography */
  font-family: 'miamiwriting', sans-serif; /* Your custom font */
  font-size: 28px; /* Adjust size to fit your button */
  color: white; 
  text-transform: uppercase;
  letter-spacing: 2px;
  
  /* Outline for readability */
  text-shadow: 
    -2px -2px 0 #000,  
     2px -2px 0 #000,
    -2px  2px 0 #000,
     2px  2px 0 #000;
     
  pointer-events: none;
}

/* === SCROLLING LEADERBOARD ANIMATION === */
@keyframes scrollUp {
  0% { transform: translateY(0); }
  100% { transfo
[truncated — 4856 more characters]
```

### src/lib/supabaseClient.ts

```typescript
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL as string;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY as string;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

```

### src/lib/supabase.ts

```typescript
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

if (!supabaseUrl || !supabaseAnonKey) {
  throw new Error('Missing Supabase environment variables');
}

export const supabase = createClient(supabaseUrl, supabaseAnonKey);
```

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