404 lines
12 KiB
TypeScript
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(Number(e.target.value))}
|
||
min="0"
|
||
step="0.01"
|
||
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>
|
||
);
|
||
}
|