Creating a Pokémon guessing game using Supabase, Drizzle, and Next.js in just 2 hours!

ashish - Apr 23 - - Dev Community

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!

  1. Init a new Next.js project with app router and TailwindCSS

    pnpm create next-app@latest
    
  2. Setting up Supabase
    Create a new Supabase project, and get
    the connection string for the database from settings >
    database.

Supabase dashboard

Add the connection string inside a new `.env` file in the 
root of your project like this - 
Enter fullscreen mode Exit fullscreen mode
DATABASE_URL=your_connection_string
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
// 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(),
});
Enter fullscreen mode Exit fullscreen mode

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&apos;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} />}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . .