Guess where they're from. A geography guessing game where you identify a person's origin based on their photo โ powered by Leaflet maps, PostGIS scoring, and Supabase.
Think GeoGuessr, but for people and cultures.
| Feature | Description |
|---|---|
| ๐ฎ Core Game | View a person's image, click a world map to guess their origin |
| ๐ Haversine Scoring | Exponential decay scoring (max 5000 pts) based on distance accuracy |
| ๐ Global Leaderboard | Top 50 players, auto-aggregated scores, medal rankings |
| ๐ GitHub OAuth | One-click sign-in via GitHub โ no passwords needed |
| ๐ ๏ธ Admin Panel | Create levels: upload images, pick locations on map, set difficulty |
| ๐บ๏ธ Leaflet + OpenStreetMap | Interactive maps for both guessing and admin location selection |
| ๐ก๏ธ Anti-Cheat | Rate limiting (15 guesses/min), unique constraint (1 guess/level/user) |
| ๐ PostGIS | Spatial geography types, spatial indexes, precise coordinate storage |
| ๐ Row Level Security | All tables protected with Supabase RLS policies |
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript |
| Styling | TailwindCSS v4 |
| Maps | Leaflet + OpenStreetMap |
| Database | PostgreSQL + PostGIS (via Supabase) |
| Auth | Supabase Auth (GitHub OAuth) |
| Storage | Supabase Storage (level images) |
| Deployment | Vercel / any Node.js host |
src/
โโโ app/
โ โโโ layout.tsx # Root layout (Inter font, dark theme)
โ โโโ page.tsx # Landing page (hero + features)
โ โโโ globals.css # Tailwind + custom dark styles
โ โโโ login/page.tsx # GitHub OAuth sign-in
โ โโโ game/page.tsx # Game page (random unplayed level)
โ โโโ leaderboard/page.tsx # Global leaderboard (top 50, ISR)
โ โโโ admin/page.tsx # Admin panel (role-gated)
โ โโโ auth/
โ โ โโโ callback/route.ts # OAuth code exchange
โ โ โโโ signout/route.ts # Sign out handler
โ โโโ api/
โ โโโ guess/route.ts # Guess submission API
โ โโโ admin/levels/route.ts # Level CRUD API (admin only)
โโโ components/
โ โโโ Navbar.tsx # Navigation with auth state
โ โโโ GameMap.tsx # Leaflet map for guessing
โ โโโ GameClient.tsx # Game UI (image, controls, score)
โ โโโ AdminMap.tsx # Leaflet map for admin location picker
โ โโโ AdminClient.tsx # Admin UI (level creation + management)
โโโ lib/
โ โโโ supabase/
โ โ โโโ client.ts # Browser Supabase client
โ โ โโโ server.ts # Server Supabase client
โ โ โโโ middleware.ts # Session refresh + route protection
โ โโโ scoring.ts # Haversine distance + score calculation
โโโ types/
โ โโโ database.ts # TypeScript interfaces
โโโ middleware.ts # Root middleware
supabase/
โโโ schema.sql # Full database schema + triggers
- Node.js 18+
- npm 9+
- A Supabase project (free tier works)
- A GitHub OAuth App
git clone https://github.com/TAG-IIIT/Ethnoguessr.git
cd Ethnoguessr
npm installCopy the example and fill in your Supabase credentials:
cp .env.local.example .env.localNEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-keyFind these in Supabase Dashboard โ Settings โ API
Run supabase/schema.sql in Supabase Dashboard โ SQL Editor. This creates:
profilesโ user profiles (auto-created on GitHub login)levelsโ game levels with PostGIS locationsguessesโ user guesses with distance/scoreleaderboardโ auto-aggregated scores via trigger
Then create the coordinate extraction function:
create or replace function get_level_coords(p_level_id uuid)
returns table(lat double precision, lng double precision)
language sql security definer
as $$
select
ST_Y(location::geometry) as lat,
ST_X(location::geometry) as lng
from public.levels
where id = p_level_id and is_active = true;
$$;- Create a GitHub OAuth App at github.com/settings/developers
- Homepage URL:
http://localhost:3000 - Callback URL:
https://<your-supabase-url>/auth/v1/callback
- Homepage URL:
- Enable GitHub in Supabase Dashboard โ Authentication โ Providers
- Paste the Client ID and Client Secret
- Go to Supabase Dashboard โ Storage
- Create a bucket named
level-images(set to Public) - Add storage policies (run in SQL Editor):
create policy "Allow authenticated uploads"
on storage.objects for insert to authenticated
with check (bucket_id = 'level-images');
create policy "Allow public reads"
on storage.objects for select to public
using (bucket_id = 'level-images');
create policy "Allow authenticated updates"
on storage.objects for update to authenticated
using (bucket_id = 'level-images');
create policy "Allow authenticated deletes"
on storage.objects for delete to authenticated
using (bucket_id = 'level-images');npm run devOpen http://localhost:3000 and sign in with GitHub.
After your first login, run in SQL Editor:
update public.profiles set role = 'admin' where username = 'YOUR_GITHUB_USERNAME';You'll now see the Admin link in the navbar.
Uses the Haversine formula to calculate great-circle distance, then applies exponential decay:
score = round(5000 ร e^(-distance_km / 2000))
| Distance | Score |
|---|---|
| 0 km (exact) | 5000 |
| 100 km | 4756 |
| 500 km | 3894 |
| 1000 km | 3033 |
| 2000 km | 1839 |
| 5000 km | 410 |
| 10000 km | 34 |
- Load โ Server fetches a random active level the user hasn't played
- View โ User sees the person's image (signed URL, 1hr expiry)
- Guess โ User clicks on the Leaflet map to place their guess
- Score โ API calculates Haversine distance and exponential decay score
- Result โ Shows distance, score bar, correct/guessed markers with dashed line
- Next โ Proceeds to the next unplayed level
erDiagram
auth_users ||--o| profiles : creates
profiles ||--o{ guesses : makes
profiles ||--o| leaderboard : aggregates
levels ||--o{ guesses : receives
profiles {
uuid id PK
text username UK
text avatar_url
text role
timestamptz created_at
}
levels {
uuid id PK
text image_url
geography location
text hint
text difficulty
boolean is_active
uuid created_by FK
timestamptz created_at
}
guesses {
uuid id PK
uuid user_id FK
uuid level_id FK
geography guessed_location
float distance_km
int score
timestamptz created_at
}
leaderboard {
uuid user_id PK
bigint total_score
int games_played
timestamptz updated_at
}
- Row Level Security (RLS) on all tables
- GitHub OAuth only โ no password storage
- Rate limiting โ 15 guesses per minute per user
- Unique constraint โ one guess per level per user
- Admin role check โ server-side verification for all admin operations
- Signed URLs โ level images use 1-hour expiry signed URLs
- Middleware โ session refresh + route protection on every request
MIT
Built with โค๏ธ using Next.js, Supabase, and Leaflet