Setup
Project setup, dependencies, and configuration
Project Setup
Create a New Next.js Project
First, create a new Next.js application with TypeScript:
npx create-next-app@latest reaction-time-gameyarn create next-app reaction-time-gamepnpm create next-app reaction-time-gamebun create next-app reaction-time-gameWhen prompted, select these options:
- Use recommended Next.js defaults: No, customize settings
- TypeScript: Yes
- Linter: None
- React Compiler: Yes
- Tailwind CSS: Yes
src/directory: Yes- App Router: Yes
- Customize import alias: No
If you've completed another tutorial, you can select No, reuse previous settings to skip configuration.
Navigate into your project:
cd reaction-time-gameInstall Dependencies
Install the required packages for blockchain integration:
npm install viem shreds lucide-reactyarn add viem shreds lucide-reactpnpm add viem shreds lucide-reactbun add viem shreds lucide-reactPackage breakdown:
viem- Lightweight Ethereum client library for transactionsshreds- RISE Shreds SDK for 3ms confirmationslucide-react- Icon library for UI elements
Generate a Burner Wallet
Create a burner wallet for testnet transactions. Run this in your terminal:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"This generates a random private key. Copy the output.
Security Warning
This is a burner wallet for demo purposes. Never use this pattern with real funds or mainnet!
Set Up Environment Variables
Create a .env.local file in your project root:
NEXT_PUBLIC_BURNER_KEY=0xYOUR_GENERATED_KEY_HEREReplace YOUR_GENERATED_KEY_HERE with the key you just generated (add 0x prefix).
The NEXT_PUBLIC_ prefix makes this variable accessible in the browser. This is acceptable for burner wallets but never use this for production keys!
Configuration
Configure Fonts
Update your root layout to include arcade-style fonts:
import type { Metadata } from "next";
import { Geist, Geist_Mono, Press_Start_2P, Rajdhani } from "next/font/google";
import "./globals.css";
import { Navbar } from "@/components/Navbar";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const pressStart = Press_Start_2P({
weight: "400",
subsets: ["latin"],
variable: "--font-doom",
});
const rajdhani = Rajdhani({
weight: ["400", "500", "600", "700"],
subsets: ["latin"],
variable: "--font-rajdhani",
});
export const metadata: Metadata = {
title: "Reaction Time Game - RISE",
description: "Test your reflexes with blockchain-backed precision",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} ${pressStart.variable} ${rajdhani.variable} antialiased`}
>
<Navbar />
{children}
</body>
</html>
);
}Fonts included:
Press Start 2P- Retro arcade font for titlesRajdhani- Modern gaming font for UIGeist- Clean sans-serif for body text
Update Global Styles
Replace your globals.css with Tailwind imports:
@import "tailwindcss";That's it! Tailwind CSS 4 uses a simplified import system.
Building the Navbar
Create a navbar to display wallet information and balance:
"use client";
import { useEffect, useState } from "react";
import { createPublicClient, http, formatEther } from "viem";
import { riseTestnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { Copy, Check } from "lucide-react";
const account = privateKeyToAccount(
process.env.NEXT_PUBLIC_BURNER_KEY as `0x${string}`
);
const publicClient = createPublicClient({
chain: riseTestnet,
transport: http(),
});
export function Navbar() {
const [balance, setBalance] = useState<string>("0");
const [copied, setCopied] = useState(false);
useEffect(() => {
const fetchBalance = async () => {
const bal = await publicClient.getBalance({
address: account.address,
});
setBalance(formatEther(bal));
};
fetchBalance();
const interval = setInterval(fetchBalance, 10000); // Update every 10s
return () => clearInterval(interval);
}, []);
const copyAddress = async () => {
await navigator.clipboard.writeText(account.address);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<nav className="fixed top-0 left-0 right-0 z-50 bg-black/50 backdrop-blur-md border-b border-purple-500/20">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-purple-400 font-[var(--font-doom)]">
REACTION TIME
</h1>
<div className="flex items-center gap-6">
<div className="text-right">
<p className="text-xs text-gray-400">Balance</p>
<p className="text-lg font-semibold text-white">
{parseFloat(balance).toFixed(4)} ETH
</p>
</div>
<div className="flex items-center gap-2">
<div className="text-right">
<p className="text-xs text-gray-400">Burner Wallet</p>
<p className="text-sm font-mono text-white">
{account.address.slice(0, 6)}...{account.address.slice(-4)}
</p>
</div>
<button
onClick={copyAddress}
className="p-2 rounded-lg bg-purple-500/20 hover:bg-purple-500/30 transition-colors"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4 text-purple-400" />
)}
</button>
</div>
</div>
</div>
</div>
</nav>
);
}Key features:
- Displays ETH balance (refreshes every 10 seconds)
- Shows truncated burner wallet address
- Copy address to clipboard with visual feedback
- Arcade-themed styling with purple accents
Now you're ready to implement the game logic in the next section.