Building the Next.js Frontend for Your Prediction Game - Part 2

Bringing Your Blockchain-Based Prediction Game to Users: Building the Next.js Frontend

Building the Next.js Frontend for Your Prediction Game - Part 2

Building the Next.js Frontend for Your Prediction Game - Part 2


Introduction

In this second part of the tutorial, I'll guide you through building a Frontend application using Next.js to interact with the smart contract you deployed in Part 1. This frontend will allow users to connect their MetaMask wallets, make predictions, place bets, and see the results of their game.

Prerequisites:

Before diving into the tutorial, ensure you have the following:

  • A basic understanding of React and Next.js.

  • Node.js and npm installed on your machine.

  • The Smart Contract deployed to the Linea Sepolia Testnet (from Part 1).

  • The Smart Contract address and ABI from the deployed contract.

Step 1: Setting Up the Next.js Project

  1. Create a New Next.js Project:

Open your terminal and create a new Next.js project and navigate to the project directory using the following command:

npx create-next-app prediction-game && cd prediction-game
  1. Install Dependencies:

Install the viemjs for interacting with the blockchain:

npm install viem ethers
  1. Install Tailwind CSS for styling:
npx tailwindcss init -p

Configure Tailwind by adding the following to your tailwind.config.js:

module.exports = {
content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Add Tailwind to your global CSS file in styles/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

Step 2: Setting Up the Environment Variables

Create a .env.local file in your project root:

Store your Smart Contract address:

NEXT_PUBLIC_CONTRACT_ADDRESS=0xYourContractAddress

Add the ABI File:

Create a new file named abi.js in the root directory.

Copy the ABI of your smart contract (from Part 1) and paste in an export const declaration.

export const ABI = [//paste ABI here]

Step 3: Connecting to MetaMask

Create a MetaMask Connection Component:

In your pages/index.js, remove all the template code. Set up a basic structure to connect to MetaMask:

import { useState } from "react";
import {
  createWalletClient,
  custom,
} from "viem";
import { lineaSepolia } from "viem/chains";

export default function Home() {
  const [client, setClient] = useState(null);
  const [address, setAddress] = useState(null);
  const [connected, setConnected] = useState(false);

  const connectToMetaMask = async () => {
    if (typeof window !== "undefined" && typeof window.ethereum !== "undefined") {
      try {
        await window.ethereum.request({ method: "eth_requestAccounts" });
        const walletClient = createWalletClient({
          chain: lineaSepolia,
          transport: custom(window.ethereum),
        });
        const [userAddress] = await walletClient.getAddresses();
        setClient(walletClient);
        setAddress(userAddress);
        setConnected(true);
        console.log("Connected account:", userAddress);
      } catch (error) {
        console.error("User denied account access:", error);
      }
    } else {
      console.log("MetaMask is not installed or not running in a browser environment!");
    }
  };

  return (
    <main
      className={`flex min-h-screen flex-col items-center justify-between p-24`}
    >
      {!connected ? (
        <button
          onClick={connectToMetaMask}
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
        >
          Connect Wallet
        </button>
      ) : (
        <div>
          <p>Connected as: {address}</p>
        </div>
      )}
    </main>
  );
}

This component allows users to connect their MetaMask wallet.

Step 4: Building the Form for the play Method

Add the Form UI:

Modify your Home component to include a form that allows users to input their prediction and bet amount. We will add various states to manage the form inputs.

import ABI from "../abi";

const publicClient = createPublicClient({
  chain: lineaSepolia,
  transport: http(),
});

export default function Home() {
...
  // Add new States
  const [houseBalance, setHouseBalance] = useState(null);
  const [prediction, setPrediction] = useState("");
  const [betAmount, setBetAmount] = useState("");
  const [transactionHash, setTransactionHash] = useState("");

    const handlePlay = async (event) => {
      event.preventDefault();

      if (!client || !address) {
        console.error("Client or address not available");
        return;
      }

      try {
        const { request } = await publicClient.simulateContract({
          account: address,
          address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS,
          abi: ABI,
          functionName: "play",
          args: [parseInt(prediction, 10)],
          value: parseEther(betAmount),
        });

        if (!request) {
          console.error("Simulation failed to return a valid request object.");
          return;
        }

        console.log("Simulation successful. Request object:", request);

        const hash = await client.writeContract(request);
        setTransactionHash(hash);
        console.log(`Transaction sent: ${hash}`);

        const receipt = await client.waitForTransactionReceipt({hash});
        console.log(`Transaction confirmed: ${receipt.transactionHash}`);
      } catch (error) {
        console.error("Simulation or transaction failed:", error);
      }
    };

    return (
      <div>
        <form onSubmit={handlePlay} className="mt-4">
          <div className="mb-4">
            <label className="block text-gray-700 text-sm font-bold mb-2">
              Prediction (0-9):
            </label>
            <input
              type="number"
              min="0"
              max="9"
              value={prediction}
              onChange={(e) => setPrediction(e.target.value)}
              required
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
            />
          </div>
          <div className="mb-4">
            <label className="block text-gray-700 text-sm font-bold mb-2">
              Bet Amount (in ETH):
            </label>
            <input
              type="text"
              value={betAmount}
              onChange={(e) => setBetAmount(e.target.value)}
              required
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
            />
          </div>
          <button
            type="submit"
            className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
          >
            Play
          </button>
        </form>

        {transactionHash && (
          <p className="mt-4">Transaction sent: {transactionHash}</p>
        )}
      </div>
    );
}

Form Input: Users can input their prediction (a number between 0 and 9) and the amount they want to bet.

Transaction Handling: The form submits the data, simulates the transaction, and sends it if successful. The transaction hash is displayed on the UI.

Step 5: Handling Contract Events

Fetch and Display Game Results:

Add a function to handle events emitted by the contract:

const fetchGameEvents = async (blockNumber, transactionHash) => {
  try {
    const logs = await publicClient.getContractEvents({
      client: publicClient,
      address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS,
      abi: ABI,
      eventName: "GamePlayed",
      fromBlock: blockNumber,
      toBlock: blockNumber,
    });

    if (logs.length > 0) {
      const event = logs[0];
      const { args } = event;
      const { player, amount, prediction, houseNumber } = args;
      setGameResult(`Game Played: Player ${player} predicted ${prediction}, House Number: ${houseNumber}, Bet Amount: ${amount}`);
    }

    const wonLogs = await publicClient.getContractEvents({
      client: publicClient,
      address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS,
      abi: ABI,
      eventName: "GameWon",
      fromBlock: blockNumber,
      toBlock: blockNumber,
    });

    if (wonLogs.length > 0) {
      setGameResult(`You won the game!`);
    }

    const lostLogs = await publicClient.getContractEvents({
      client: publicClient,
      address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS,
      abi: ABI,
      eventName: "GameLost",
      fromBlock: blockNumber,
      toBlock: blockNumber,
    });

    if (lostLogs.length > 0) {
      setGameResult(`You lost the game.`);
    }
  } catch (error) {
    console.error("Failed to fetch game events:", error);
  }
};

This function listens for the GamePlayed, GameWon, and GameLost events and updates the UI with the appropriate message.

Update the States, the play function and the UI to display the results.

 // State to store game result
    const [gameResult, setGameResult] = useState("");

 // After confirmation in handle play, fetch and handle events
    await fetchGameEvents(receipt.blockNumber, receipt.transactionHash);

 // In the UI After Hash is displayed, return Game Result
    {gameResult && (
      <p className="mt-4">{gameResult}</p>
    )}

Step 6: Running the Application

Start the Development Server:

Run the following command to start the Next.js development server

npm run dev

Open your browser and navigate to http://localhost:3000.

Testing the App:

Connect your MetaMask wallet, place bets, and see the game results in real-time.

Conclusion:

In this second part of the tutorial, you've built a complete Frontend application for your blockchain-based prediction game using Next.js. Users can connect their MetaMask wallets, place bets, and view game results directly in the browser. You've successfully bridged the gap between blockchain and web development, creating a seamless user experience.

Feel free to customize and extend this application further. You might want to add features like historical game data, leaderboards, or more sophisticated game mechanics.

You can find the complete code here. Live Website.

Happy coding! 🚀