feat: Enhance game uniqueness validation to include remote ID and source, and redesign the add game form UI with submission state and feedback.

This commit is contained in:
code002lover 2025-12-04 18:36:47 +01:00
parent 9140498c6c
commit ff9d5632d7
2 changed files with 361 additions and 85 deletions

View File

@ -65,7 +65,9 @@ async fn add_game(
let mut games = game_list.lock().await; let mut games = game_list.lock().await;
let mut game = game.into_inner(); let mut game = game.into_inner();
if games.iter().any(|g| g.title == game.title) { if games.iter().any(|g| {
g.title == game.title || (g.remote_id == game.remote_id && g.source == game.source)
}) {
return None; return None;
} }

View File

@ -12,6 +12,7 @@ export function GameList() {
const [price, setPrice] = useState(0); const [price, setPrice] = useState(0);
const [remoteId, setRemoteId] = useState(0); const [remoteId, setRemoteId] = useState(0);
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const fetchGames = () => { const fetchGames = () => {
apiFetch("/api/games") apiFetch("/api/games")
@ -33,6 +34,7 @@ export function GameList() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true);
const game = { const game = {
title, title,
source, source,
@ -53,106 +55,378 @@ export function GameList() {
}); });
if (res.ok) { if (res.ok) {
setMessage("Game added successfully!"); setMessage("success");
setTitle(""); setTitle("");
setMinPlayers(1);
setMaxPlayers(1);
setPrice(0);
setRemoteId(0);
fetchGames(); fetchGames();
// Reset other fields if needed setTimeout(() => setMessage(""), 3000);
} else { } else {
setMessage("Failed to add game."); setMessage("error");
setTimeout(() => setMessage(""), 3000);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setMessage("Error adding game."); 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%, #0a4f8c 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%, #0a4f8c 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 ( return (
<div> <div>
<h2>Add New Game</h2> <style>
{message && ( {`
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.add-game-input:focus {
border-color: var(--accent-color) !important;
box-shadow: 0 0 0 4px rgba(9, 109, 192, 0.15) !important;
outline: none;
}
.add-game-input:hover:not(:focus) {
border-color: var(--text-muted);
}
.submit-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(9, 109, 192, 0.4);
}
.submit-btn:active:not(:disabled) {
transform: translateY(0);
}
`}
</style>
<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",
}}
>
Add New Game
</h2>
<p <p
style={{ color: message.includes("success") ? "#4caf50" : "#f44336" }} style={{
margin: 0,
fontSize: "0.9rem",
color: "rgba(255, 255, 255, 0.8)",
}}
> >
{message} Add a game to your collection
</p> </p>
</div>
</div>
<div style={formBodyStyles}>
{message && (
<div style={messageStyles}>
<span style={{ fontSize: "1.2rem" }}>
{message === "success" ? "✓" : "✕"}
</span>
<span style={{ fontWeight: 500 }}>
{message === "success"
? "Game added successfully!"
: "Failed to add game. Please try again."}
</span>
</div>
)} )}
<form
onSubmit={handleSubmit} <form onSubmit={handleSubmit}>
className="form-group" {/* Basic Info Section */}
style={{ maxWidth: "500px" }} <div style={sectionStyles}>
> <div style={sectionTitleStyles}>
<div className="form-group"> <span>📝</span>
<label>Title</label> Basic Information
</div>
<div style={inputGroupStyles}>
<label style={labelStyles}>Game Title</label>
<input <input
type="text" type="text"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
required required
placeholder="Game Title" placeholder="Enter game title..."
style={inputStyles}
className="add-game-input"
/> />
</div> </div>
<div className="form-group">
<label>Source</label> <div style={inputGroupStyles}>
<label style={labelStyles}>Platform Source</label>
<select <select
value={source} value={source}
onChange={(e) => setSource(Number(e.target.value))} 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.STEAM}>🎮 Steam</option>
<option value={Source.ROBLOX}>Roblox</option> <option value={Source.ROBLOX}>🟢 Roblox</option>
</select> </select>
</div> </div>
<div </div>
style={{
display: "grid", <div style={dividerStyles}></div>
gridTemplateColumns: "1fr 1fr",
gap: "1rem", {/* Player Count Section */}
}} <div style={sectionStyles}>
> <div style={sectionTitleStyles}>
<div className="form-group"> <span>👥</span>
<label>Min Players</label> Player Count
</div>
<div style={gridStyles}>
<div style={inputGroupStyles}>
<label style={labelStyles}>Minimum Players</label>
<input <input
type="number" type="number"
value={minPlayers} value={minPlayers}
onChange={(e) => setMinPlayers(Number(e.target.value))} onChange={(e) => setMinPlayers(Number(e.target.value))}
min="1"
style={inputStyles}
className="add-game-input"
/> />
</div> </div>
<div className="form-group"> <div style={inputGroupStyles}>
<label>Max Players</label> <label style={labelStyles}>Maximum Players</label>
<input <input
type="number" type="number"
value={maxPlayers} value={maxPlayers}
onChange={(e) => setMaxPlayers(Number(e.target.value))} onChange={(e) => setMaxPlayers(Number(e.target.value))}
min="1"
style={inputStyles}
className="add-game-input"
/> />
</div> </div>
</div> </div>
<div </div>
style={{
display: "grid", <div style={dividerStyles}></div>
gridTemplateColumns: "1fr 1fr",
gap: "1rem", {/* Additional Info Section */}
}} <div style={sectionStyles}>
> <div style={sectionTitleStyles}>
<div className="form-group"> <span>💰</span>
<label>Price</label> Additional Details
</div>
<div style={gridStyles}>
<div style={inputGroupStyles}>
<label style={labelStyles}>Price ($)</label>
<input <input
type="number" type="number"
value={price} value={price}
onChange={(e) => setPrice(Number(e.target.value))} onChange={(e) => setPrice(Number(e.target.value))}
min="0"
step="0.01"
style={inputStyles}
className="add-game-input"
/> />
</div> </div>
<div className="form-group"> <div style={inputGroupStyles}>
<label>Remote ID</label> <label style={labelStyles}>Remote ID</label>
<input <input
type="number" type="number"
value={remoteId} value={remoteId}
onChange={(e) => setRemoteId(Number(e.target.value))} onChange={(e) => setRemoteId(Number(e.target.value))}
min="0"
style={inputStyles}
className="add-game-input"
/> />
</div> </div>
</div> </div>
<button type="submit" style={{ marginTop: "1rem" }}> </div>
Add Game
<button
type="submit"
disabled={isSubmitting}
style={submitButtonStyles}
className="submit-btn"
>
{isSubmitting ? (
<>
<span
style={{
width: "18px",
height: "18px",
border: "2px solid rgba(255,255,255,0.3)",
borderTopColor: "white",
borderRadius: "50%",
animation: "spin 0.8s linear infinite",
}}
></span>
Adding Game...
</>
) : (
<>
<span style={{ fontSize: "1.1rem" }}></span>
Add Game to Collection
</>
)}
</button> </button>
</form> </form>
</div>
</div>
<style>
{`
@keyframes spin {
to { transform: rotate(360deg); }
}
`}
</style>
<div style={{ marginTop: "3rem" }}> <div style={{ marginTop: "3rem" }}>
<h3>Existing Games</h3> <h3>Existing Games</h3>