Initial commit

This commit is contained in:
Zdeněk Burda
2026-01-09 21:26:40 +01:00
parent e83aec6dca
commit 41e3ce6f25
404 changed files with 61250 additions and 28 deletions

View File

@@ -0,0 +1,12 @@
import { create } from "zustand";
type ContestRefreshStore = {
refreshKey: number;
triggerRefresh: () => void;
};
export const useContestRefreshStore = create<ContestRefreshStore>((set) => ({
refreshKey: 0,
triggerRefresh: () =>
set((s) => ({ refreshKey: s.refreshKey + 1 })),
}));

View File

@@ -0,0 +1,87 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type ContestSummary = {
id: number;
name: string; // už přeložené jméno podle locale
description?: string | null;
is_active: boolean;
is_test: boolean;
is_mcr: boolean;
is_sixhr: boolean;
start_time: string | null; // ISO string z API
duration: number; // hodiny
logs_deadline_days: number;
rounds?: RoundSummary[];
};
export type RoundSummary = {
id: number;
contest_id: number;
name: string;
description?: string | null;
bands?: { id: number; name: string }[];
categories?: { id: number; name: string }[];
power_categories?: { id: number; name: string }[];
is_active: boolean;
is_test: boolean;
is_sixhr: boolean;
start_time: string | null;
end_time: string | null;
logs_deadline: string | null;
preliminary_evaluation_run_id?: number | null;
official_evaluation_run_id?: number | null;
test_evaluation_run_id?: number | null;
};
type ContestState = {
selectedContest: ContestSummary | null;
selectedRound: RoundSummary | null;
selectedContestRounds: RoundSummary[];
setSelectedContest: (contest: ContestSummary | null) => void;
setSelectedRound: (round: RoundSummary | null) => void;
setSelectedContestRounds: (rounds: RoundSummary[]) => void;
clearSelection: () => void;
};
export const useContestStore = create<ContestState>()(
persist(
(set) => ({
selectedContest: null,
selectedRound: null,
selectedContestRounds: [],
setSelectedContest: (contest) =>
set({
selectedContest: contest,
// při přepnutí závodu implicitně zruš vybrané kolo
selectedRound: null,
selectedContestRounds: contest?.rounds ?? [],
}),
setSelectedRound: (round) => set({ selectedRound: round }),
setSelectedContestRounds: (rounds) => set({ selectedContestRounds: rounds }),
clearSelection: () =>
set({
selectedContest: null,
selectedRound: null,
selectedContestRounds: [],
}),
}),
{
name: 'contest-store',
partialize: (state) => ({
selectedContest: state.selectedContest,
selectedRound: state.selectedRound,
selectedContestRounds: state.selectedContestRounds,
}),
}
)
);
// Použití:
// import { useContestStore } from '@/stores/contestStore';
// const selectedContest = useContestStore((s) => s.selectedContest);
// const selectedRound = useContestStore((s) => s.selectedRound);
// const setSelectedContest = useContestStore((s) => s.setSelectedContest);
// const setSelectedRound = useContestStore((s) => s.setSelectedRound);
// po kliknutí v seznamu závodů:
//setSelectedContest(contestFromApi);

View File

@@ -0,0 +1,69 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type Locale = 'cs' | 'en';
type LanguageState = {
locale: Locale;
setLocale: (locale: Locale) => void;
};
const LOCALE_COOKIE = 'locale';
function detectInitialLocale(): Locale {
if (typeof document !== 'undefined') {
// 1) cookie
const cookieMatch = document.cookie
.split('; ')
.find((row) => row.startsWith(`${LOCALE_COOKIE}=`));
if (cookieMatch) {
const value = cookieMatch.split('=')[1];
if (value === 'cs' || value === 'en') return value;
}
// 2) <html lang="...">
const htmlLang = document.documentElement.lang;
if (htmlLang.startsWith('cs')) return 'cs';
if (htmlLang.startsWith('en')) return 'en';
}
return 'cs';
}
function setLocaleCookie(locale: Locale) {
const expires = new Date();
expires.setFullYear(expires.getFullYear() + 1);
document.cookie = [
`${LOCALE_COOKIE}=${encodeURIComponent(locale)}`,
`expires=${expires.toUTCString()}`,
'path=/',
'SameSite=Lax',
].join('; ');
}
export const useLanguageStore = create<LanguageState>()(
persist(
(set) => ({
locale: detectInitialLocale(),
setLocale: (locale) => {
set({ locale });
// sync s DOM a Laravel
if (typeof document !== 'undefined') {
document.documentElement.lang = locale;
setLocaleCookie(locale);
}
},
}),
{
name: 'language-store',
partialize: (state) => ({ locale: state.locale }),
}
)
);
// Použití:
// import { useLanguageStore } from '@/stores/languageStore';
// const locale = useLanguageStore((s) => s.locale);
// const setLocale = useLanguageStore((s) => s.setLocale);

View File

@@ -0,0 +1,37 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type User = {
id: number;
name: string;
email: string;
is_admin?: boolean;
is_active?: boolean;
// případně další pole: roles, callsign, atd.
};
type UserState = {
user: User | null;
setUser: (user: User | null) => void;
clearUser: () => void;
};
export const useUserStore = create<UserState>()(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
}),
{ name: 'user-store' }
)
);
// použití:
// import { useUserStore } from '@/stores/userStore';
// const user = useUserStore((s) => s.user); // získání přihlášeného uživatele
// const isAuthenticated = !!user;
// const setUser = useUserStore((s) => s.setUser);
// const clearUser = useUserStore((s) => s.clearUser);