import { useState, useEffect } from "react"; import { Game, Source, GameList as GameListProto, Person as PersonProto, Opinion, AddOpinionRequest, RemoveOpinionRequest, } from "../items"; import { Link, useLocation } from "react-router-dom"; import { apiFetch, get_auth_status } from "./api"; import { GameImage } from "./GameImage"; export function GameList() { const [games, setGames] = useState([]); const [title, setTitle] = useState(""); const [source, setSource] = useState(Source.STEAM); const [minPlayers, setMinPlayers] = useState(1); const [maxPlayers, setMaxPlayers] = useState(1); const [price, setPrice] = useState(0); const [remoteId, setRemoteId] = useState(0); const [message, setMessage] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [opinions, setOpinions] = useState([]); const fetchGames = () => { apiFetch("/api/games") .then((res) => res.arrayBuffer()) .then((buffer) => { try { const list = GameListProto.decode(new Uint8Array(buffer)); setGames(list.games); } catch (e) { console.error("Failed to decode games:", e); } }) .catch(console.error); }; useEffect(() => { get_auth_status().then((user) => { apiFetch(`/api/${user?.username}`) .then((res) => res.arrayBuffer()) .then((buffer) => { try { const person = PersonProto.decode(new Uint8Array(buffer)); const opinions = person.opinion; setOpinions(opinions); } catch (e) { console.error("Failed to decode games:", e); } }); }); }, []); // Scroll to Existing Games section if hash is present const location = useLocation(); useEffect(() => { if (location.hash === "#existing-games") { const el = document.getElementById("existing-games"); if (el) { setTimeout(() => { el.scrollIntoView({ behavior: "smooth", block: "start" }); }, 100); } else { console.error("Element not found"); } } }, [location]); useEffect(() => { fetchGames(); }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); const game = { title, source, minPlayers, maxPlayers, price, remoteId, }; try { const encoded = Game.encode(game).finish(); const res = await apiFetch("/api/game", { method: "POST", headers: { "Content-Type": "application/octet-stream", }, body: encoded, }); if (res.ok) { setMessage("success"); setTitle(""); setMinPlayers(1); setMaxPlayers(1); setPrice(0); setRemoteId(0); fetchGames(); setTimeout(() => setMessage(""), 3000); } else { setMessage("error"); setTimeout(() => setMessage(""), 3000); } } catch (err) { console.error(err); setMessage("error"); setTimeout(() => setMessage(""), 3000); } finally { setIsSubmitting(false); } }; const formCardStyles: React.CSSProperties = { background: "linear-gradient(135deg, var(--secondary-bg) 0%, var(--secondary-alt-bg) 100%)", borderRadius: "20px", padding: "0", maxWidth: "520px", boxShadow: "0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px var(--border-color)", overflow: "hidden", }; const formHeaderStyles: React.CSSProperties = { background: "linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)", padding: "1.5rem 2rem", display: "flex", alignItems: "center", gap: "1rem", }; const formBodyStyles: React.CSSProperties = { padding: "2rem", }; const sectionStyles: React.CSSProperties = { marginBottom: "1.5rem", }; const sectionTitleStyles: React.CSSProperties = { fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.1em", color: "var(--text-muted)", marginBottom: "1rem", display: "flex", alignItems: "center", gap: "0.5rem", }; const inputGroupStyles: React.CSSProperties = { position: "relative", marginBottom: "1rem", }; const labelStyles: React.CSSProperties = { fontSize: "0.85rem", color: "var(--text-muted)", marginBottom: "0.5rem", display: "block", fontWeight: 500, }; const inputStyles: React.CSSProperties = { width: "100%", padding: "0.875rem 1rem", backgroundColor: "var(--tertiary-bg)", border: "2px solid var(--border-color)", borderRadius: "12px", color: "var(--text-color)", fontSize: "1rem", transition: "all 0.2s ease", boxSizing: "border-box", }; const gridStyles: React.CSSProperties = { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem", }; const dividerStyles: React.CSSProperties = { height: "1px", background: "linear-gradient(90deg, transparent, var(--border-color), transparent)", margin: "1.5rem 0", }; const submitButtonStyles: React.CSSProperties = { width: "100%", padding: "1rem", background: "linear-gradient(135deg, var(--accent-color) 0%, var(--secondary-accent) 100%)", border: "none", borderRadius: "12px", color: "white", fontSize: "1rem", fontWeight: 600, cursor: isSubmitting ? "not-allowed" : "pointer", transition: "all 0.3s ease", display: "flex", alignItems: "center", justifyContent: "center", gap: "0.5rem", opacity: isSubmitting ? 0.7 : 1, transform: isSubmitting ? "none" : undefined, }; const messageStyles: React.CSSProperties = { padding: "1rem", borderRadius: "12px", marginBottom: "1.5rem", display: "flex", alignItems: "center", gap: "0.75rem", animation: "slideIn 0.3s ease", backgroundColor: message === "success" ? "rgba(76, 175, 80, 0.15)" : "rgba(244, 67, 54, 0.15)", border: `1px solid ${ message === "success" ? "rgba(76, 175, 80, 0.3)" : "rgba(244, 67, 54, 0.3)" }`, color: message === "success" ? "#4caf50" : "#f44336", }; return (
🎮

Add New Game

Add a game to your collection

{message && (
{message === "success" ? "✓" : "✕"} {message === "success" ? "Game added successfully!" : "Failed to add game. Please try again."}
)}
{/* Basic Info Section */}
📝 Basic Information
setTitle(e.target.value)} required placeholder="Enter game title..." style={inputStyles} className="add-game-input" />
{/* Player Count Section */}
👥 Player Count
setMinPlayers(Number(e.target.value))} min="1" style={inputStyles} className="add-game-input" />
setMaxPlayers(Number(e.target.value))} min="1" style={inputStyles} className="add-game-input" />
{/* Additional Info Section */}
💰 Additional Details
setPrice(Number(e.target.value))} min="0" step="0.01" style={inputStyles} className="add-game-input" />
setRemoteId(Number(e.target.value))} min="0" style={inputStyles} className="add-game-input" />

Existing Games

    {games.map((game) => { const opinion = opinions.find((op) => op.title === game.title); function handleOpinion(title: string, number: number): void { if (number == 2) { apiFetch("/api/opinion", { method: "PATCH", headers: { "Content-Type": "application/octet-stream", }, body: RemoveOpinionRequest.encode( AddOpinionRequest.create({ gameTitle: title, }) ).finish(), }) .then((res) => res.arrayBuffer()) .then((resBuffer) => { const response = PersonProto.decode( new Uint8Array(resBuffer) ); setOpinions(response.opinion); }) .catch((err) => { console.error(err); }); return; } const wouldPlay = number == 1; apiFetch(`/api/opinion`, { method: "POST", headers: { "Content-Type": "application/octet-stream", }, body: AddOpinionRequest.encode( AddOpinionRequest.create({ gameTitle: title, wouldPlay, }) ).finish(), }) .then((res) => res.arrayBuffer()) .then((resBuffer) => { const response = PersonProto.decode( new Uint8Array(resBuffer) ); setOpinions(response.opinion); }) .catch((err) => { console.error(err); }); } return (
    {game.title}
    ); })}
); }