All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
259 lines
11 KiB
TypeScript
259 lines
11 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Navbar, Nav, NavDropdown, Modal, Button } from "react-bootstrap";
|
|
import { useAuth } from "../context/auth";
|
|
import SettingsModal from "./modals/SettingsModal";
|
|
import { useSettings, ThemePreference } from "../context/settings";
|
|
import FeaturesVotingModal from "./modals/FeaturesVotingModal";
|
|
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
|
|
import RefreshMenuModal from "./modals/RefreshMenuModal";
|
|
import GenerateQrModal from "./modals/GenerateQrModal";
|
|
import GenerateMockDataModal from "./modals/GenerateMockDataModal";
|
|
import ClearMockDataModal from "./modals/ClearMockDataModal";
|
|
import { useNavigate } from "react-router";
|
|
import { STATS_URL } from "../AppRoutes";
|
|
import { FeatureRequest, getVotes, updateVote, LunchChoices } from "../../../types";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
|
|
|
const CHANGELOG = [
|
|
"Nový moderní design aplikace",
|
|
"Oprava parsování Sladovnické a TechTower",
|
|
"Možnost označit se jako objednávající u volby \"budu objednávat\"",
|
|
"Možnost generovat QR kódy pro platby (i mimo Pizza day)",
|
|
];
|
|
|
|
const IS_DEV = process.env.NODE_ENV === 'development';
|
|
|
|
type Props = {
|
|
choices?: LunchChoices;
|
|
dayIndex?: number;
|
|
};
|
|
|
|
export default function Header({ choices, dayIndex }: Props) {
|
|
const auth = useAuth();
|
|
const settings = useSettings();
|
|
const navigate = useNavigate();
|
|
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false);
|
|
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
|
|
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
|
const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState<boolean>(false);
|
|
const [changelogModalOpen, setChangelogModalOpen] = useState<boolean>(false);
|
|
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false);
|
|
const [generateMockModalOpen, setGenerateMockModalOpen] = useState<boolean>(false);
|
|
const [clearMockModalOpen, setClearMockModalOpen] = useState<boolean>(false);
|
|
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
|
|
|
|
// Zjistíme aktuální efektivní téma (pro zobrazení správné ikony)
|
|
const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark'>('light');
|
|
|
|
useEffect(() => {
|
|
const updateEffectiveTheme = () => {
|
|
if (settings?.themePreference === 'system') {
|
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
setEffectiveTheme(isDark ? 'dark' : 'light');
|
|
} else {
|
|
setEffectiveTheme(settings?.themePreference || 'light');
|
|
}
|
|
};
|
|
|
|
updateEffectiveTheme();
|
|
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
mediaQuery.addEventListener('change', updateEffectiveTheme);
|
|
return () => mediaQuery.removeEventListener('change', updateEffectiveTheme);
|
|
}, [settings?.themePreference]);
|
|
|
|
useEffect(() => {
|
|
if (auth?.login) {
|
|
getVotes().then(response => {
|
|
setFeatureVotes(response.data);
|
|
})
|
|
}
|
|
}, [auth?.login]);
|
|
|
|
const closeSettingsModal = () => {
|
|
setSettingsModalOpen(false);
|
|
}
|
|
|
|
const closeVotingModal = () => {
|
|
setVotingModalOpen(false);
|
|
}
|
|
|
|
const closePizzaModal = () => {
|
|
setPizzaModalOpen(false);
|
|
}
|
|
|
|
const closeRefreshMenuModal = () => {
|
|
setRefreshMenuModalOpen(false);
|
|
}
|
|
|
|
const closeQrModal = () => {
|
|
setQrModalOpen(false);
|
|
}
|
|
|
|
const handleQrMenuClick = () => {
|
|
if (!settings?.bankAccount || !settings?.holderName) {
|
|
alert('Pro generování QR kódů je nutné mít v nastavení vyplněné číslo účtu a jméno držitele účtu.');
|
|
return;
|
|
}
|
|
setQrModalOpen(true);
|
|
}
|
|
|
|
const toggleTheme = () => {
|
|
// Přepínáme mezi light a dark (ignorujeme system pro jednoduchost)
|
|
const newTheme: ThemePreference = effectiveTheme === 'dark' ? 'light' : 'dark';
|
|
settings?.setThemePreference(newTheme);
|
|
}
|
|
|
|
const isValidInteger = (str: string) => {
|
|
str = str.trim();
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
str = str.replace(/^0+/, "") || "0";
|
|
const n = Math.floor(Number(str));
|
|
return n !== Infinity && String(n) === str && n >= 0;
|
|
}
|
|
|
|
const saveSettings = (bankAccountNumber?: string, bankAccountHolderName?: string, hideSoupsOption?: boolean, themePreference?: ThemePreference) => {
|
|
if (bankAccountNumber) {
|
|
try {
|
|
// Validace kódu banky
|
|
if (!bankAccountNumber.includes('/')) {
|
|
throw new Error("Číslo účtu neobsahuje lomítko/kód banky")
|
|
}
|
|
const split = bankAccountNumber.split("/");
|
|
if (split[1].length !== 4) {
|
|
throw new Error("Kód banky musí být 4 číslice")
|
|
}
|
|
if (!isValidInteger(split[1])) {
|
|
throw new Error("Kód banky není číslo")
|
|
}
|
|
|
|
// Validace čísla a předčíslí
|
|
let cislo = split[0];
|
|
|
|
if (cislo.indexOf('-') > 0) {
|
|
cislo = cislo.replace('-', '');
|
|
}
|
|
if (!isValidInteger(cislo)) {
|
|
throw new Error("Předčíslí nebo číslo účtu neobsahuje pouze číslice")
|
|
}
|
|
if (cislo.length < 16) {
|
|
cislo = cislo.padStart(16, '0');
|
|
}
|
|
let sum = 0;
|
|
for (let i = 0; i < cislo.length; i++) {
|
|
const char = cislo.charAt(i);
|
|
const order = (cislo.length - 1) - i;
|
|
const weight = (2 ** order) % 11;
|
|
sum += Number.parseInt(char) * weight
|
|
}
|
|
if (sum % 11 !== 0) {
|
|
throw new Error("Číslo účtu je neplatné")
|
|
}
|
|
} catch (e: any) {
|
|
alert(e.message)
|
|
return
|
|
}
|
|
}
|
|
settings?.setBankAccountNumber(bankAccountNumber);
|
|
settings?.setBankAccountHolderName(bankAccountHolderName);
|
|
settings?.setHideSoupsOption(hideSoupsOption);
|
|
if (themePreference) {
|
|
settings?.setThemePreference(themePreference);
|
|
}
|
|
closeSettingsModal();
|
|
}
|
|
|
|
const saveFeatureVote = async (option: FeatureRequest, active: boolean) => {
|
|
await updateVote({ body: { option, active } });
|
|
const votes = [...featureVotes || []];
|
|
if (active) {
|
|
votes.push(option);
|
|
} else {
|
|
votes.splice(votes.indexOf(option), 1);
|
|
}
|
|
setFeatureVotes(votes);
|
|
}
|
|
|
|
return <Navbar variant='dark' expand="lg">
|
|
<Navbar.Brand href="/">Luncher</Navbar.Brand>
|
|
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
|
<Navbar.Collapse id="basic-navbar-nav">
|
|
<Nav className="nav">
|
|
<button
|
|
className="theme-toggle"
|
|
onClick={toggleTheme}
|
|
title={effectiveTheme === 'dark' ? 'Přepnout na světlý režim' : 'Přepnout na tmavý režim'}
|
|
aria-label="Přepnout barevný motiv"
|
|
>
|
|
<FontAwesomeIcon icon={effectiveTheme === 'dark' ? faSun : faMoon} />
|
|
</button>
|
|
<NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown">
|
|
<NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</NavDropdown.Item>
|
|
<NavDropdown.Item onClick={() => setRefreshMenuModalOpen(true)}>Přenačtení menu</NavDropdown.Item>
|
|
<NavDropdown.Item onClick={() => setVotingModalOpen(true)}>Hlasovat o nových funkcích</NavDropdown.Item>
|
|
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
|
<NavDropdown.Item onClick={handleQrMenuClick}>Generování QR kódů</NavDropdown.Item>
|
|
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
|
<NavDropdown.Item onClick={() => setChangelogModalOpen(true)}>Novinky</NavDropdown.Item>
|
|
{IS_DEV && (
|
|
<>
|
|
<NavDropdown.Divider />
|
|
<NavDropdown.Item onClick={() => setGenerateMockModalOpen(true)}>🔧 Generovat mock data</NavDropdown.Item>
|
|
<NavDropdown.Item onClick={() => setClearMockModalOpen(true)}>🔧 Smazat data dne</NavDropdown.Item>
|
|
</>
|
|
)}
|
|
<NavDropdown.Divider />
|
|
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
|
|
</NavDropdown>
|
|
</Nav>
|
|
</Navbar.Collapse>
|
|
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} />
|
|
<RefreshMenuModal isOpen={refreshMenuModalOpen} onClose={closeRefreshMenuModal} />
|
|
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
|
|
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
|
|
{choices && settings?.bankAccount && settings?.holderName && (
|
|
<GenerateQrModal
|
|
isOpen={qrModalOpen}
|
|
onClose={closeQrModal}
|
|
choices={choices}
|
|
bankAccount={settings.bankAccount}
|
|
bankAccountHolder={settings.holderName}
|
|
/>
|
|
)}
|
|
{IS_DEV && (
|
|
<>
|
|
<GenerateMockDataModal
|
|
isOpen={generateMockModalOpen}
|
|
onClose={() => setGenerateMockModalOpen(false)}
|
|
currentDayIndex={dayIndex}
|
|
/>
|
|
<ClearMockDataModal
|
|
isOpen={clearMockModalOpen}
|
|
onClose={() => setClearMockModalOpen(false)}
|
|
currentDayIndex={dayIndex}
|
|
/>
|
|
</>
|
|
)}
|
|
<Modal show={changelogModalOpen} onHide={() => setChangelogModalOpen(false)}>
|
|
<Modal.Header closeButton>
|
|
<Modal.Title><h2>Novinky</h2></Modal.Title>
|
|
</Modal.Header>
|
|
<Modal.Body>
|
|
<ul>
|
|
{CHANGELOG.map((item, index) => (
|
|
<li key={index}>{item}</li>
|
|
))}
|
|
</ul>
|
|
</Modal.Body>
|
|
<Modal.Footer>
|
|
<Button variant="secondary" onClick={() => setChangelogModalOpen(false)}>
|
|
Zavřít
|
|
</Button>
|
|
</Modal.Footer>
|
|
</Modal>
|
|
</Navbar>
|
|
}
|