-
Notifications
You must be signed in to change notification settings - Fork 2
Add leaderboard-frontend app with UI components #708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: aryan/swarm-leadership-page-mvp
Are you sure you want to change the base?
Changes from all commits
1a67d5b
00d3956
8f4dd38
5f4641d
28127a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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) |
| 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"] | ||
| } |
| 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"> --> | ||
| </head> | ||
|
|
||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
|
|
||
| </html> | ||
| 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", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
| } | ||
| } | ||
| 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" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably since these are all global, this should be imported by 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 | ||
| 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() |
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no styles! 👀