Skip to content
Draft
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
16 changes: 14 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ BACKEND_ONLY_PKGS := packages/txm,apps/randomness,apps/faucet
BACKEND_PKGS := support/common,$(BACKEND_ONLY_PKGS)

# all the swarm leaderboard packages
LEADERBOARD_PKGS := apps/leaderboard-backend
LEADERBOARD_PKGS := apps/leaderboard-backend,apps/leaderboard-frontend

# all typescript packages, excluding docs
TS_PKGS := $(ACCOUNT_PKGS),$(DEMOS_PKGS),${BACKEND_ONLY_PKGS},apps/leaderboard-backend
TS_PKGS := $(ACCOUNT_PKGS),$(DEMOS_PKGS),${BACKEND_ONLY_PKGS},${LEADERBOARD_PKGS}

# all packages that have a package.json
NPM_PKGS := $(TS_PKGS),apps/docs,contracts,support/configs
Expand Down Expand Up @@ -186,6 +186,18 @@ leaderboard-backend.prod: leaderboard-backend.build
cd apps/leaderboard-backend && make prod;
.PHONY: leaderboard-backend.prod

leaderboard-frontend.dev: setup.ts shared.dev
cd apps/leaderboard-frontend && make dev;
.PHONY: leaderboard-frontend.dev

leaderboard-frontend.build: shared.build
cd apps/leaderboard-frontend && make build;
.PHONY: leaderboard-frontend.build

leaderboard-frontend.prod: leaderboard-frontend.build
cd apps/leaderboard-frontend && make prod;
.PHONY: leaderboard-frontend.prod

iframe.dev: shared.dev sdk.dev ## Serves the wallet iframe at http://localhost:5160
cd apps/iframe && make dev
.PHONY: iframe.dev
Expand Down
4 changes: 4 additions & 0 deletions apps/leaderboard-frontend/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include ../../makefiles/lib.mk
include ../../makefiles/vite.mk
include ../../makefiles/formatting.mk
include ../../makefiles/help.mk
15 changes: 15 additions & 0 deletions apps/leaderboard-frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# leaderboard-frontend

## Quickstart

- `make setup` — Install dependencies
- `make dev` — Start the dev server with hot reload
- `make build` — Build the frontend for production
- `make prod` — Run the built app in production mode

## Running from the Monorepo Root

To run the frontend (with iframe support, if needed), from the repo root:

- `make leaderboard-frontend.dev` — Dev mode (hot reload, with iframe)
- `make leaderboard-frontend.prod` — Production mode (with iframe)
4 changes: 4 additions & 0 deletions apps/leaderboard-frontend/biome.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "../../node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["../../support/configs/biome.jsonc"]
}
17 changes: 17 additions & 0 deletions apps/leaderboard-frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/happychain.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Leaderboard Frontend</title>
<!-- <link rel="stylesheet" href="/src/index.css"> -->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no styles! 👀

</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
20 changes: 20 additions & 0 deletions apps/leaderboard-frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "leaderboard-frontend",
"private": true,
"version": "0.2.1",
"type": "module",
"dependencies": {
"@happy.tech/react": "workspace:0.2.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"viem": "^2.21.53"
},
"devDependencies": {
"@happy.tech/configs": "workspace:0.1.0",
"@types/react": "^18.3.4",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you seem to be missing packages you are using 🧐

"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.7.0",
"typescript": "^5.6.2",
"vite": "^5.4.2"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/leaderboard-frontend/public/happychain.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions apps/leaderboard-frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Link, Route, BrowserRouter as Router, Routes } from "react-router-dom"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error: The following dependencies are imported but could not be resolved:

react-router-dom

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also why react-router-dom and not the main package react-router for a new project

(or tanstack router for that matter to line up with the iframe?)

import GamesPage from "./components/GamesPage"
import GuildsPage from "./components/GuildsPage"
import HomeLogoButton from "./components/HomeLogoButton"
import Leaderboards from "./components/Leaderboards"
import ProfilePage from "./components/ProfilePage"
import WalletConnect from "./components/WalletConnect"
import "./index.css"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably since these are all global, this should be imported by main.tsx or just directly from the html (this is preferred)

if its imported like this, then they styles are imported as a a byproduct of the processed CSS, which means we need to load and parse the JS before the styles can be applied. If its in the index.html, it can load and be applied in parallel to the JS, which means it goes faster ;)


