Creating a Pokémon guessing game using Supabase, Drizzle, and Next.js in just 2 hours!
Tags: webdev, javascript, react, tutorial
Published: 2024-04-23 19:43:06

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
    ```bash
pnpm create next-app@latest
```

2. Setting up Supabase
    Create a new [Supabase](https://supabase.com) project, and get 
    the connection string for the database from settings > 
    database.

![Supabase dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nk7wzti7guchjktyrmjm.png)

    Add the connection string inside a new `.env` file in the 
    root of your project like this - 

```bash
DATABASE_URL=your_connection_string
```

3. Install and setup drizzle

    ```bash
    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 -

```ts
// 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 -

```ts
//  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);
```
```ts
// 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](https://orm.drizzle.team/docs/overview) 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](https://github.com/asrvd/pokeguesser), feel free to check it out!

Let's see the code for the home page file - 
```tsx
// 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} />}
    </>
  );
}
```
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 -
```tsx
// 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 -
```ts
// 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!