game_list/frontend/src/GameDetails.tsx
2026-01-12 14:27:01 +01:00

143 lines
4.3 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 { useParams, useNavigate } from "react-router-dom";
import { Game, Source } from "../items";
import { apiFetch, get_is_admin } from "./api";
import { LoadingState, EmptyState, ErrorState } from "./components/EmptyState";
interface Props {
onShowToast?: (message: string, type?: "success" | "error" | "info") => void;
}
export function GameDetails({ onShowToast }: Props) {
const { title } = useParams<{ title: string }>();
const navigate = useNavigate();
const [game, setGame] = useState<Game | null>(null);
const [loading, setLoading] = useState(!!title);
const [error, setError] = useState<string | null>(title ? null : "Game title is missing");
const isAdmin = get_is_admin();
useEffect(() => {
if (!title) return;
(async () => {
try {
const res = await apiFetch(`/api/game/${encodeURIComponent(title)}`);
if (!res.ok) {
if (res.status === 404) {
throw new Error("Game not found");
}
throw new Error("Failed to load game");
}
const buffer = await res.arrayBuffer();
setGame(Game.decode(new Uint8Array(buffer)));
} catch (err) {
console.error(err);
setError(err instanceof Error ? err.message : "Failed to load game");
} finally {
setLoading(false);
}
})();
}, [title]);
const handleDelete = async () => {
if (
!confirm(
`Are you sure you want to delete "${game?.title}"? This action cannot be undone.`
)
) {
return;
}
try {
const res = await apiFetch(`/api/game/${encodeURIComponent(title || "")}`, {
method: "DELETE",
});
if (res.ok) {
onShowToast?.(`"${game?.title}" deleted successfully`, "success");
navigate("/games");
} else {
onShowToast?.("Failed to delete game", "error");
}
} catch (err) {
console.error(err);
onShowToast?.("An error occurred while deleting the game", "error");
}
};
if (loading) return <LoadingState message="Loading game details..." />;
if (error) return <ErrorState message={error} onRetry={() => navigate(0)} />;
if (!game) return <EmptyState icon="🎮" title="Game not found" description="This game doesn't exist or has been deleted" />;
const getExternalLink = () => {
if (game.source === Source.STEAM) {
return `https://store.steampowered.com/app/${game.remoteId}`;
} else if (game.source === Source.ROBLOX) {
return `https://www.roblox.com/games/${game.remoteId}`;
}
return "#";
};
return (
<div className="card" style={{ maxWidth: "600px", margin: "0 auto" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "1rem",
}}
>
<h2 style={{ margin: 0 }}>{game.title}</h2>
{isAdmin && (
<div style={{ display: "flex", gap: "0.5rem" }}>
<button
onClick={() => navigate(`/game/${encodeURIComponent(game.title)}/edit`)}
className="btn-secondary"
style={{ padding: "0.5rem 1rem", fontSize: "0.9rem" }}
>
Edit
</button>
<button
onClick={handleDelete}
className="btn-primary"
style={{
padding: "0.5rem 1rem",
fontSize: "0.9rem",
background: "#f44336",
border: "none",
}}
>
🗑 Delete
</button>
</div>
)}
</div>
<div style={{ display: "grid", gap: "1rem", marginTop: "1rem" }}>
<div>
<strong>Source:</strong>{" "}
{game.source === Source.STEAM ? "Steam" : "Roblox"}
</div>
<div>
<strong>Players:</strong> {game.minPlayers} - {game.maxPlayers}
</div>
<div>
<strong>Price:</strong> ${game.price}
</div>
</div>
<div style={{ marginTop: "2rem" }}>
<a
href={getExternalLink()}
target="_blank"
rel="noopener noreferrer"
className="btn-primary"
style={{ textDecoration: "none", display: "inline-block" }}
>
View on {game.source === Source.STEAM ? "Steam" : "Roblox"}
</a>
</div>
</div>
);
}