Add profile photo uploads and custom difficulty support
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
"use client";
|
||||
import { useI18n } from "@/components/I18nProvider";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState, type FormEvent } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Timer, ZoomIn, Users } from "lucide-react";
|
||||
import { difficulties, type Difficulty } from "@/shared/protocol";
|
||||
import {
|
||||
difficulties,
|
||||
type DifficultyPreset,
|
||||
} from "@/shared/protocol";
|
||||
import { api } from "@/lib/client-api";
|
||||
export { api } from "@/lib/client-api";
|
||||
export function StartGame({
|
||||
@@ -16,13 +19,65 @@ export function StartGame({
|
||||
hostName: string | null;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>("normal"),
|
||||
const [difficulty, setDifficulty] = useState("normal"),
|
||||
[customPresets, setCustomPresets] = useState<DifficultyPreset[]>([]),
|
||||
[showDifficultyForm, setShowDifficultyForm] = useState(false),
|
||||
[difficultyName, setDifficultyName] = useState(""),
|
||||
[difficultySeconds, setDifficultySeconds] = useState(90),
|
||||
[difficultyZoom, setDifficultyZoom] = useState(true),
|
||||
[savingDifficulty, setSavingDifficulty] = useState(false),
|
||||
[mode, setMode] = useState<"solo" | "multiplayer">("solo"),
|
||||
[playerLimit, setPlayerLimit] = useState(4),
|
||||
[loading, setLoading] = useState(false),
|
||||
[error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
const starting = useRef(false);
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
api<{ presets: DifficultyPreset[] }>("difficulty-presets")
|
||||
.then(({ presets }) => {
|
||||
if (!disposed) setCustomPresets(presets);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, []);
|
||||
const options: DifficultyPreset[] = [
|
||||
...Object.entries(difficulties).map(([id, preset]) => ({
|
||||
id,
|
||||
name: preset.label,
|
||||
seconds: preset.seconds,
|
||||
zoom: preset.zoom,
|
||||
})),
|
||||
...customPresets,
|
||||
];
|
||||
async function saveDifficulty(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (savingDifficulty) return;
|
||||
setSavingDifficulty(true);
|
||||
setError("");
|
||||
try {
|
||||
const { preset } = await api<{ preset: DifficultyPreset }>(
|
||||
"difficulty-presets",
|
||||
{
|
||||
name: difficultyName.trim(),
|
||||
seconds: difficultySeconds,
|
||||
zoom: difficultyZoom,
|
||||
},
|
||||
);
|
||||
setCustomPresets((current) => [...current, preset]);
|
||||
setDifficulty(preset.id);
|
||||
setDifficultyName("");
|
||||
setDifficultySeconds(90);
|
||||
setDifficultyZoom(true);
|
||||
setShowDifficultyForm(false);
|
||||
} catch (failure) {
|
||||
setError((failure as Error).message);
|
||||
} finally {
|
||||
setSavingDifficulty(false);
|
||||
}
|
||||
}
|
||||
async function start() {
|
||||
if (starting.current) return;
|
||||
starting.current = true;
|
||||
@@ -67,27 +122,97 @@ export function StartGame({
|
||||
</button>
|
||||
</div>
|
||||
<div className="difficulty-options">
|
||||
{Object.entries(difficulties).map(([key, value]) => (
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={key}
|
||||
aria-pressed={difficulty === key}
|
||||
className={difficulty === key ? "selected" : ""}
|
||||
key={option.id}
|
||||
aria-pressed={difficulty === option.id}
|
||||
className={difficulty === option.id ? "selected" : ""}
|
||||
disabled={loading}
|
||||
onClick={() => setDifficulty(key as Difficulty)}
|
||||
onClick={() => setDifficulty(option.id)}
|
||||
>
|
||||
<strong>{t(value.label)}</strong>
|
||||
<strong>
|
||||
{option.id in difficulties ? t(option.name) : option.name}
|
||||
</strong>
|
||||
<span>
|
||||
<Timer size={13} />
|
||||
{value.seconds}
|
||||
{option.seconds}
|
||||
{t("s per round")}
|
||||
</span>
|
||||
<span>
|
||||
<ZoomIn size={13} />
|
||||
{value.zoom ? t("Zoom allowed") : t("No zoom")}
|
||||
{option.zoom ? t("Zoom allowed") : t("No zoom")}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="difficulty-library">
|
||||
<button
|
||||
type="button"
|
||||
className="text-link"
|
||||
aria-expanded={showDifficultyForm}
|
||||
onClick={() => {
|
||||
setShowDifficultyForm((visible) => !visible);
|
||||
setError("");
|
||||
}}
|
||||
disabled={loading || savingDifficulty}
|
||||
>
|
||||
{showDifficultyForm
|
||||
? t("Cancel")
|
||||
: "+ " + t("Create custom difficulty")}
|
||||
</button>
|
||||
{showDifficultyForm && (
|
||||
<form className="difficulty-builder" onSubmit={saveDifficulty}>
|
||||
<label htmlFor="custom-difficulty-name">
|
||||
{t("Difficulty name")}
|
||||
</label>
|
||||
<input
|
||||
id="custom-difficulty-name"
|
||||
value={difficultyName}
|
||||
onChange={(event) => setDifficultyName(event.target.value)}
|
||||
minLength={2}
|
||||
maxLength={40}
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder={t("Name this difficulty")}
|
||||
disabled={savingDifficulty || loading}
|
||||
/>
|
||||
<label htmlFor="custom-difficulty-seconds">
|
||||
{t("Round time")}
|
||||
</label>
|
||||
<div className="difficulty-builder-time">
|
||||
<input
|
||||
id="custom-difficulty-seconds"
|
||||
type="number"
|
||||
min={15}
|
||||
max={600}
|
||||
step={15}
|
||||
value={difficultySeconds}
|
||||
onChange={(event) =>
|
||||
setDifficultySeconds(Number(event.target.value))
|
||||
}
|
||||
required
|
||||
disabled={savingDifficulty || loading}
|
||||
/>
|
||||
<span>{t("s per round")}</span>
|
||||
</div>
|
||||
<label className="difficulty-builder-zoom">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={difficultyZoom}
|
||||
onChange={(event) => setDifficultyZoom(event.target.checked)}
|
||||
disabled={savingDifficulty || loading}
|
||||
/>
|
||||
{t("Allow zoom")}
|
||||
</label>
|
||||
<button
|
||||
className="button primary"
|
||||
disabled={savingDifficulty || loading}
|
||||
>
|
||||
{savingDifficulty ? t("Saving…") : t("Save difficulty")}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
<p className="muted">
|
||||
{t("5 rounds · 25,000 possible points · No movement")}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user