Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions client/app/client-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";

import dynamic from "next/dynamic";
import { ReactNode } from "react";

const Providers = dynamic(
() => import("@/providers/providers").then((mod) => mod.Providers),
{ ssr: false },
);

export default function ClientLayout({ children }: { children: ReactNode }) {
return <Providers>{children}</Providers>;
}
4 changes: 2 additions & 2 deletions client/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Providers } from "@/providers/providers";
import ClientLayout from "./client-layout";
import "@rainbow-me/rainbowkit/styles.css";
import type { Metadata } from "next";
import "./globals.css";
Expand All @@ -16,7 +16,7 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
<ClientLayout>{children}</ClientLayout>
</body>
</html>
);
Expand Down
24 changes: 16 additions & 8 deletions client/app/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,18 @@ export default function Leaderboard() {
],
});

const highScore = data?.[0]?.result;
const champion = data?.[1]?.result;
const highScore = data?.[0]?.result as bigint | undefined;
const champion = data?.[1]?.result as string | undefined;

const formatAddress = (address: string) => {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
const formatAddress = (address: string) =>
`${address.slice(0, 6)}...${address.slice(-4)}`;

const formatScore = (score: bigint): string => {
if (score <= BigInt(1_000_000_000)) {
return Number(score).toLocaleString();
}
const str = score.toString();
return `${str[0]}.${str.slice(1, 4)}e+${str.length - 1}`;
};

return (
Expand Down Expand Up @@ -96,7 +103,7 @@ export default function Leaderboard() {
<div className="flex items-center gap-2">
<h3 className="text-lg md:text-2xl font-bold text-transparent bg-gradient-to-r from-white to-gray-300 bg-clip-text truncate">
{champion
? formatAddress(champion as string)
? formatAddress(champion)
: "No Champion"}
</h3>
{champion && (
Expand Down Expand Up @@ -124,9 +131,10 @@ export default function Leaderboard() {
Error
</div>
) : (
<div className="text-2xl md:text-3xl font-bold text-transparent bg-gradient-to-r from-yellow-400 to-orange-400 bg-clip-text">
High Score:{" "}
{highScore ? Number(highScore).toLocaleString() : "0"}
<div className="text-2xl md:text-3xl font-bold text-transparent bg-gradient-to-r from-yellow-400 to-orange-400 bg-clip-text break-all">
{highScore !== undefined
? formatScore(highScore)
: "0"}
</div>
)}
</div>
Expand Down
10 changes: 10 additions & 0 deletions client/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Board from "@/components/board";
import ConnectWallet from "@/components/connect-wallet";
import { GameOverDialog } from "@/components/game-over-dialog";
import Navigation from "@/components/navigation";
import NetworkConsole from "@/components/network-console";
import { Button } from "@/components/ui/button";
import UpcomingBlocks from "@/components/upcoming-blocks";
import { useTetris } from "@/hooks/use-tetris";
Expand Down Expand Up @@ -36,6 +37,7 @@ export default function Home() {
setIsGameOver,
coronationHash,
isNewChamp,
networkLogs,
} = useTetris();

const handlePlayAgain = () => {
Expand Down Expand Up @@ -121,6 +123,10 @@ export default function Home() {
)}
</div>
</div>

<div className="container mx-auto px-4 pb-8 mt-4 max-w-3xl">
<NetworkConsole logs={networkLogs} />
</div>
</div>
</div>

Expand Down Expand Up @@ -261,6 +267,10 @@ export default function Home() {
)}
</div>

<div className="px-3 py-2">
<NetworkConsole logs={networkLogs} />
</div>

{/* Connection Status */}
{!isWsConnected && (
<div className="fixed bottom-0 left-0 right-0 bg-yellow-600/90 text-center py-1 z-50">
Expand Down
94 changes: 94 additions & 0 deletions client/components/network-console.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"use client";

import { useEffect, useRef, useState } from "react";
import type { NetworkLogEntry } from "@/hooks/use-tetris";

interface NetworkConsoleProps {
logs: NetworkLogEntry[];
}

export default function NetworkConsole({ logs }: NetworkConsoleProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const [expanded, setExpanded] = useState<number | null>(null);

useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [logs.length]);

const formatTime = (iso: string) => {
const d = new Date(iso);
return d.toLocaleTimeString("en-US", { hour12: false, fractionalSecondDigits: 3 });
};

const truncatePayload = (data: unknown): string => {
const str = JSON.stringify(data);
return str.length > 120 ? str.slice(0, 120) + "..." : str;
};

return (
<div className="bg-gray-950/90 border border-emerald-500/30 rounded-xl overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 bg-emerald-900/30 border-b border-emerald-500/20">
<div className="flex items-center gap-2">
<span className="inline-block w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
<span className="text-xs font-bold text-emerald-300 tracking-wider uppercase">
Yellow Network Console
</span>
</div>
<span className="text-xs text-gray-500">
{logs.length} message{logs.length !== 1 ? "s" : ""}
</span>
</div>

<div
ref={scrollRef}
className="overflow-y-auto p-3 space-y-1 font-mono text-xs"
style={{ maxHeight: "240px" }}
>
{logs.length === 0 ? (
<div className="text-gray-600 text-center py-4">
Waiting for network messages...
</div>
) : (
logs.map((log, i) => {
const isSent = log.direction === "sent";
const isExpanded = expanded === i;

return (
<div key={i}>
<button
type="button"
className="w-full text-left flex items-start gap-2 hover:bg-white/5 rounded px-1 py-0.5 transition-colors"
onClick={() => setExpanded(isExpanded ? null : i)}
>
<span className="text-gray-600 shrink-0">
{formatTime(log.timestamp)}
</span>
<span
className={`shrink-0 font-bold ${isSent ? "text-blue-400" : "text-emerald-400"}`}
>
{isSent ? ">>>" : "<<<"}
</span>
<span className="text-yellow-300 shrink-0 font-semibold">
{log.method}
</span>
{!isExpanded && (
<span className="text-gray-500 truncate">
{truncatePayload(log.data)}
</span>
)}
</button>
{isExpanded && (
<pre className="ml-6 mt-1 mb-2 p-2 bg-black/40 rounded text-gray-300 overflow-x-auto whitespace-pre-wrap break-all text-[10px] leading-relaxed border border-gray-800">
{JSON.stringify(log.data, null, 2)}
</pre>
)}
</div>
);
})
)}
</div>
</div>
);
}
27 changes: 26 additions & 1 deletion client/hooks/use-tetris.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import {
getEmptyBoard,
} from "@/hooks/use-tetris-board";

export interface NetworkLogEntry {
direction: "sent" | "received";
timestamp: string;
method: string;
data: unknown;
}

enum TickSpeed {
Normal = 800,
Sliding = 100,
Expand All @@ -29,6 +36,7 @@ export function useTetris() {
undefined,
);
const [isNewChamp, setIsNewChamp] = useState(false);
const [networkLogs, setNetworkLogs] = useState<NetworkLogEntry[]>([]);

const wsRef = useRef<WebSocket | null>(null);
const [isWsConnected, setIsWsConnected] = useState(false);
Expand Down Expand Up @@ -102,6 +110,23 @@ export function useTetris() {
}
break;

case "networkLog": {
const method =
data.data?.req?.[1] ??
data.data?.res?.[1] ??
"unknown";
setNetworkLogs((prev) => {
const next = [...prev, {
direction: data.direction,
timestamp: data.timestamp,
method,
data: data.data,
}];
return next.length > 100 ? next.slice(-100) : next;
});
break;
}

case "error":
console.error("WebSocket error:", data.message);
setIsLoadingBlocks(false);
Expand Down Expand Up @@ -412,7 +437,7 @@ export function useTetris() {
setIsGameOver,
coronationHash,
isNewChamp,
// Mobile control functions
networkLogs,
moveLeft,
moveRight,
rotate,
Expand Down
Loading