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:
parent
9140498c6c
commit
ff9d5632d7
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 && (
|
{`
|
||||||
<p
|
@keyframes slideIn {
|
||||||
style={{ color: message.includes("success") ? "#4caf50" : "#f44336" }}
|
from {
|
||||||
>
|
opacity: 0;
|
||||||
{message}
|
transform: translateY(-10px);
|
||||||
</p>
|
}
|
||||||
)}
|
to {
|
||||||
<form
|
opacity: 1;
|
||||||
onSubmit={handleSubmit}
|
transform: translateY(0);
|
||||||
className="form-group"
|
}
|
||||||
style={{ maxWidth: "500px" }}
|
}
|
||||||
>
|
.add-game-input:focus {
|
||||||
<div className="form-group">
|
border-color: var(--accent-color) !important;
|
||||||
<label>Title</label>
|
box-shadow: 0 0 0 4px rgba(9, 109, 192, 0.15) !important;
|
||||||
<input
|
outline: none;
|
||||||
type="text"
|
}
|
||||||
value={title}
|
.add-game-input:hover:not(:focus) {
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
border-color: var(--text-muted);
|
||||||
required
|
}
|
||||||
placeholder="Game Title"
|
.submit-btn:hover:not(:disabled) {
|
||||||
/>
|
transform: translateY(-2px);
|
||||||
</div>
|
box-shadow: 0 8px 20px rgba(9, 109, 192, 0.4);
|
||||||
<div className="form-group">
|
}
|
||||||
<label>Source</label>
|
.submit-btn:active:not(:disabled) {
|
||||||
<select
|
transform: translateY(0);
|
||||||
value={source}
|
}
|
||||||
onChange={(e) => setSource(Number(e.target.value))}
|
`}
|
||||||
|
</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",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value={Source.STEAM}>Steam</option>
|
🎮
|
||||||
<option value={Source.ROBLOX}>Roblox</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Min Players</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={minPlayers}
|
|
||||||
onChange={(e) => setMinPlayers(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div>
|
||||||
<label>Max Players</label>
|
<h2
|
||||||
<input
|
style={{
|
||||||
type="number"
|
margin: 0,
|
||||||
value={maxPlayers}
|
fontSize: "1.5rem",
|
||||||
onChange={(e) => setMaxPlayers(Number(e.target.value))}
|
fontWeight: 700,
|
||||||
/>
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add New Game
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
color: "rgba(255, 255, 255, 0.8)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add a game to your collection
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
style={{
|
<div style={formBodyStyles}>
|
||||||
display: "grid",
|
{message && (
|
||||||
gridTemplateColumns: "1fr 1fr",
|
<div style={messageStyles}>
|
||||||
gap: "1rem",
|
<span style={{ fontSize: "1.2rem" }}>
|
||||||
}}
|
{message === "success" ? "✓" : "✕"}
|
||||||
>
|
</span>
|
||||||
<div className="form-group">
|
<span style={{ fontWeight: 500 }}>
|
||||||
<label>Price</label>
|
{message === "success"
|
||||||
<input
|
? "Game added successfully!"
|
||||||
type="number"
|
: "Failed to add game. Please try again."}
|
||||||
value={price}
|
</span>
|
||||||
onChange={(e) => setPrice(Number(e.target.value))}
|
</div>
|
||||||
/>
|
)}
|
||||||
</div>
|
|
||||||
<div className="form-group">
|
<form onSubmit={handleSubmit}>
|
||||||
<label>Remote ID</label>
|
{/* Basic Info Section */}
|
||||||
<input
|
<div style={sectionStyles}>
|
||||||
type="number"
|
<div style={sectionTitleStyles}>
|
||||||
value={remoteId}
|
<span>📝</span>
|
||||||
onChange={(e) => setRemoteId(Number(e.target.value))}
|
Basic Information
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div style={inputGroupStyles}>
|
||||||
|
<label style={labelStyles}>Game Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(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>
|
||||||
|
|
||||||
|
{/* Player Count Section */}
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Additional Info Section */}
|
||||||
|
<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))}
|
||||||
|
min="0"
|
||||||
|
style={inputStyles}
|
||||||
|
className="add-game-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" style={{ marginTop: "1rem" }}>
|
</div>
|
||||||
Add Game
|
|
||||||
</button>
|
<style>
|
||||||
</form>
|
{`
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div style={{ marginTop: "3rem" }}>
|
<div style={{ marginTop: "3rem" }}>
|
||||||
<h3>Existing Games</h3>
|
<h3>Existing Games</h3>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user