After completing my work and studies a bit earlier yesterday, I had some free time on hand and since I haven't built any fun side projects since the last 2 months, I decided to build one before I went to sleep and surprisingly I ended up building a very fun little Pokémon guessing game using Supabase, Drizzle, & Next.js in roughly 2 hours! This blog post is not exactly a tutorial but more like a story of how I built the whole thing.
Here's the live app link if you wanna play around with it - https://pokeguesser-ten.vercel.app/
The plan
If you have ever watched Pokémon, you would know they had this guessing game after the interval where you would guess a Pokémon based on its shadow. I wanted to build something similar but since that can be a bit more tough, I decided to let users see the Pokémon and not just the shadow. Since, I wanted to finish this project faster, I also decided to let everyone have a global catch record instead of everyone having their own.
Setting up everything
Setting up the project was very easy, thanks to the amazing documentation these tools have!
-
Init a new Next.js project with app router and TailwindCSS
pnpm create next-app@latest
Setting up Supabase
Create a new Supabase project, and get
the connection string for the database from settings >
database.
Add the connection string inside a new `.env` file in the
root of your project like this -
DATABASE_URL=your_connection_string
-
Install and setup drizzle
pnpm add drizzle-orm postgres pnpm add -D drizzle-kit
Now, we need to add the setup and config files for drizzle. Create a drizzle.config.ts
file in the root of your project and add the following -
// drizzle.config.ts
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";
dotenv.config();
export default {
schema: "./src/lib/db/schema.ts",
out: "./drizzle",
driver: "pg",
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config;
Create a new folder under src/
named lib/
and add another folder named db/
inside it. Add these two files inside the folder -
// src/lib/db/index.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL is not set");
}
declare module global {
let postgresSqlClient: ReturnType<typeof postgres> | undefined;
}
let postgresSqlClient;
if (process.env.NODE_ENV !== "production") {
if (!global.postgresSqlClient) {
global.postgresSqlClient = postgres(connectionString);
}
postgresSqlClient = global.postgresSqlClient;
} else {
postgresSqlClient = postgres(connectionString);
}
export const db = drizzle(postgresSqlClient);
// src/lib/db/schema.ts
import { pgTable, varchar, integer } from "drizzle-orm/pg-core";
export const pokemon = pgTable("pokemon", {
id: integer("id").primaryKey().notNull(),
name: varchar("name", { length: 50 }).notNull(),
type: varchar("type", { length: 50 }).notNull(),
});
These two files contain the schema of our table and the initialized database wrapper. You can look at Drizzle Docs to learn more about how it works!
And with this everything's setup, we just need to create the UI and backend functions.
Finishing up
I won't go into a lot of detail into each file, the project is open sourced and all the source code is available here, feel free to check it out!
Let's see the code for the home page file -
// src/app/page.tsx
import Image from "next/image";
import Guesser from "@/components/Guesser";
import { db } from "@/lib/db";
import { pokemon } from "@/lib/db/schema";
async function fetchRandomPokemon() {
// the random pokemon should not be one that has already been caught
const caughtPokemons = await db.select().from(pokemon);
const caughtPokemonIDs = caughtPokemons.map((pokemon) => pokemon.id);
let randomPokemonID;
do {
randomPokemonID = Math.floor(Math.random() * 898) + 1;
} while (caughtPokemonIDs.includes(randomPokemonID));
const response = await fetch(
`https://pokeapi.co/api/v2/pokemon/${randomPokemonID}`
);
const randomPokemon = await response.json();
return {
id: randomPokemon.id,
name: randomPokemon.name,
type: randomPokemon.types[0].type.name,
};
}
export default async function Home() {
const pokemon = await fetchRandomPokemon();
return (
<>
<h1 className="text-4xl font-bold text-balance">
Catch'em all! Can you guess this Pokémon?
</h1>
<Image
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${pokemon.id}.png`}
alt="A random Pokémon"
width={300}
height={300}
/>
{pokemon && <Guesser pokemon={pokemon} />}
</>
);
}
It fetches a random pokemon from the pokeAPI using a random integer making sure that the pokemon hasn't already been caught.
Here's the code for the Guesser
Component -
// src/components/Guesser.tsx
"use client";
import { useState } from "react";
import random from "@/lib/actions/random";
import catchPokemon from "@/lib/actions/catch";
export default function Guesser({
pokemon,
}: {
pokemon: {
id: number;
name: string;
type: string;
};
}) {
const [guess, setGuess] = useState("");
const [correct, setCorrect] = useState(false);
const [showResult, setShowResult] = useState(false);
const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
async function handleSubmit(event: React.FormEvent) {
event.preventDefault();
if (guess.toLowerCase() === pokemon.name.toLowerCase()) {
setCorrect(true);
setShowResult(true);
catchPokemon({ guessedPokemon: pokemon }).then(() => {
setShowResult(false);
setGuess("");
});
} else {
setCorrect(false);
setGuess("");
setShowResult(true);
await sleep(3000);
setShowResult(false);
}
}
return (
<div className="flex flex-col gap-2 items-center font-silk">
<div className="flex gap-2 items-center">
<input
type="text"
value={guess}
onChange={(event) => setGuess(event.target.value)}
className="border border-gray-300 rounded p-2"
placeholder="Enter your guess"
/>
<button
onClick={handleSubmit}
className="bg-blue-500 text-white rounded p-2 hover:bg-blue-600"
>
Catch!
</button>
<button
className="bg-red-500 text-white rounded p-2 hover:bg-red-600"
onClick={() => random()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 256 256"
>
<path
fill="currentColor"
d="m214.2 213.1l-.5.5h-.1l-.5.5l-.3.2l-.4.3l-.3.2l-.3.2h-.4l-.3.2h-.4l-.4.2H168a8 8 0 0 1 0-16h20.7L42.3 53.7a8.1 8.1 0 0 1 11.4-11.4L200 188.7V168a8 8 0 0 1 16 0v40.8a.4.4 0 0 0-.1.3a.9.9 0 0 1-.1.5v.3a.8.8 0 0 0-.1.4l-.2.4c0 .1-.1.2-.1.4l-.2.3c0 .1-.1.2-.1.4l-.2.3l-.2.3l-.3.4Zm-63.6-99.7a8 8 0 0 0 5.7-2.4L200 67.3V88a8 8 0 0 0 16 0V47.2a.4.4 0 0 1-.1-.3a.9.9 0 0 0-.1-.5v-.3a.8.8 0 0 1-.1-.4c-.1-.1-.1-.3-.2-.4s-.1-.2-.1-.4l-.2-.3c0-.1-.1-.2-.1-.4s-.2-.2-.2-.3s-.2-.2-.2-.3l-.3-.4l-.2-.3l-.5-.5h-.1c-.2-.2-.4-.3-.5-.5l-.3-.2l-.4-.3l-.3-.2l-.3-.2h-.4l-.3-.2h-.4l-.4-.2H168a8 8 0 0 0 0 16h20.7L145 99.7a7.9 7.9 0 0 0 0 11.3a7.7 7.7 0 0 0 5.6 2.4ZM99.7 145l-57.4 57.3a8.1 8.1 0 0 0 0 11.4a8.2 8.2 0 0 0 11.4 0l57.3-57.4A8 8 0 0 0 99.7 145Z"
/>
</svg>
</button>
</div>
<span className={`text-xs`}>
You can only catch the Pokémon if you guess its name correctly!
</span>
{showResult && (
<div
className={`text-xs ${correct ? "text-green-500" : "text-red-500"}`}
>
{correct
? `${pokemon.name} has been caught!`
: "You missed it! Try again!"}
</div>
)}
</div>
);
}
As you can see I'm using two server actions here named random
and catchPokemon
. The first one just revalidates the cache for home age which generates a new Pokemon to guess, the other one is used to store the caught Pokemon to db if the guess is correct. Here's the code for it -
// src/lib/actions/catch.ts
"use server";
import { db } from "@/lib/db";
import { pokemon } from "@/lib/db/schema";
import { revalidatePath } from "next/cache";
type Pokemon = typeof pokemon.$inferInsert;
export default async function catchPokemon({
guessedPokemon,
}: {
guessedPokemon: Pokemon;
}) {
const caughtPokemon = await db.insert(pokemon).values(guessedPokemon);
revalidatePath("/");
return caughtPokemon;
}
And this is more or less the code I wrote in those 2 hours! You can check the whole project on github and feel free to ask me about any queries you might have in the comments below!
Conclusion
Building this project was quite a fun experience, I got to learn about connecting Supabase with Drizzle, and using Drizzle with Next.js as well. Hope you liked the project, thanks for reading!