Initial commit
This commit is contained in:
624
resources/js/components/RoundCreateForm.tsx
Normal file
624
resources/js/components/RoundCreateForm.tsx
Normal file
@@ -0,0 +1,624 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Button, Input, Switch, Textarea } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLanguageStore } from "@/stores/languageStore";
|
||||
|
||||
type TranslationPayload = {
|
||||
cs?: string;
|
||||
en?: string;
|
||||
};
|
||||
|
||||
type ContestOption = {
|
||||
id: number;
|
||||
name: string | TranslationPayload;
|
||||
bands?: { id: number; name: string }[];
|
||||
categories?: { id: number; name: string }[];
|
||||
power_categories?: { id: number; name: string }[];
|
||||
rule_set_id?: number | null;
|
||||
duration?: number;
|
||||
logs_deadline_days?: number;
|
||||
};
|
||||
|
||||
export type RoundFromApi = {
|
||||
id: number;
|
||||
contest_id: number;
|
||||
name: string | TranslationPayload;
|
||||
description?: string | TranslationPayload | null;
|
||||
bands?: { id: number; name: string }[];
|
||||
categories?: { id: number; name: string }[];
|
||||
power_categories?: { id: number; name: string }[];
|
||||
rule_set_id?: number | null;
|
||||
|
||||
is_active: boolean;
|
||||
is_test: boolean;
|
||||
is_sixhr: boolean;
|
||||
|
||||
start_time: string | null;
|
||||
end_time: string | null;
|
||||
logs_deadline: string | null;
|
||||
};
|
||||
|
||||
type RoundFormMode = "create" | "edit";
|
||||
|
||||
type RoundCreateFormProps = {
|
||||
mode?: RoundFormMode; // default "create"
|
||||
round?: RoundFromApi | null; // pro edit
|
||||
onCreated?: (round: RoundFromApi) => void;
|
||||
onUpdated?: (round: RoundFromApi) => void;
|
||||
contestId?: number | null;
|
||||
};
|
||||
|
||||
const buildTranslationPayload = (cs: string, en: string): TranslationPayload => {
|
||||
const trimmedCs = cs.trim();
|
||||
const trimmedEn = en.trim();
|
||||
|
||||
if (!trimmedCs && !trimmedEn) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (trimmedCs && trimmedEn) {
|
||||
return {
|
||||
cs: trimmedCs,
|
||||
en: trimmedEn,
|
||||
};
|
||||
}
|
||||
|
||||
const value = trimmedCs || trimmedEn;
|
||||
return {
|
||||
cs: value,
|
||||
en: value,
|
||||
};
|
||||
};
|
||||
|
||||
const extractTranslations = (
|
||||
field: string | TranslationPayload | null | undefined
|
||||
): TranslationPayload => {
|
||||
if (!field) return {};
|
||||
if (typeof field === "string") return { cs: field };
|
||||
return field;
|
||||
};
|
||||
|
||||
const toDatetimeLocal = (value: string | null): string => {
|
||||
if (!value) return "";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
const pad = (n: number) => `${n}`.padStart(2, "0");
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
const normalizeDatetime = (value: string): string | undefined => {
|
||||
if (!value.trim()) return undefined;
|
||||
return value;
|
||||
};
|
||||
|
||||
const isSixHourBandName = (name?: string | null) => {
|
||||
if (!name) return false;
|
||||
const lower = name.toLowerCase();
|
||||
return lower.includes("145") || lower.includes("435");
|
||||
};
|
||||
|
||||
export default function RoundCreateForm({
|
||||
mode = "create",
|
||||
round,
|
||||
onCreated,
|
||||
onUpdated,
|
||||
contestId: forcedContestId = null,
|
||||
}: RoundCreateFormProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const locale = useLanguageStore((s) => s.locale);
|
||||
|
||||
const isEdit = mode === "edit" && round != null;
|
||||
|
||||
const [contestId, setContestId] = useState<string>(forcedContestId ? String(forcedContestId) : "");
|
||||
const [contests, setContests] = useState<ContestOption[]>([]);
|
||||
const [loadingContests, setLoadingContests] = useState(false);
|
||||
|
||||
const [nameCs, setNameCs] = useState("");
|
||||
const [nameEn, setNameEn] = useState("");
|
||||
const [descriptionCs, setDescriptionCs] = useState("");
|
||||
const [descriptionEn, setDescriptionEn] = useState("");
|
||||
|
||||
const [startTime, setStartTime] = useState("");
|
||||
const [endTime, setEndTime] = useState("");
|
||||
const [logsDeadline, setLogsDeadline] = useState("");
|
||||
const [contestDurationHours, setContestDurationHours] = useState<number | null>(null);
|
||||
const [contestDeadlineDays, setContestDeadlineDays] = useState<number | null>(null);
|
||||
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [isTest, setIsTest] = useState(false);
|
||||
const [isSixHr, setIsSixHr] = useState(false);
|
||||
|
||||
const [availableBands, setAvailableBands] = useState<ContestOption["bands"]>([]);
|
||||
const [availableCategories, setAvailableCategories] = useState<ContestOption["categories"]>([]);
|
||||
const [availablePowerCategories, setAvailablePowerCategories] = useState<ContestOption["power_categories"]>([]);
|
||||
|
||||
const [selectedBandIds, setSelectedBandIds] = useState<number[]>([]);
|
||||
const [selectedCategoryIds, setSelectedCategoryIds] = useState<number[]>([]);
|
||||
const [selectedPowerCategoryIds, setSelectedPowerCategoryIds] = useState<number[]>([]);
|
||||
const [availableRuleSets, setAvailableRuleSets] = useState<{ id: number; name: string; code?: string | null }[]>([]);
|
||||
const [selectedRuleSetId, setSelectedRuleSetId] = useState<number | null>(null);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const hasAllowedSixHrBand = useMemo(() => {
|
||||
if (!selectedBandIds.length) return false;
|
||||
return availableBands.some((b) => selectedBandIds.includes(b.id) && isSixHourBandName(b.name));
|
||||
}, [availableBands, selectedBandIds]);
|
||||
|
||||
// načti seznam závodů pro select
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
setLoadingContests(true);
|
||||
const [contestsRes, bandsRes, categoriesRes, powerCatsRes, ruleSetsRes] = await Promise.all([
|
||||
axios.get<ContestOption[] | { data: ContestOption[] }>("/api/contests", {
|
||||
withCredentials: true,
|
||||
headers: { Accept: "application/json" },
|
||||
params: { lang: locale },
|
||||
}),
|
||||
axios.get("/api/bands", { headers: { Accept: "application/json" } }),
|
||||
axios.get("/api/categories", { headers: { Accept: "application/json" } }),
|
||||
axios.get("/api/power-categories", { headers: { Accept: "application/json" } }),
|
||||
axios.get("/api/evaluation-rule-sets", { headers: { Accept: "application/json" } }),
|
||||
]);
|
||||
if (!active) return;
|
||||
const normalize = (res: any) => (Array.isArray(res?.data) ? res.data : res?.data?.data ?? res.data ?? []);
|
||||
setContests(normalize(contestsRes));
|
||||
setAvailableBands(normalize(bandsRes));
|
||||
setAvailableCategories(normalize(categoriesRes));
|
||||
setAvailablePowerCategories(normalize(powerCatsRes));
|
||||
const ruleSets = normalize(ruleSetsRes);
|
||||
setAvailableRuleSets(ruleSets);
|
||||
if (!isEdit && !selectedRuleSetId) {
|
||||
const defaultRuleSet = ruleSets.find((item: any) => item.code === "default_vhf_compat");
|
||||
if (defaultRuleSet) {
|
||||
setSelectedRuleSetId(defaultRuleSet.id);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (!active) return;
|
||||
setError(t("unable_to_load_contests") ?? "Nepodařilo se načíst seznam závodů.");
|
||||
} finally {
|
||||
if (active) setLoadingContests(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [locale, t]);
|
||||
|
||||
// předvyplnění při editaci
|
||||
useEffect(() => {
|
||||
if (!isEdit || !round) return;
|
||||
|
||||
const name = extractTranslations(round.name);
|
||||
const desc = extractTranslations(round.description ?? null);
|
||||
|
||||
setContestId(String(round.contest_id ?? ""));
|
||||
setNameCs(name.cs ?? "");
|
||||
setNameEn(name.en ?? "");
|
||||
setDescriptionCs(desc.cs ?? "");
|
||||
setDescriptionEn(desc.en ?? "");
|
||||
|
||||
setStartTime(toDatetimeLocal(round.start_time));
|
||||
setEndTime(toDatetimeLocal(round.end_time));
|
||||
setLogsDeadline(toDatetimeLocal(round.logs_deadline));
|
||||
|
||||
setIsActive(!!round.is_active);
|
||||
setIsTest(!!round.is_test);
|
||||
setIsSixHr(!!round.is_sixhr);
|
||||
|
||||
setSelectedBandIds(Array.isArray((round as any).bands) ? (round as any).bands.map((b: any) => b.id) : []);
|
||||
setSelectedCategoryIds(Array.isArray((round as any).categories) ? (round as any).categories.map((c: any) => c.id) : []);
|
||||
setSelectedPowerCategoryIds(Array.isArray((round as any).power_categories) ? (round as any).power_categories.map((p: any) => p.id) : []);
|
||||
setSelectedRuleSetId((round as any).rule_set_id ?? null);
|
||||
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
}, [isEdit, round]);
|
||||
|
||||
// pokud při editaci chybí vazby, doťukej detail kola
|
||||
useEffect(() => {
|
||||
if (!isEdit || !round) return;
|
||||
const needsDetail =
|
||||
!(Array.isArray((round as any).bands) && (round as any).bands.length) ||
|
||||
!(Array.isArray((round as any).categories) && (round as any).categories.length) ||
|
||||
!(Array.isArray((round as any).power_categories) && (round as any).power_categories.length);
|
||||
if (!needsDetail) return;
|
||||
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get(`/api/rounds/${round.id}`, {
|
||||
headers: { Accept: "application/json" },
|
||||
params: { lang: locale },
|
||||
withCredentials: true,
|
||||
});
|
||||
if (!active) return;
|
||||
const data = res.data;
|
||||
setSelectedBandIds(Array.isArray(data.bands) ? data.bands.map((b: any) => b.id) : []);
|
||||
setSelectedCategoryIds(Array.isArray(data.categories) ? data.categories.map((c: any) => c.id) : []);
|
||||
setSelectedPowerCategoryIds(Array.isArray(data.power_categories) ? data.power_categories.map((p: any) => p.id) : []);
|
||||
|
||||
setIsActive(!!data.is_active);
|
||||
setIsTest(!!data.is_test);
|
||||
setIsSixHr(!!data.is_sixhr);
|
||||
setStartTime(toDatetimeLocal(data.start_time));
|
||||
setEndTime(toDatetimeLocal(data.end_time));
|
||||
setLogsDeadline(toDatetimeLocal(data.logs_deadline));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { active = false; };
|
||||
}, [isEdit, round, locale]);
|
||||
|
||||
// pokud máme zvolený contest, načti detail pro defaulty (vazby) a parametry
|
||||
useEffect(() => {
|
||||
if (!contestId) return;
|
||||
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get<ContestOption>(`/api/contests/${contestId}`, {
|
||||
headers: { Accept: "application/json" },
|
||||
params: { lang: locale },
|
||||
withCredentials: true,
|
||||
});
|
||||
if (!active) return;
|
||||
const data = res.data;
|
||||
if (!isEdit) {
|
||||
if (data?.bands) setSelectedBandIds(data.bands.map((b) => b.id));
|
||||
if (data?.categories) setSelectedCategoryIds(data.categories.map((c) => c.id));
|
||||
if (data?.power_categories) setSelectedPowerCategoryIds(data.power_categories.map((p) => p.id));
|
||||
if (data?.rule_set_id) setSelectedRuleSetId(data.rule_set_id);
|
||||
}
|
||||
if (typeof data.duration === "number") setContestDurationHours(data.duration);
|
||||
if (typeof data.logs_deadline_days === "number") setContestDeadlineDays(data.logs_deadline_days);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { active = false; };
|
||||
}, [contestId, locale, isEdit]);
|
||||
|
||||
// Auto-nastavení konce kola podle startu a délky závodu
|
||||
useEffect(() => {
|
||||
if (!startTime || endTime || contestDurationHours == null) return;
|
||||
const startDate = new Date(startTime);
|
||||
if (Number.isNaN(startDate.getTime())) return;
|
||||
const endDate = new Date(startDate);
|
||||
endDate.setHours(endDate.getHours() + contestDurationHours);
|
||||
setEndTime(toDatetimeLocal(endDate.toISOString()));
|
||||
}, [startTime, endTime, contestDurationHours]);
|
||||
|
||||
// Auto-nastavení uzávěrky logů při vyplnění/změně konce
|
||||
useEffect(() => {
|
||||
if (!endTime || contestDeadlineDays == null) return;
|
||||
const endDate = new Date(endTime);
|
||||
if (Number.isNaN(endDate.getTime())) return;
|
||||
const deadlineDate = new Date(endDate);
|
||||
deadlineDate.setDate(deadlineDate.getDate() + contestDeadlineDays);
|
||||
setLogsDeadline(toDatetimeLocal(deadlineDate.toISOString()));
|
||||
}, [endTime, contestDeadlineDays]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSixHr && !hasAllowedSixHrBand) {
|
||||
setIsSixHr(false);
|
||||
}
|
||||
}, [hasAllowedSixHrBand, isSixHr]);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
if (!contestId.trim()) {
|
||||
setError(t("Vyber závod.") ?? "Vyber závod.");
|
||||
return;
|
||||
}
|
||||
|
||||
const namePayload = buildTranslationPayload(nameCs, nameEn);
|
||||
if (Object.keys(namePayload).length === 0) {
|
||||
setError(t("Vyplň alespoň jeden překlad názvu kola.") ?? "Vyplň alespoň jeden překlad názvu kola.");
|
||||
return;
|
||||
}
|
||||
|
||||
const descriptionPayload = buildTranslationPayload(descriptionCs, descriptionEn);
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
contest_id: Number(contestId),
|
||||
name: namePayload,
|
||||
is_active: isActive,
|
||||
is_test: isTest,
|
||||
is_sixhr: isSixHr,
|
||||
};
|
||||
|
||||
if (Object.keys(descriptionPayload).length > 0) {
|
||||
payload.description = descriptionPayload;
|
||||
}
|
||||
|
||||
const normalizedStart = normalizeDatetime(startTime);
|
||||
const normalizedEnd = normalizeDatetime(endTime);
|
||||
const normalizedDeadline = normalizeDatetime(logsDeadline);
|
||||
|
||||
if (normalizedStart) payload.start_time = normalizedStart;
|
||||
if (normalizedEnd) payload.end_time = normalizedEnd;
|
||||
if (normalizedDeadline) payload.logs_deadline = normalizedDeadline;
|
||||
|
||||
payload.band_ids = selectedBandIds;
|
||||
payload.category_ids = selectedCategoryIds;
|
||||
payload.power_category_ids = selectedPowerCategoryIds;
|
||||
if (selectedRuleSetId) payload.rule_set_id = selectedRuleSetId;
|
||||
|
||||
if (isSixHr && !hasAllowedSixHrBand) {
|
||||
payload.is_sixhr = false;
|
||||
setIsSixHr(false);
|
||||
setError(t("six_hr_band_warning") ?? "6H kategorie je povolena jen pro pásma 145 MHz a 435 MHz.");
|
||||
}
|
||||
|
||||
// 6h jen pro pásma 145 / 435 – pokud nesplňuje, vypni is_sixhr
|
||||
if (isSixHr) {
|
||||
const bands = (round as any)?.bands ?? [];
|
||||
const currentBands = (bands.length ? bands.map((b: any) => b.id) : selectedBandIds) ?? [];
|
||||
const hasAllowedBand = (round as any)?.bands
|
||||
? bands.some((b: any) => isSixHourBandName(b.name))
|
||||
: selectedBandIds.some((id) => {
|
||||
const found = (round as any)?.availableBands?.find?.((b: any) => b.id === id);
|
||||
return found ? isSixHourBandName(found.name) : true;
|
||||
});
|
||||
if (!hasAllowedBand) {
|
||||
payload.is_sixhr = false;
|
||||
setIsSixHr(false);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await axios.get("/sanctum/csrf-cookie", { withCredentials: true });
|
||||
|
||||
let response;
|
||||
|
||||
if (isEdit && round) {
|
||||
response = await axios.put(`/api/rounds/${round.id}`, payload, {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
withXSRFToken: true,
|
||||
});
|
||||
setSuccess(t("Kolo bylo upraveno.") ?? "Kolo bylo upraveno.");
|
||||
onUpdated?.(response.data as RoundFromApi);
|
||||
} else {
|
||||
response = await axios.post("/api/rounds", payload, {
|
||||
headers: { Accept: "application/json" },
|
||||
withCredentials: true,
|
||||
withXSRFToken: true,
|
||||
});
|
||||
setSuccess(t("Kolo bylo vytvořeno.") ?? "Kolo bylo vytvořeno.");
|
||||
onCreated?.(response.data as RoundFromApi);
|
||||
// reset formuláře po vytvoření
|
||||
setNameCs("");
|
||||
setNameEn("");
|
||||
setDescriptionCs("");
|
||||
setDescriptionEn("");
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
setLogsDeadline("");
|
||||
setIsActive(true);
|
||||
setIsTest(false);
|
||||
setIsSixHr(false);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
setError(e.response?.data?.message ?? "Chyba při ukládání kola.");
|
||||
} else {
|
||||
setError("Chyba při ukládání kola.");
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
{t("contest_name") ?? "Závod"}
|
||||
</label>
|
||||
<select
|
||||
value={contestId}
|
||||
onChange={(e) => setContestId(e.target.value)}
|
||||
disabled={loadingContests || submitting || !!forcedContestId || isEdit}
|
||||
className="w-full border rounded px-3 py-2 text-sm bg-white dark:bg-gray-900"
|
||||
required
|
||||
>
|
||||
<option value="">{t("Vyber závod") ?? "Vyber závod"}</option>
|
||||
{contests.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{typeof c.name === "string" ? c.name : c.name[locale] ?? c.name["en"] ?? c.name["cs"] ?? c.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Input
|
||||
label={t("round_name") ?? "Název (cs)"}
|
||||
labelPlacement="outside"
|
||||
placeholder="Kolo (cs)"
|
||||
value={nameCs}
|
||||
onChange={(e) => setNameCs(e.target.value)}
|
||||
isRequired
|
||||
/>
|
||||
<Input
|
||||
label={t("round_name") ?? "Název (en)"}
|
||||
labelPlacement="outside"
|
||||
placeholder="Round (en)"
|
||||
value={nameEn}
|
||||
onChange={(e) => setNameEn(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<Textarea
|
||||
label={t("round_description") ?? "Popis (cs)"}
|
||||
labelPlacement="outside"
|
||||
placeholder="Popis kola (cs)"
|
||||
value={descriptionCs}
|
||||
onChange={(e) => setDescriptionCs(e.target.value)}
|
||||
minRows={2}
|
||||
/>
|
||||
<Textarea
|
||||
label={t("round_description") ?? "Popis (en)"}
|
||||
labelPlacement="outside"
|
||||
placeholder="Round description (en)"
|
||||
value={descriptionEn}
|
||||
onChange={(e) => setDescriptionEn(e.target.value)}
|
||||
minRows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<Input
|
||||
type="datetime-local"
|
||||
label={t("Začátek") ?? "Začátek"}
|
||||
labelPlacement="outside-left"
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.target.value)}
|
||||
isRequired
|
||||
/>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
label={t("Konec") ?? "Konec"}
|
||||
labelPlacement="outside-left"
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
isRequired
|
||||
/>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
label={t("Uzávěrka logů") ?? "Uzávěrka logů"}
|
||||
labelPlacement="outside-left"
|
||||
value={logsDeadline}
|
||||
onChange={(e) => setLogsDeadline(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Switch
|
||||
isSelected={isActive}
|
||||
onValueChange={setIsActive}
|
||||
>
|
||||
{t("round_active") ?? "Aktivní"}
|
||||
</Switch>
|
||||
<Switch
|
||||
isSelected={isTest}
|
||||
onValueChange={setIsTest}
|
||||
>
|
||||
{t("round_test") ?? "Testovací"}
|
||||
</Switch>
|
||||
<Switch
|
||||
isSelected={isSixHr}
|
||||
onValueChange={setIsSixHr}
|
||||
isDisabled={!hasAllowedSixHrBand}
|
||||
>
|
||||
6h
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold mb-2">Pásma</p>
|
||||
<div className="space-y-2">
|
||||
{availableBands?.map((band) => (
|
||||
<label key={band.id} className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedBandIds.includes(band.id)}
|
||||
onChange={() =>
|
||||
setSelectedBandIds((prev) =>
|
||||
prev.includes(band.id) ? prev.filter((id) => id !== band.id) : [...prev, band.id]
|
||||
)
|
||||
}
|
||||
disabled={loadingContests || submitting}
|
||||
/>
|
||||
<span>{band.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-semibold mb-2">Kategorie</p>
|
||||
<div className="space-y-2">
|
||||
{availableCategories?.map((cat) => (
|
||||
<label key={cat.id} className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedCategoryIds.includes(cat.id)}
|
||||
onChange={() =>
|
||||
setSelectedCategoryIds((prev) =>
|
||||
prev.includes(cat.id) ? prev.filter((id) => id !== cat.id) : [...prev, cat.id]
|
||||
)
|
||||
}
|
||||
disabled={loadingContests || submitting}
|
||||
/>
|
||||
<span>{cat.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-semibold mb-2">Výkonové kategorie</p>
|
||||
<div className="space-y-2">
|
||||
{availablePowerCategories?.map((p) => (
|
||||
<label key={p.id} className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPowerCategoryIds.includes(p.id)}
|
||||
onChange={() =>
|
||||
setSelectedPowerCategoryIds((prev) =>
|
||||
prev.includes(p.id) ? prev.filter((id) => id !== p.id) : [...prev, p.id]
|
||||
)
|
||||
}
|
||||
disabled={loadingContests || submitting}
|
||||
/>
|
||||
<span>{p.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold mb-2">Ruleset</p>
|
||||
<select
|
||||
value={selectedRuleSetId ?? ""}
|
||||
onChange={(e) => setSelectedRuleSetId(e.target.value ? Number(e.target.value) : null)}
|
||||
disabled={loadingContests || submitting}
|
||||
className="w-full border rounded px-3 py-2 text-sm bg-white dark:bg-gray-900"
|
||||
>
|
||||
<option value="">--</option>
|
||||
{availableRuleSets.map((ruleSet) => (
|
||||
<option key={ruleSet.id} value={ruleSet.id}>
|
||||
{ruleSet.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="text-sm text-red-600">{error}</div>}
|
||||
{success && <div className="text-sm text-green-600">{success}</div>}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button type="submit" color="primary" isLoading={submitting}>
|
||||
{isEdit ? t("Uložit změny") ?? "Uložit změny" : t("Vytvořit kolo") ?? "Vytvořit kolo"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user