Initial commit
This commit is contained in:
158
resources/js/components/ContestsSelectBox.tsx
Normal file
158
resources/js/components/ContestsSelectBox.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user