Initial commit
This commit is contained in:
190
resources/js/components/RoundDetail.tsx
Normal file
190
resources/js/components/RoundDetail.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Card, CardHeader, CardBody, Divider } from "@heroui/react";
|
||||
import { useLanguageStore } from "@/stores/languageStore";
|
||||
import { useContestStore, type ContestSummary, type RoundSummary } from "@/stores/contestStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type RoundDetailProps = {
|
||||
roundId: number;
|
||||
};
|
||||
|
||||
type RoundDetailData = RoundSummary & {
|
||||
contest_id: number;
|
||||
contest?: ContestSummary | null;
|
||||
description?: string | Record<string, string> | null;
|
||||
logs_deadline?: string | null;
|
||||
start_time: string | null;
|
||||
end_time: string | null;
|
||||
rule_set_id?: number | null;
|
||||
rule_set?: { id: number; name: string } | null;
|
||||
preliminary_evaluation_run_id?: number | null;
|
||||
official_evaluation_run_id?: number | null;
|
||||
test_evaluation_run_id?: number | null;
|
||||
};
|
||||
|
||||
function parseRoundDate(value: string | null): Date | null {
|
||||
if (!value) return null;
|
||||
const direct = new Date(value);
|
||||
if (!Number.isNaN(direct.getTime())) return direct;
|
||||
const normalized = value.includes("T") ? value : value.replace(" ", "T");
|
||||
const fallback = new Date(normalized);
|
||||
if (!Number.isNaN(fallback.getTime())) return fallback;
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null, locale: string): string {
|
||||
if (!value) return "—";
|
||||
const date = parseRoundDate(value);
|
||||
if (!date) return value;
|
||||
return date.toLocaleString(locale);
|
||||
}
|
||||
|
||||
function isPastDeadline(value: string | null): boolean {
|
||||
const date = parseRoundDate(value);
|
||||
if (!date) return false;
|
||||
return (new Date() > date);
|
||||
}
|
||||
|
||||
const resolveTranslation = (field: any, locale: string): string => {
|
||||
if (!field) return "";
|
||||
if (typeof field === "string") return field;
|
||||
if (typeof field === "object") {
|
||||
if (field[locale]) return field[locale];
|
||||
if (field["en"]) return field["en"];
|
||||
const first = Object.values(field)[0];
|
||||
return typeof first === "string" ? first : "";
|
||||
}
|
||||
return String(field);
|
||||
};
|
||||
|
||||
export default function RoundDetail({ roundId }: RoundDetailProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const locale = useLanguageStore((s) => s.locale);
|
||||
const setSelectedContest = useContestStore((s) => s.setSelectedContest);
|
||||
const setSelectedRound = useContestStore((s) => s.setSelectedRound);
|
||||
|
||||
const [detail, setDetail] = useState<RoundDetailData | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!roundId) return;
|
||||
|
||||
let active = true;
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await axios.get<RoundDetailData>(`/api/rounds/${roundId}`, {
|
||||
headers: { Accept: "application/json" },
|
||||
params: { lang: locale },
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
if (!active) return;
|
||||
setDetail(res.data);
|
||||
|
||||
// set store for breadcrumbs/overview
|
||||
if (res.data.contest) {
|
||||
setSelectedContest({
|
||||
id: res.data.contest.id,
|
||||
name: resolveTranslation((res.data as any).contest.name, locale),
|
||||
description: resolveTranslation((res.data as any).contest.description ?? null, locale),
|
||||
is_active: res.data.contest.is_active,
|
||||
is_mcr: res.data.contest.is_mcr,
|
||||
is_sixhr: res.data.contest.is_sixhr,
|
||||
start_time: res.data.contest.start_time ?? null,
|
||||
duration: res.data.contest.duration ?? 0,
|
||||
});
|
||||
}
|
||||
setSelectedRound({
|
||||
id: res.data.id,
|
||||
contest_id: res.data.contest_id,
|
||||
name: resolveTranslation(res.data.name, locale),
|
||||
description: resolveTranslation(res.data.description ?? null, locale),
|
||||
is_active: res.data.is_active,
|
||||
is_test: res.data.is_test,
|
||||
is_sixhr: res.data.is_sixhr,
|
||||
start_time: res.data.start_time,
|
||||
end_time: res.data.end_time,
|
||||
logs_deadline: res.data.logs_deadline ?? null,
|
||||
preliminary_evaluation_run_id: res.data.preliminary_evaluation_run_id ?? null,
|
||||
official_evaluation_run_id: res.data.official_evaluation_run_id ?? null,
|
||||
test_evaluation_run_id: res.data.test_evaluation_run_id ?? null,
|
||||
});
|
||||
} catch {
|
||||
if (!active) return;
|
||||
setError("Nepodařilo se načíst detail kola.");
|
||||
} finally {
|
||||
if (active) setLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [roundId, locale, setSelectedContest, setSelectedRound]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-lg font-semibold">
|
||||
{detail ? resolveTranslation(detail.name, locale) : t("round_name") ?? "Kolo"}
|
||||
</span>
|
||||
{detail?.description && (
|
||||
<span className="text-sm text-foreground-500">
|
||||
{resolveTranslation(detail.description, locale)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Divider />
|
||||
<CardBody>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
{loading && <p className="text-sm text-foreground-500">Načítám detail…</p>}
|
||||
{detail && !loading && (
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex gap-2">
|
||||
<span className="font-semibold">{t("round_schedule") ?? "Termín"}:</span>
|
||||
<span>{formatDateTime(detail.start_time, locale)} — {formatDateTime(detail.end_time, locale)}</span>
|
||||
</div>
|
||||
{detail.logs_deadline && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-semibold">{t("round_logs_deadline") ?? "Logy do"}:</span>
|
||||
<span>{formatDateTime(detail.logs_deadline, locale)}</span>
|
||||
{detail.logs_deadline && isPastDeadline(detail.logs_deadline) && (
|
||||
<span>
|
||||
{t("round_logs_deadline_passed") ??
|
||||
"Termín pro nahrání logů již vypršel."}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<span className="font-semibold">{t("round_active") ?? "Aktivní"}:</span>
|
||||
<span>{detail.is_active ? (t("yes") ?? "Ano") : (t("no") ?? "Ne")}</span>
|
||||
</div>
|
||||
{detail.is_test && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-semibold">{t("round_test") ?? "Test"}:</span>
|
||||
<span>{t("yes") ?? "Ano"}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<span className="font-semibold">6h:</span>
|
||||
<span>{detail.is_sixhr ? (t("yes") ?? "Ano") : (t("no") ?? "Ne")}</span>
|
||||
</div>
|
||||
{(detail.rule_set || detail.rule_set_id) && (
|
||||
<div className="flex gap-2">
|
||||
<span className="font-semibold">Ruleset:</span>
|
||||
<span>{detail.rule_set?.name ?? `#${detail.rule_set_id}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user