Here I discuss how I built a full-stack Geoguessr clone
Technologies used
- Next.js
- Tailwind
- Google Maps Javascript SDK and @googlemaps/react-wrapper to play nicely with React
- Prisma (for schema setup only - ORM not used)
- Planetscale (MySQL) and @planetscale/database serverless driver
- tRPC (running on the edge)
- Clerk for authentication
Basic idea
I started off just wanting to let the user play the game without worrying about logging them in or anything like that. For this, the user goes to /game
and they are presented with the familiar:
The user may then add panoramas of their choice to the map, by visiting the /map-maker
route:
Architecture
The architecture is pretty simple:
- The Next.js app is deployed to Vercel
- Edge functions running on Vercel are used to serve the tRPC API
- Authentication is handled by Clerk (I used the cloud/managed version)
- Planetscale is used to manage the MySQL database
Google Maps
In order to use Google Maps, I needed to create a Google Cloud project and enable the Maps Javascript API. I then created an API key and added it to the environment variables in Vercel.
Prisma schema
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_URL")
relationMode = "prisma"
}
model Map {
id Int @id @default(autoincrement())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
MapPanorama MapPanorama[]
Challenge Challenge[]
userId String
User User @relation(fields: [userId], references: [id])
@@index([userId])
}
model User {
id String @id
email String @unique
createdAt DateTime @default(now())
Challenge Challenge[]
Map Map[]
ChallengeGame ChallengeGame[]
}
model MapPanorama {
id Int @id @default(autoincrement())
panoramaId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Map Map @relation(fields: [mapId], references: [id])
mapId Int
@@index([mapId])
@@index([panoramaId])
}
model Challenge {
id Int @id @default(autoincrement())
url String @unique
panorama1 String?
panorama2 String?
panorama3 String?
panorama4 String?
panorama5 String?
createdAt DateTime @default(now())
Map Map @relation(fields: [mapId], references: [id])
mapId Int
userId String
User User @relation(fields: [userId], references: [id])
ChallengeGame ChallengeGame[]
@@index([mapId])
@@index([userId])
@@index([url])
}
model ChallengeGame {
id Int @id @default(autoincrement())
challengeUrl String
Challenge Challenge @relation(fields: [challengeUrl], references: [url])
userId String
User User @relation(fields: [userId], references: [id])
guess1 Json?
guess2 Json?
guess3 Json?
guess4 Json?
guess5 Json?
createdAt DateTime @default(now())
currentRound Int @default(0)
@@index([challengeUrl])
@@index([userId])
}
YouTube Video
You can embed YouTube videos in your blog posts.
Tweet
You can embed tweets in your blog posts.
CodePen
You can embed codepens in your blog posts.
Ecosystem - Pen in CSS by Ruphaa
GitHub Gist
You can embed GitHub gists in your blog posts.
Lesser Known HTML Elements
abbr
GIF is a bitmap image format.
sub
H2O
sup
Xn + Yn = Zn
kbd
Press CTRL+ALT+Delete to end the session.
mark
Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures.