diff --git a/client/src/AppRoutes.tsx b/client/src/AppRoutes.tsx index 1c37683..30e707e 100644 --- a/client/src/AppRoutes.tsx +++ b/client/src/AppRoutes.tsx @@ -6,15 +6,22 @@ import { ToastContainer } from "react-toastify"; import { SocketContext, socket } from "./context/socket"; import StatsPage from "./pages/StatsPage"; import OrderGroupsPage from "./pages/OrderGroupsPage"; +import SuggestionsPage from "./pages/SuggestionsPage"; import App from "./App"; export const STATS_URL = '/stats'; export const OBJEDNANI_URL = '/objednani'; +export const NAVRHY_URL = '/navrhy'; export default function AppRoutes() { return ( } /> + + + + } /> diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 07dc519..49acaa2 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -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(false); - const [votingModalOpen, setVotingModalOpen] = useState(false); const [pizzaModalOpen, setPizzaModalOpen] = useState(false); const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState(false); const [changelogModalOpen, setChangelogModalOpen] = useState(false); @@ -39,18 +37,9 @@ export default function Header({ choices, dayIndex }: Props) { const [qrModalOpen, setQrModalOpen] = useState(false); const [generateMockModalOpen, setGenerateMockModalOpen] = useState(false); const [clearMockModalOpen, setClearMockModalOpen] = useState(false); - const [featureVotes, setFeatureVotes] = useState([]); 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 Luncher @@ -190,7 +164,7 @@ export default function Header({ choices, dayIndex }: Props) { setSettingsModalOpen(true)}>Nastavení setRefreshMenuModalOpen(true)}>Přenačtení menu - setVotingModalOpen(true)}>Hlasovat o nových funkcích + navigate(NAVRHY_URL)}>Návrhy na vylepšení setPizzaModalOpen(true)}>Pizza kalkulačka Generování QR kódů navigate(STATS_URL)}>Statistiky @@ -220,7 +194,6 @@ export default function Header({ choices, dayIndex }: Props) { - {choices && settings?.bankAccount && settings?.holderName && ( void; + onSubmit: (title: string, description: string) => Promise; +}; + +/** Modální dialog pro přidání nového návrhu na vylepšení. */ +export default function AddSuggestionModal({ isOpen, onClose, onSubmit }: Readonly) { + 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 ( + + +

Nový návrh na vylepšení

+
+ + + Název + setTitle(e.target.value)} + onKeyDown={e => e.stopPropagation()} + autoFocus + /> + Krátký, výstižný název navrhované úpravy. + + + Popis + setDescription(e.target.value)} + onKeyDown={e => e.stopPropagation()} + /> + + + + + + +
+ ); +} diff --git a/client/src/components/modals/FeaturesVotingModal.tsx b/client/src/components/modals/FeaturesVotingModal.tsx deleted file mode 100644 index a265500..0000000 --- a/client/src/components/modals/FeaturesVotingModal.tsx +++ /dev/null @@ -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) { - - const handleChange = (e: React.ChangeEvent) => { - onChange(e.currentTarget.value as FeatureRequest, e.currentTarget.checked); - } - - return - - - Hlasujte pro nové funkce -

Je možno vybrat maximálně 4 možnosti

-
-
- - {(Object.keys(FeatureRequest) as Array).map(key => { - return - })} -

Něco jiného? Dejte vědět.

-
- - - -
-} \ No newline at end of file diff --git a/client/src/components/modals/SuggestionDetailModal.tsx b/client/src/components/modals/SuggestionDetailModal.tsx new file mode 100644 index 0000000..564d98a --- /dev/null +++ b/client/src/components/modals/SuggestionDetailModal.tsx @@ -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) { + return ( + + +

{suggestion?.title}

+
+ +

+ Navrhovatel: {suggestion?.author} · Hlasy: {suggestion?.voteScore} +

+

{suggestion?.description}

+
+ + + +
+ ); +} diff --git a/client/src/pages/StatsPage.scss b/client/src/pages/StatsPage.scss index d6f4a62..7b6c82b 100644 --- a/client/src/pages/StatsPage.scss +++ b/client/src/pages/StatsPage.scss @@ -89,67 +89,4 @@ .recharts-cartesian-grid-vertical line { stroke: var(--luncher-border); } - - .voting-stats-section { - margin-top: 48px; - width: 100%; - max-width: 800px; - - h2 { - font-size: 1.5rem; - font-weight: 700; - color: var(--luncher-text); - margin-bottom: 16px; - text-align: center; - } - } - - .voting-stats-table { - width: 100%; - background: var(--luncher-bg-card); - border-radius: var(--luncher-radius-lg); - box-shadow: var(--luncher-shadow); - border: 1px solid var(--luncher-border-light); - overflow: hidden; - border-collapse: collapse; - - th { - background: var(--luncher-primary); - color: #ffffff; - padding: 12px 20px; - text-align: left; - font-weight: 600; - font-size: 0.9rem; - - &:last-child { - text-align: center; - width: 120px; - } - } - - td { - padding: 12px 20px; - border-bottom: 1px solid var(--luncher-border-light); - color: var(--luncher-text); - font-size: 0.9rem; - - &:last-child { - text-align: center; - font-weight: 600; - color: var(--luncher-primary); - } - } - - tbody tr { - transition: var(--luncher-transition); - - &:hover { - background: var(--luncher-bg-hover); - } - - &:last-child td { - border-bottom: none; - } - } - } } diff --git a/client/src/pages/StatsPage.tsx b/client/src/pages/StatsPage.tsx index 0f88eac..9e938d7 100644 --- a/client/src/pages/StatsPage.tsx +++ b/client/src/pages/StatsPage.tsx @@ -4,7 +4,7 @@ import Header from "../components/Header"; import { useAuth } from "../context/auth"; import Login from "../Login"; import { formatDate, getFirstWorkDayOfWeek, getHumanDate, getLastWorkDayOfWeek } from "../Utils"; -import { WeeklyStats, LunchChoice, VotingStats, FeatureRequest, getStats, getVotingStats } from "../../../types"; +import { WeeklyStats, LunchChoice, getStats } from "../../../types"; import Loader from "../components/Loader"; import { faChevronLeft, faChevronRight, faGear } from "@fortawesome/free-solid-svg-icons"; import { Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts"; @@ -32,7 +32,6 @@ export default function StatsPage() { const auth = useAuth(); const [dateRange, setDateRange] = useState(); const [data, setData] = useState(); - const [votingStats, setVotingStats] = useState(); // Prvotní nastavení aktuálního týdne useEffect(() => { @@ -49,19 +48,6 @@ export default function StatsPage() { } }, [dateRange]); - // Načtení statistik hlasování - useEffect(() => { - getVotingStats().then(response => { - setVotingStats(response.data); - }); - }, []); - - const sortedVotingStats = useMemo(() => { - if (!votingStats) return []; - return Object.entries(votingStats) - .sort((a, b) => (b[1] as number) - (a[1] as number)); - }, [votingStats]); - const renderLine = (location: LunchChoice) => { const index = Object.values(LunchChoice).indexOf(location); return data.locations[location] ?? 0} stroke={COLORS[index]} strokeWidth={STROKE_WIDTH} /> @@ -142,27 +128,6 @@ export default function StatsPage() { - {sortedVotingStats.length > 0 && ( -
-

Hlasování o funkcích

- - - - - - - - - {sortedVotingStats.map(([feature, count]) => ( - - - - - ))} - -
FunkcePočet hlasů
{FeatureRequest[feature as keyof typeof FeatureRequest] ?? feature}{count as number}
-
- )}