Validate and deduplicate custom difficulty presets
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 6m4s

This commit is contained in:
2026-10-16 13:29:32 +05:00
parent 1a34eb71c2
commit 0bd05d6e0b
10 changed files with 427 additions and 41 deletions

View File

@@ -7,9 +7,11 @@ import { useRouter } from "next/navigation";
import { ArrowRight, Timer, ZoomIn, Users } from "lucide-react";
import {
difficulties,
customDifficultySchema,
type DifficultyPreset,
} from "@/shared/protocol";
import { api } from "@/lib/client-api";
import { mergeDifficultyPresets } from "@/shared/difficulty-presets";
export { api } from "@/lib/client-api";
export function StartGame({
datasetId,
@@ -32,13 +34,19 @@ export function StartGame({
[error, setError] = useState("");
const router = useRouter();
const starting = useRef(false);
const savingPreset = useRef(false);
useEffect(() => {
let disposed = false;
api<{ presets: DifficultyPreset[] }>("difficulty-presets")
.then(({ presets }) => {
if (!disposed) setCustomPresets(presets);
if (!disposed)
setCustomPresets((current) =>
mergeDifficultyPresets(current, presets),
);
})
.catch(() => {});
.catch((failure) => {
if (!disposed) setError(failure.message);
});
return () => {
disposed = true;
};
@@ -54,19 +62,25 @@ export function StartGame({
];
async function saveDifficulty(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (savingDifficulty) return;
if (savingPreset.current || starting.current) return;
const parsed = customDifficultySchema.safeParse({
name: difficultyName,
seconds: difficultySeconds,
zoom: difficultyZoom,
});
if (!parsed.success) {
setError(parsed.error.issues[0].message);
return;
}
savingPreset.current = true;
setSavingDifficulty(true);
setError("");
try {
const { preset } = await api<{ preset: DifficultyPreset }>(
"difficulty-presets",
{
name: difficultyName.trim(),
seconds: difficultySeconds,
zoom: difficultyZoom,
},
parsed.data,
);
setCustomPresets((current) => [...current, preset]);
setCustomPresets((current) => mergeDifficultyPresets(current, [preset]));
setDifficulty(preset.id);
setDifficultyName("");
setDifficultySeconds(90);
@@ -75,11 +89,12 @@ export function StartGame({
} catch (failure) {
setError((failure as Error).message);
} finally {
savingPreset.current = false;
setSavingDifficulty(false);
}
}
async function start() {
if (starting.current) return;
if (starting.current || savingPreset.current) return;
starting.current = true;
setLoading(true);
setError("");
@@ -168,17 +183,17 @@ export function StartGame({
<input
id="custom-difficulty-name"
value={difficultyName}
onChange={(event) => setDifficultyName(event.target.value)}
onChange={(event) =>
setDifficultyName([...event.target.value].slice(0, 40).join(""))
}
minLength={2}
maxLength={40}
maxLength={80}
required
autoComplete="off"
placeholder={t("Name this difficulty")}
disabled={savingDifficulty || loading}
/>
<label htmlFor="custom-difficulty-seconds">
{t("Round time")}
</label>
<label htmlFor="custom-difficulty-seconds">{t("Round time")}</label>
<div className="difficulty-builder-time">
<input
id="custom-difficulty-seconds"
@@ -253,7 +268,9 @@ export function StartGame({
<button
className="button primary full"
onClick={start}
disabled={loading || (mode === "multiplayer" && !hostName)}
disabled={
loading || savingDifficulty || (mode === "multiplayer" && !hostName)
}
>
{loading
? t("Preparing your world…")