Compare commits

...

2 Commits

Author SHA1 Message Date
aidan 8916717a8a Update README.md 2025-07-25 15:42:27 -04:00
Aidan Haas 66186eb016 add clock screensaver, basic team ordering, connection error message 2025-07-24 19:48:00 -04:00
8 changed files with 190 additions and 103 deletions
+11 -1
View File
@@ -1,4 +1,4 @@
# Pool Tracker # Full Stack Pool Tracker
Simple web app the keeps track of who's turn it is in pool. Simple web app the keeps track of who's turn it is in pool.
Uses Next.JS for frontend and Flask for the backend. Uses Next.JS for frontend and Flask for the backend.
@@ -7,8 +7,18 @@ Syncs in realtime between devices with sockets
Add the players with the homepage, and view who's turn it is on the `/live` page Add the players with the homepage, and view who's turn it is on the `/live` page
![Idle Screen](image.png)
Idle Screensaver ()
![No Connection](image-1.png)
No Connection Message
![Main Menu](image-2.png)
Root Menu
![Tablet Screen](image-3.png)
Live page
Tools used: Tools used:
SVGs generated from https://www.svgrepo.com SVGs generated from https://www.svgrepo.com
https://www.freetool.dev/emoji-picker/ https://www.freetool.dev/emoji-picker/
+17
View File
@@ -49,10 +49,27 @@ def reset():
def status(): def status():
return jsonify(game_state) return jsonify(game_state)
def orderplayers():
stripes = []
solids = []
for player in game_state["players"]:
if player["group"] == "stripes":
stripes.append(player)
else:
solids.append(player)
print(stripes)
print(solids)
combined = [player for pair in zip(stripes, solids) for player in pair]
print(combined)
game_state["players"] = combined
@app.route("/start", methods=["GET"]) @app.route("/start", methods=["GET"])
def start_game(): def start_game():
players = game_state["players"] players = game_state["players"]
game_state["gameActive"] = True game_state["gameActive"] = True
orderplayers()
current_player = players[game_state["playerTurn"]] current_player = players[game_state["playerTurn"]]
socketio.emit("player_update", {"nextPlayer": current_player}) socketio.emit("player_update", {"nextPlayer": current_player})
return jsonify(game_state) return jsonify(game_state)
+33 -4
View File
@@ -14,7 +14,7 @@ export default function HomePage() {
const backendUrl = `${baseUrl}:${process.env.NEXT_PUBLIC_API_BASE}`; const backendUrl = `${baseUrl}:${process.env.NEXT_PUBLIC_API_BASE}`;
const socket = io(`${backendUrl}`); const socket = io(`${backendUrl}`);
const [time, setTime] = useState<Date>(new Date()); const [time, setTime] = useState(new Date());
const [currentPlayer, setCurrentPlayer] = useState<string | null>(null); const [currentPlayer, setCurrentPlayer] = useState<string | null>(null);
@@ -37,6 +37,30 @@ export default function HomePage() {
socket.off('player_update'); socket.off('player_update');
}; };
}, []); }, []);
// Idle clock stuff
useEffect(() => {
const interval = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(interval);
}, []);
const timeString = time.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
});
const dateString = time.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
const day = time.getDate();
const suffix =
day % 10 === 1 && day !== 11 ? 'st' :
day % 10 === 2 && day !== 12 ? 'nd' :
day % 10 === 3 && day !== 13 ? 'rd' : 'th';
const formattedDate = `${time.toLocaleString('en-US', { month: 'long' })} ${day}${suffix}, ${time.getFullYear()}`;
const advanceTurn = async () => { const advanceTurn = async () => {
await fetch(`${baseUrl}/next`); // triggers backend to emit event await fetch(`${baseUrl}/next`); // triggers backend to emit event
@@ -77,9 +101,14 @@ export default function HomePage() {
); );
} else { } else {
return ( return (
<main className="p-6"> <main>
{time.getTime()} <div className="h-screen flex flex-col items-center justify-center text-center text-4xl font-semibold">
This will show the time <div>{timeString}</div>
<div className="text-2xl mt-2">{formattedDate}</div>
</div>
<div className="w-full py-4 text-center text-gray-500">
<div className="text-xl mt-2">Go to {baseUrl} to start a game!</div>
</div>
</main> </main>
); );
} }
+128 -97
View File
@@ -1,4 +1,5 @@
'use client'; 'use client';
import { connect } from "http2";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import io from 'socket.io-client'; import io from 'socket.io-client';
@@ -7,6 +8,7 @@ import io from 'socket.io-client';
export default function Home() { export default function Home() {
const nameInputRef = useRef<HTMLInputElement>(null); const nameInputRef = useRef<HTMLInputElement>(null);
const currentPlayers = useState<string | null>(null); const currentPlayers = useState<string | null>(null);
const [apiConnected, setAPIStatus] = useState<boolean>(false);
const [gameStatus, setGameStatus] = useState<{ gameActive: boolean, players: any[] }>({ const [gameStatus, setGameStatus] = useState<{ gameActive: boolean, players: any[] }>({
gameActive: false, gameActive: false,
players: [], players: [],
@@ -20,13 +22,16 @@ export default function Home() {
const backendUrl = `${baseUrl}:${process.env.NEXT_PUBLIC_API_BASE}`; const backendUrl = `${baseUrl}:${process.env.NEXT_PUBLIC_API_BASE}`;
const socket = io(`${backendUrl}`); const socket = io(`${backendUrl}`);
const fetchStatus = async () => { const fetchStatus = async () => {
try { try {
const res = await fetch(`${backendUrl}/status`); const res = await fetch(`${backendUrl}/status`);
const data = await res.json(); const data = await res.json();
setAPIStatus(true);
setGameStatus(data); setGameStatus(data);
} catch (err) { } catch (err) {
console.error("Failed to fetch status", err); console.error("Failed to fetch status", err);
setAPIStatus(false);
} }
}; };
@@ -40,6 +45,7 @@ export default function Home() {
useEffect(() => { useEffect(() => {
socket.on('connect', () => { socket.on('connect', () => {
console.log('Connected to Socket.IO server'); console.log('Connected to Socket.IO server');
setAPIStatus(true);
fetchStatus(); fetchStatus();
}); });
@@ -93,110 +99,135 @@ export default function Home() {
await fetch(`${backendUrl}/reset`); // triggers backend to emit event await fetch(`${backendUrl}/reset`); // triggers backend to emit event
}; };
return ( console.log(apiConnected);
if (apiConnected) {
return (
<div>
<div className="border rounded p-4 max-w-md m-5">
<h2 className="text-xl font-semibold mb-3">🎯 Game Info</h2>
<p className="mb-2">Game Active: <strong>{gameStatus.gameActive ? "Yes" : "No"}</strong></p>
<table className="w-full text-left border-t">
<thead>
<tr><th className="py-1">Player</th><th>Group</th></tr>
</thead>
<tbody>
{gameStatus.players.map((player, idx) => (
<tr key={idx} className="border-t">
<td className="py-1">{player.name}</td>
<td>{player.group}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<div className="flex gap-8 mb-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="group"
value="stripes"
checked={selectedGroup === "stripes"}
onChange={() => setSelectedGroup("stripes")}
className="cursor-pointer"
/>
Stripes
</label>
<div> <label className="flex items-center gap-2 cursor-pointer">
<div className="border rounded p-4 max-w-md m-5"> <input
<h2 className="text-xl font-semibold mb-3">🎯 Game Info</h2> type="radio"
<p className="mb-2">Game Active: <strong>{gameStatus.gameActive ? "Yes" : "No"}</strong></p> name="group"
<table className="w-full text-left border-t"> value="solids"
<thead> checked={selectedGroup === "solids"}
<tr><th className="py-1">Player</th><th>Group</th></tr> onChange={() => setSelectedGroup("solids")}
</thead> className="cursor-pointer"
<tbody> />
{gameStatus.players.map((player, idx) => ( Solids
<tr key={idx} className="border-t"> </label>
<td className="py-1">{player.name}</td> </div>
<td>{player.group}</td> <div className="flex gap-4 items-center flex-col sm:flex-row">
</tr> <input ref={nameInputRef} id="name" placeholder="Enter Name Here"></input>
))} </div>
</tbody>
</table>
</div>
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start"> <div className="flex gap-4 items-center flex-col sm:flex-row">
<div className="flex gap-8 mb-4"> <button
<label className="flex items-center gap-2 cursor-pointer"> onClick={addPlayer}
<input className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto">
type="radio" <Image
name="group" className="dark:invert"
value="stripes" src="/add.svg"
checked={selectedGroup === "stripes"} alt="Vercel logomark"
onChange={() => setSelectedGroup("stripes")} width={20}
className="cursor-pointer" height={20}
/> />
Stripes Add Player</button>
</label> <button
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
<label className="flex items-center gap-2 cursor-pointer"> onClick={startGame}
<input >
type="radio"
name="group"
value="solids"
checked={selectedGroup === "solids"}
onChange={() => setSelectedGroup("solids")}
className="cursor-pointer"
/>
Solids
</label>
</div>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<input ref={nameInputRef} id="name" placeholder="Enter Name Here"></input>
</div>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<button
onClick={addPlayer}
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto">
<Image <Image
className="dark:invert" className="dark:invert"
src="/add.svg" src="/play-button.svg"
alt="Vercel logomark" alt="Vercel logomark"
width={20} width={20}
height={20} height={20}
/> />
Add Player</button> Start Game
<button </button>
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto" <button
onClick={startGame} className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
> onClick={resetGame}
<Image >
className="dark:invert" <Image
src="/play-button.svg" className="dark:invert"
alt="Vercel logomark" src="/reset.svg"
width={20} alt="Vercel logomark"
height={20} width={20}
/> height={20}
Start Game />
</button> Reset Game</button>
<button <button
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto" onClick={advanceTurn}
onClick={resetGame} className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto">
> <Image
<Image className="dark:invert"
className="dark:invert" src="/next.svg"
src="/reset.svg" alt="Vercel logomark"
alt="Vercel logomark" width={20}
width={20} height={20}
height={20} />Next Turn</button>
/> </div>
Reset Game</button> </main>
<button </div>
onClick={advanceTurn}
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto">
<Image
className="dark:invert"
src="/next.svg"
alt="Vercel logomark"
width={20}
height={20}
/>Next Turn</button>
</div>
</main>
</div> </div>
</div> );
); }
else
return (
<div>
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<p>Not Connected to API {apiConnected}</p>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<button
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
onClick={startGame}
>
<Image
className="dark:invert"
src="/play-button.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Try Reconnecting
</button>
</div>
</main>
</div>
</div>
);
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB