仅需 2 小时即可使用 Supabase、Drizzle 和 Next.js 创建一个 Pokémon 猜谜游戏!

2025-06-04

仅需 2 小时即可使用 Supabase、Drizzle 和 Next.js 创建一个 Pokémon 猜谜游戏!

昨天稍早完成了工作和学习后,我手头上有一些空闲时间。由于过去两个月我都没有做过什么有趣的业余项目,所以我决定在睡觉前做一个。令人惊讶的是,我最终用 Supabase、Drizzle 和 Next.js 花了大约 2 个小时,做了一个非常有趣的宝可梦猜谜游戏!这篇博文与其说是教程,不如说更像是我如何构建整个游戏的故事。

如果您想试用一下,这是实时应用程序链接 - https://pokeguesser-ten.vercel.app/

计划

如果你看过《宝可梦》,你就会知道他们在中场休息后会有一个猜谜游戏,玩家需要根据宝可梦的影子来猜宝可梦。我想做一个类似的游戏,但由于难度较高,所以我决定让玩家看到宝可梦,而不仅仅是影子。为了更快地完成这个项目,我还决定让每个人都有一个全局的捕捉记录,而不是每个人都有自己的记录。

设置一切

由于这些工具拥有出色的文档,因此设置项目非常容易!

  1. 使用应用路由器和 TailwindCSS 初始化一个新的 Next.js 项目

    pnpm create next-app@latest
    
  2. 设置 Supabase
    创建一个新的Supabase项目,并
    从设置 > 数据库获取数据库的连接字符串

Supabase 仪表板

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. 安装和设置 drizzle

    pnpm add drizzle-orm postgres
    pnpm add -D drizzle-kit
    

现在,我们需要添加 drizzle 的安装和配置文件。drizzle.config.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;
Enter fullscreen mode Exit fullscreen mode

src/在named下创建一个新文件夹lib/,并在其中添加另一个 named 文件夹db/。在文件夹中添加以下两个文件:

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

这两个文件包含我们表的模式和已初始化的数据库包装器。您可以查看Drizzle 文档来了解更多关于其工作原理的信息!

一切设置完成后,我们只需要创建 UI 和后端功能。

完成

我不会详细介绍每个文件,该项目是开源的,所有源代码都可以在这里找到,请随时查看!

让我们看看主页文件的代码 -

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

它使用随机整数从 pokeAPI 获取一个随机的口袋妖怪,确保该口袋妖怪尚未被捕获。

这是组件的代码Guesser-

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

如你所见,我在这里使用了两个名为random和 的服务器操作catchPokemon。第一个操作会重新验证缓存中的家庭年龄,从而生成一个新的宝可梦供猜测;另一个操作则用于在猜测正确的情况下将捕获的宝可梦存储到数据库中。代码如下:

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

这就是我在那两个小时内写的代码!你可以在 GitHub 上查看整个项目,有任何疑问也可以在下面的评论区问我!

结论

构建这个项目的过程非常有趣,我学习了如何将 Supabase 与 Drizzle 连接,以及如何将 Drizzle 与 Next.js 结合使用。希望你喜欢这个项目,感谢阅读!

文章来源:https://dev.to/asheeshh/creating-a-pokemon-guessing-game-using-supabase-drizzle-and-nextjs-in-just-2-hours-1m9o
PREV
在您的 HTML 文件中直接使用 JavaScript 框架的强大功能!
NEXT
100+ 面向 Web 开发人员的项目创意资源