Files
game_list/frontend/src/EditGame.tsx
T
2026-01-12 11:19:21 +01:00

404 lines
12 KiB
TypeScript

import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Game, Source } from "../items";
import { apiFetch } from "./api";
import type { ToastType } from "./Toast";
interface Props {
onShowToast?: (message: string, type?: ToastType) => void;
}
export function EditGame({ onShowToast }: Props) {
const { title } = useParams<{ title: string }>();
const navigate = useNavigate();
const [game, setGame] = useState<Game | null>(null);
const [newTitle, setNewTitle] = useState("");
const [source, setSource] = useState<Source>(Source.STEAM);
const [minPlayers, setMinPlayers] = useState(1);
const [maxPlayers, setMaxPlayers] = useState(1);
const [price, setPrice] = useState(0);
const [remoteId, setRemoteId] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [remoteIdError, setRemoteIdError] = useState("");
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!title) return;
apiFetch(`/api/game/${encodeURIComponent(title)}`)
.then(async (res) => {
if (!res.ok) throw new Error("Game not found");
return Game.decode(new Uint8Array(await res.arrayBuffer()));
})
.then((data) => {
setGame(data);
setNewTitle(data.title);
setSource(data.source);
setMinPlayers(data.minPlayers);
setMaxPlayers(data.maxPlayers);
setPrice(data.price);
setRemoteId(data.remoteId);
setLoading(false);
})
.catch((err) => {
console.error(err);
onShowToast?.("Failed to load game", "error");
setLoading(false);
});
}, [title, onShowToast]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (remoteId === 0) {
setRemoteIdError("Remote ID must be greater than 0");
return;
}
if (!newTitle.trim()) {
onShowToast?.("Game title is required", "error");
return;
}
setRemoteIdError("");
setIsSubmitting(true);
const updatedGame = {
title: newTitle.trim(),
source,
minPlayers,
maxPlayers,
price,
remoteId,
};
try {
const encoded = Game.encode(updatedGame).finish();
const res = await apiFetch("/api/game", {
method: "PATCH",
headers: {
"Content-Type": "application/octet-stream",
},
body: encoded,
});
if (res.ok) {
onShowToast?.("Game updated successfully!", "success");
navigate(`/game/${encodeURIComponent(newTitle.trim())}`);
} else {
onShowToast?.("Failed to update game. Please try again.", "error");
}
} catch (err) {
console.error(err);
onShowToast?.("An error occurred while updating the game.", "error");
} finally {
setIsSubmitting(false);
}
};
if (loading) return <div>Loading...</div>;
if (!game) return <div>Game not found</div>;
const formCardStyles: React.CSSProperties = {
background:
"linear-gradient(135deg, var(--secondary-bg) 0%, var(--secondary-alt-bg) 100%)",
borderRadius: "20px",
padding: "0",
maxWidth: "520px",
margin: "0 auto",
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 buttonStyles: React.CSSProperties = {
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 cancelStyles: React.CSSProperties = {
...buttonStyles,
background: "var(--tertiary-bg)",
color: "var(--text-color)",
border: "2px solid var(--border-color)",
};
return (
<div>
<div style={formCardStyles}>
<div style={formHeaderStyles}>
<div
style={{
width: "48px",
height: "48px",
borderRadius: "14px",
backgroundColor: "rgba(255, 255, 255, 0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "1.5rem",
}}
>
✏️
</div>
<div>
<h2
style={{
margin: 0,
fontSize: "1.5rem",
fontWeight: 700,
color: "white",
}}
>
Edit Game
</h2>
<p
style={{
margin: 0,
fontSize: "0.9rem",
color: "rgba(255, 255, 255, 0.8)",
}}
>
Update game information
</p>
</div>
</div>
<div style={formBodyStyles}>
<form onSubmit={handleSubmit}>
<div style={sectionStyles}>
<div style={sectionTitleStyles}>
<span>📝</span>
Basic Information
</div>
<div style={inputGroupStyles}>
<label style={labelStyles}>Game Title</label>
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
required
placeholder="Enter game title..."
style={inputStyles}
className="add-game-input"
/>
</div>
<div style={inputGroupStyles}>
<label style={labelStyles}>Platform Source</label>
<select
value={source}
onChange={(e) => setSource(Number(e.target.value))}
style={{
...inputStyles,
cursor: "pointer",
appearance: "none",
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23a0a0a0' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E")`,
backgroundRepeat: "no-repeat",
backgroundPosition: "right 1rem center",
paddingRight: "2.5rem",
}}
className="add-game-input"
>
<option value={Source.STEAM}>🎮 Steam</option>
<option value={Source.ROBLOX}>🟢 Roblox</option>
</select>
</div>
</div>
<div style={dividerStyles}></div>
<div style={sectionStyles}>
<div style={sectionTitleStyles}>
<span>👥</span>
Player Count
</div>
<div style={gridStyles}>
<div style={inputGroupStyles}>
<label style={labelStyles}>Minimum Players</label>
<input
type="number"
value={minPlayers}
onChange={(e) => setMinPlayers(Number(e.target.value))}
min="1"
style={inputStyles}
className="add-game-input"
/>
</div>
<div style={inputGroupStyles}>
<label style={labelStyles}>Maximum Players</label>
<input
type="number"
value={maxPlayers}
onChange={(e) => setMaxPlayers(Number(e.target.value))}
min="1"
style={inputStyles}
className="add-game-input"
/>
</div>
</div>
</div>
<div style={dividerStyles}></div>
<div style={sectionStyles}>
<div style={sectionTitleStyles}>
<span>💰</span>
Additional Details
</div>
<div style={gridStyles}>
<div style={inputGroupStyles}>
<label style={labelStyles}>Price ()</label>
<input
type="number"
value={price}
onChange={(e) => setPrice(Math.ceil(Number(e.target.value.replace(',', '.'))))}
min="0"
step="1"
style={inputStyles}
className="add-game-input"
/>
</div>
<div style={inputGroupStyles}>
<label style={labelStyles}>Remote ID</label>
<input
type="number"
value={remoteId}
onChange={(e) => {
setRemoteId(Number(e.target.value));
setRemoteIdError("");
}}
min="0"
style={{
...inputStyles,
borderColor: remoteIdError ? "#f44336" : undefined,
}}
className="add-game-input"
/>
{remoteIdError && (
<div
style={{
color: "#f44336",
fontSize: "0.75rem",
marginTop: "0.25rem",
}}
>
{remoteIdError}
</div>
)}
</div>
</div>
</div>
<div style={gridStyles}>
<button
type="button"
onClick={() => navigate(`/game/${encodeURIComponent(game.title)}`)}
disabled={isSubmitting}
style={cancelStyles}
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
style={buttonStyles}
>
{isSubmitting ? (
<>Updating...</>
) : (
<>
<span style={{ fontSize: "1.1rem" }}>💾</span>
Save Changes
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
);
}