function App() {
return (
<Router>
<div className="app-root">
<header className="top-bar">
<div className="top-bar-left">
<HomeLogoButton />
</div>
<div className="top-bar-right">
<WalletConnect />
<Link to="/guilds" className="profile-btn">
Guilds
</Link>
<Link to="/games" className="profile-btn">
Games
</Link>
<Link to="/profile" className="profile-btn">
Profile
</Link>
</div>
</header>
<main className="main-content">
<Routes>
<Route
path="/"
element={
<div className="home-welcome-box">
<h1 className="home-welcome-title">Welcome to HappyChain Leaderboard!</h1>
<Leaderboards />
</div>
}
/>
<Route path="/profile" element={<ProfilePage />} />
<Route path="/guilds" element={<GuildsPage />} />
<Route path="/games" element={<GamesPage />} />
</Routes>
</main>
</div>
</Router>
)
}

export default App
4 changes: 4 additions & 0 deletions apps/leaderboard-frontend/src/clients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createHappyPublicClient, createHappyWalletClient } from "@happy.tech/core"

export const publicClient = createHappyPublicClient()
export const walletClient = createHappyWalletClient()
147 changes: 147 additions & 0 deletions apps/leaderboard-frontend/src/components/GamesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { useHappyWallet } from "@happy.tech/react"
import { useEffect, useState } from "react"
import GameCard from "./games/GameCard"
import GameCreateBox from "./games/GameCreateBox"
import GameDetails from "./games/GameDetails"
import MyScoresCard from "./games/MyScoresCard"

export type Game = {
id: number
name: string
description?: string
admin_id: number
}

const GamesPage = () => {
const { user } = useHappyWallet()
const [games, setGames] = useState<Game[]>([])
const [showDetails, setShowDetails] = useState<Game | null>(null)
const [newGameName, setNewGameName] = useState("")
const [newGameDescription, setNewGameDescription] = useState("")
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)

// Fetch games for this user (by admin wallet)
useEffect(() => {
if (!user) {
setGames([])
return
}
setLoading(true)
fetch(`/api/games/admin/${user.address}`)
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data?.ok && Array.isArray(data.data)) {
setGames(data.data)
} else {
setGames([])
}
})
.finally(() => setLoading(false))
}, [user])

// Create new game
const handleCreateGame = async () => {
setMessage(null)
setError(null)
if (!newGameName) return
setLoading(true)
try {
const res = await fetch("/api/games", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: newGameName,
description: newGameDescription,
admin_wallet: user?.address,
}),
})
const data = await res.json()
if (res.ok && data.ok) {
setGames((g) => [...g, data.data])
setMessage("Game created!")
setNewGameName("")
setNewGameDescription("")
} else {
setError(data?.error || JSON.stringify(data))
}
} catch (e) {
setError("Failed to create game: " + e)
} finally {
setLoading(false)
}
}

return (
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-evenly",
alignItems: "flex-start",
gap: 3,
height: "100vh",
width: "100vw",
}}
>
{/* LEFT: GAMES COLUMN */}
<div
className="games-page"
style={{
width: 400,
background: "#fff",
borderRadius: 12,
boxShadow: "0 2px 8px #0001",
padding: 24,
display: "flex",
flexDirection: "column",
alignItems: "stretch",
}}
>
<h2>Games</h2>
<GameCreateBox
newGameName={newGameName}
setNewGameName={setNewGameName}
newGameDescription={newGameDescription}
setNewGameDescription={setNewGameDescription}
loading={loading}
handleCreateGame={handleCreateGame}
/>
{message && <div className="message">{message}</div>}
{error && <div className="error">{error}</div>}
{loading ? (
<div className="loading-spinner">Loading games...</div>
) : (
<div className="games-list">
{games.length === 0 ? (
<div className="empty-list">No games found.</div>
) : (
games.map((game) => <GameCard key={game.id} game={game} onManage={setShowDetails} />)
)}
</div>
)}
{showDetails && <GameDetails game={showDetails} onClose={() => setShowDetails(null)} />}
</div>
{/* RIGHT: MY SCORES COLUMN */}
{user && (
<div
style={{
width: 400,
background: "#fff",
borderRadius: 12,
boxShadow: "0 2px 8px #0001",
padding: 24,
display: "flex",
flexDirection: "column",
alignItems: "stretch",
}}
>
<MyScoresCard userWallet={user.address} games={games} />
</div>
)}
</div>
)
}

export default GamesPage
Loading