Files
vkv/resources/js/components/ContestsSelectBox.tsx
Zdeněk Burda 41e3ce6f25 Initial commit
2026-01-09 21:26:40 +01:00

159 lines
4.5 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import axios from "axios";
import { Select, SelectItem, type Selection } from "@heroui/react";
import { useTranslation } from "react-i18next";
import { useLanguageStore } from "@/stores/languageStore";
import { useContestStore, type ContestSummary } from "@/stores/contestStore";
import { useContestRefreshStore } from "@/stores/contestRefreshStore";
import { useNavigate } from "react-router-dom";
type ContestItem = ContestSummary & {
description?: string | null;
bands?: { id: number; name: string }[];
};
type PaginatedResponse<T> = {
data: T[];
};
type ContestsSelectBoxProps = {
/** Zobraz pouze aktivní závody (is_active === true). Default: false */
onlyActive?: boolean;
/** Label pro Select, volitelné */
label?: string;
/** Placeholder text */
placeholder?: string;
};
export default function ContestsSelectBox({
onlyActive = false,
label,
placeholder,
}: ContestsSelectBoxProps) {
const { t } = useTranslation("common");
const locale = useLanguageStore((s) => s.locale);
const refreshKey = useContestRefreshStore((s) => s.refreshKey);
const navigate = useNavigate();
const [items, setItems] = useState<ContestItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set(["none"]));
const setSelectedContest = useContestStore((s) => s.setSelectedContest);
const selectedContest = useContestStore((s) => s.selectedContest);
// předvyplň výběr podle storu
useEffect(() => {
if (selectedContest) {
setSelectedKeys(new Set([String(selectedContest.id)]));
} else {
setSelectedKeys(new Set(["none"]));
}
}, [selectedContest]);
useEffect(() => {
let active = true;
(async () => {
try {
setLoading(true);
setError(null);
const res = await axios.get<PaginatedResponse<ContestItem> | ContestItem[]>(
"/api/contests",
{
withCredentials: true,
headers: { Accept: "application/json" },
params: { lang: locale },
}
);
if (!active) return;
const data = Array.isArray(res.data)
? res.data
: (res.data as PaginatedResponse<ContestItem>).data;
setItems(data);
} catch {
if (!active) return;
setError(t("unable_to_load_contests") ?? "Nepodařilo se načíst seznam závodů.");
} finally {
if (active) setLoading(false);
}
})();
return () => {
active = false;
};
}, [locale, t, refreshKey]);
const visibleItems = useMemo(
() => (onlyActive ? items.filter((c) => c.is_active) : items),
[items, onlyActive]
);
const handleSelectionChange = (keys: Selection) => {
setSelectedKeys(keys);
if (keys === "all") return;
const id = Array.from(keys)[0];
if (!id || id === "none") {
if (selectedContest) {
setSelectedContest(null);
navigate("/contests");
}
return;
}
if (selectedContest && String(selectedContest.id) === String(id)) {
return;
}
const selected = visibleItems.find((c) => String(c.id) === String(id));
if (selected) {
setSelectedContest(selected);
navigate(`/contests/${selected.id}`);
}
};
if (loading) return <div>{t("contests_loading") ?? "Načítám závody…"}</div>;
if (error) return <div className="text-sm text-red-600">{error}</div>;
if (visibleItems.length === 0) {
return <div>{t("contests_empty") ?? "Žádné závody nejsou k dispozici."}</div>;
}
return (
<Select
aria-label="Contests select"
label={label ?? t("contest_name") ?? "Závod"}
placeholder={placeholder ?? t("select_contest") ?? "Vyber závod"}
selectedKeys={selectedKeys}
onSelectionChange={handleSelectionChange}
color="primary"
isMultiline={true}
>
<SelectItem key="none" textValue="None">
{t("select_none") ?? "Žádný závod"}
</SelectItem>
{visibleItems.map((item) => (
<SelectItem
key={item.id}
textValue={item.name}
description={
<>
<div>{item.description || "—"}</div>
<div className="text-[11px] text-foreground-500">
{(item.bands ?? []).map((b) => b.name).join(", ") || "—"}
</div>
</>
}
>
{item.name}
</SelectItem>
))}
</Select>
);
}