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

404 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}