feat: nová stránka pro návrhy na vylepšení
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 38s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 21s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 38s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s
This commit is contained in:
27 files changed
+857
-515
No files matched your search
@@ -4,15 +4,14 @@ import { useAuth } from "../context/auth";
|
||||
import SettingsModal from "./modals/SettingsModal";
|
||||
import { useSettings, ThemePreference } from "../context/settings";
|
||||
import HuePicker from "./HuePicker";
|
||||
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, OBJEDNANI_URL } from "../AppRoutes";
|
||||
import { FeatureRequest, getVotes, updateVote, LunchChoices, getChangelogs } from "../../../types";
|
||||
import { STATS_URL, OBJEDNANI_URL, NAVRHY_URL } from "../AppRoutes";
|
||||
import { LunchChoices, getChangelogs } from "../../../types";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
||||
import { formatDateString } from "../Utils";
|
||||
@@ -31,7 +30,6 @@ export default function Header({ choices, dayIndex }: Props) {
|
||||
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);
|
||||
@@ -39,18 +37,9 @@ export default function Header({ choices, dayIndex }: Props) {
|
||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false);
|
||||
const [generateMockModalOpen, setGenerateMockModalOpen] = useState<boolean>(false);
|
||||
const [clearMockModalOpen, setClearMockModalOpen] = useState<boolean>(false);
|
||||
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
|
||||
|
||||
const effectiveDark = settings?.effectiveDark ?? false;
|
||||
|
||||
useEffect(() => {
|
||||
if (auth?.login) {
|
||||
getVotes().then(response => {
|
||||
setFeatureVotes(response.data);
|
||||
})
|
||||
}
|
||||
}, [auth?.login]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!auth?.login) return;
|
||||
const lastSeen = localStorage.getItem(LAST_SEEN_CHANGELOG_KEY) ?? undefined;
|
||||
@@ -68,10 +57,6 @@ export default function Header({ choices, dayIndex }: Props) {
|
||||
setSettingsModalOpen(false);
|
||||
}
|
||||
|
||||
const closeVotingModal = () => {
|
||||
setVotingModalOpen(false);
|
||||
}
|
||||
|
||||
const closePizzaModal = () => {
|
||||
setPizzaModalOpen(false);
|
||||
}
|
||||
@@ -158,17 +143,6 @@ export default function Header({ choices, dayIndex }: Props) {
|
||||
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" />
|
||||
@@ -190,7 +164,7 @@ export default function Header({ choices, dayIndex }: Props) {
|
||||
<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={() => navigate(NAVRHY_URL)}>Návrhy na vylepšení</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>
|
||||
@@ -220,7 +194,6 @@ export default function Header({ choices, dayIndex }: Props) {
|
||||
</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
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useState } from "react";
|
||||
import { Modal, Button, Form } from "react-bootstrap";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (title: string, description: string) => Promise<void>;
|
||||
};
|
||||
|
||||
/** Modální dialog pro přidání nového návrhu na vylepšení. */
|
||||
export default function AddSuggestionModal({ isOpen, onClose, onSubmit }: Readonly<Props>) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const reset = () => {
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
reset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!title.trim() || !description.trim()) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onSubmit(title.trim(), description.trim());
|
||||
reset();
|
||||
onClose();
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onHide={handleClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title><h2>Nový návrh na vylepšení</h2></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Název</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Stručný název návrhu"
|
||||
value={title}
|
||||
maxLength={120}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
autoFocus
|
||||
/>
|
||||
<Form.Text className="text-muted">Krátký, výstižný název navrhované úpravy.</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>Popis</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={5}
|
||||
placeholder="Detailní popis navrhované úpravy, řešení apod."
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Storno
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting || !title.trim() || !description.trim()}>
|
||||
Přidat
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Modal, Button, Form } from "react-bootstrap"
|
||||
import { FeatureRequest } from "../../../../types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean,
|
||||
onClose: () => void,
|
||||
onChange: (option: FeatureRequest, active: boolean) => void,
|
||||
initialValues?: FeatureRequest[],
|
||||
}
|
||||
|
||||
/** Modální dialog pro hlasování o nových funkcích. */
|
||||
export default function FeaturesVotingModal({ isOpen, onClose, onChange, initialValues }: Readonly<Props>) {
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.currentTarget.value as FeatureRequest, e.currentTarget.checked);
|
||||
}
|
||||
|
||||
return <Modal show={isOpen} onHide={onClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
Hlasujte pro nové funkce
|
||||
<p style={{ fontSize: '12px' }}>Je možno vybrat maximálně 4 možnosti</p>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{(Object.keys(FeatureRequest) as Array<keyof typeof FeatureRequest>).map(key => {
|
||||
return <Form.Check
|
||||
key={key}
|
||||
type='checkbox'
|
||||
id={key}
|
||||
label={FeatureRequest[key]}
|
||||
onChange={handleChange}
|
||||
value={key}
|
||||
defaultChecked={initialValues?.includes(key as FeatureRequest)}
|
||||
/>
|
||||
})}
|
||||
<p className="mt-3" style={{ fontSize: '12px' }}>Něco jiného? Dejte vědět.</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
Zavřít
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Modal, Button } from "react-bootstrap";
|
||||
import { Suggestion } from "../../../../types";
|
||||
|
||||
type Props = {
|
||||
suggestion?: Suggestion;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/** Modální dialog zobrazující celý detail návrhu na vylepšení. */
|
||||
export default function SuggestionDetailModal({ suggestion, onClose }: Readonly<Props>) {
|
||||
return (
|
||||
<Modal show={!!suggestion} onHide={onClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title><h2>{suggestion?.title}</h2></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p className="text-muted mb-3">
|
||||
Navrhovatel: <strong>{suggestion?.author}</strong> · Hlasy: <strong>{suggestion?.voteScore}</strong>
|
||||
</p>
|
||||
<p style={{ whiteSpace: "pre-wrap" }}>{suggestion?.description}</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Zavřít
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user