This commit is contained in:
		
							parent
							
								
									0af78e72d9
								
							
						
					
					
						commit
						ca400638d1
					
				| @ -21,9 +21,12 @@ | ||||
|     "react-dom": "^19.0.0", | ||||
|     "react-jwt": "^1.2.0", | ||||
|     "react-modal": "^3.16.1", | ||||
|     "react-router": "^7.2.0", | ||||
|     "react-router-dom": "^7.2.0", | ||||
|     "react-select-search": "^4.1.6", | ||||
|     "react-snowfall": "^2.2.0", | ||||
|     "react-toastify": "^10.0.4", | ||||
|     "recharts": "^2.15.1", | ||||
|     "sass": "^1.80.6", | ||||
|     "socket.io-client": "^4.6.1", | ||||
|     "typescript": "^5.3.3", | ||||
|  | ||||
| @ -23,6 +23,8 @@ import { getHumanDateTime, isInTheFuture } from './Utils'; | ||||
| import NoteModal from './components/modals/NoteModal'; | ||||
| import { useEasterEgg } from './context/eggs'; | ||||
| import { getImage } from './api/EasterEggApi'; | ||||
| import { Link } from 'react-router'; | ||||
| import { STATS_URL } from './AppRoutes'; | ||||
| 
 | ||||
| const EVENT_CONNECT = "connect" | ||||
| 
 | ||||
| @ -409,8 +411,8 @@ function App() { | ||||
|             <img src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> | ||||
|             Poslední změny: | ||||
|             <ul> | ||||
|               <li>Přidání restaurací Zastávka u Michala a Pivovarský šenk Šeříková</li> | ||||
|               <li>Možnost výběru restaurace a jídel kliknutím v tabulce</li> | ||||
|               <li><Link to={STATS_URL}>Statistiky</Link></li> | ||||
|             </ul> | ||||
|           </Alert> | ||||
|           {dayIndex != null && | ||||
|  | ||||
							
								
								
									
										33
									
								
								client/src/AppRoutes.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								client/src/AppRoutes.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| import { Routes, Route } from "react-router-dom"; | ||||
| import { ProvideSettings } from "./context/settings"; | ||||
| import Snowfall from "react-snowfall"; | ||||
| import { ToastContainer } from "react-toastify"; | ||||
| import { SocketContext, socket } from "./context/socket"; | ||||
| import StatsPage from "./pages/StatsPage"; | ||||
| import App from "./App"; | ||||
| 
 | ||||
| export const STATS_URL = '/stats'; | ||||
| 
 | ||||
| export default function AppRoutes() { | ||||
|     return ( | ||||
|         <Routes> | ||||
|             <Route path={STATS_URL} element={<StatsPage />} /> | ||||
|             <Route path="/" element={ | ||||
|                 <ProvideSettings> | ||||
|                     <SocketContext.Provider value={socket}> | ||||
|                         <> | ||||
|                             <Snowfall style={{ | ||||
|                                 zIndex: 2, | ||||
|                                 position: 'fixed', | ||||
|                                 width: '100vw', | ||||
|                                 height: '100vh' | ||||
|                             }} /> | ||||
|                             <App /> | ||||
|                         </> | ||||
|                         <ToastContainer /> | ||||
|                     </SocketContext.Provider> | ||||
|                 </ProvideSettings> | ||||
|             } /> | ||||
|         </Routes> | ||||
|     ); | ||||
| } | ||||
| @ -62,3 +62,45 @@ export function isInTheFuture(time: DepartureTime) { | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Vrátí index dne v týdnu, kde pondělí=0, neděle=6 | ||||
|  *  | ||||
|  * @param date datum | ||||
|  * @returns index dne v týdnu | ||||
|  */ | ||||
| export const getDayOfWeekIndex = (date: Date) => { | ||||
|     // https://stackoverflow.com/a/4467559
 | ||||
|     return (((date.getDay() - 1) % 7) + 7) % 7; | ||||
| } | ||||
| 
 | ||||
| /** Vrátí první pracovní den v týdnu předaného data. */ | ||||
| export function getFirstWorkDayOfWeek(date: Date) { | ||||
|     const firstDay = new Date(date.getTime()); | ||||
|     firstDay.setDate(date.getDate() - getDayOfWeekIndex(date)); | ||||
|     return firstDay; | ||||
| } | ||||
| 
 | ||||
| /** Vrátí poslední pracovní den v týdnu předaného data. */ | ||||
| export function getLastWorkDayOfWeek(date: Date) { | ||||
|     const lastDay = new Date(date.getTime()); | ||||
|     lastDay.setDate(date.getDate() + (4 - getDayOfWeekIndex(date))); | ||||
|     return lastDay; | ||||
| } | ||||
| 
 | ||||
| /** Vrátí datum v ISO formátu. */ | ||||
| export function formatDate(date: Date, format?: string) { | ||||
|     let day = String(date.getDate()).padStart(2, '0'); | ||||
|     let month = String(date.getMonth() + 1).padStart(2, "0"); | ||||
|     let year = String(date.getFullYear()); | ||||
| 
 | ||||
|     const f = (format === undefined) ? 'YYYY-MM-DD' : format; | ||||
|     return f.replace('DD', day).replace('MM', month).replace('YYYY', year); | ||||
| } | ||||
| 
 | ||||
| /** Vrátí human-readable reprezentaci předaného data pro zobrazení. */ | ||||
| export function getHumanDate(date: Date) { | ||||
|     let currentDay = String(date.getDate()).padStart(2, '0'); | ||||
|     let currentMonth = String(date.getMonth() + 1).padStart(2, "0"); | ||||
|     let currentYear = date.getFullYear(); | ||||
|     return `${currentDay}.${currentMonth}.${currentYear}`; | ||||
| } | ||||
							
								
								
									
										8
									
								
								client/src/api/StatsApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								client/src/api/StatsApi.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| import { WeeklyStats } from "../../../types"; | ||||
| import { api } from "./Api"; | ||||
| 
 | ||||
| const STATS_API_PREFIX = '/api/stats'; | ||||
| 
 | ||||
| export const getStats = async (startDate: string, endDate: string) => { | ||||
|     return await api.get<WeeklyStats>(`${STATS_API_PREFIX}?startDate=${startDate}&endDate=${endDate}`); | ||||
| } | ||||
| @ -8,11 +8,14 @@ import { FeatureRequest } from "../../../types"; | ||||
| import { errorHandler } from "../api/Api"; | ||||
| import { getFeatureVotes, updateFeatureVote } from "../api/VotingApi"; | ||||
| import PizzaCalculatorModal from "./modals/PizzaCalculatorModal"; | ||||
| import { useNavigate } from "react-router"; | ||||
| import { STATS_URL } from "../AppRoutes"; | ||||
| 
 | ||||
| 
 | ||||
| export default function Header() { | ||||
|     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); | ||||
| @ -108,7 +111,7 @@ export default function Header() { | ||||
|     } | ||||
| 
 | ||||
|     return <Navbar variant='dark' expand="lg"> | ||||
|         <Navbar.Brand>Luncher</Navbar.Brand> | ||||
|         <Navbar.Brand href="/">Luncher</Navbar.Brand> | ||||
|         <Navbar.Toggle aria-controls="basic-navbar-nav" /> | ||||
|         <Navbar.Collapse id="basic-navbar-nav"> | ||||
|             <Nav className="nav"> | ||||
| @ -116,6 +119,7 @@ export default function Header() { | ||||
|                     <NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</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={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item> | ||||
|                     <NavDropdown.Divider /> | ||||
|                     <NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item> | ||||
|                 </NavDropdown> | ||||
|  | ||||
| @ -1,33 +1,20 @@ | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom/client'; | ||||
| import App from './App'; | ||||
| import { SocketContext, socket } from './context/socket'; | ||||
| import { ProvideAuth } from './context/auth'; | ||||
| import { ToastContainer } from 'react-toastify'; | ||||
| import { ProvideSettings } from './context/settings'; | ||||
| import 'react-toastify/dist/ReactToastify.css'; | ||||
| import './index.css'; | ||||
| import Snowfall from 'react-snowfall'; | ||||
| import AppRoutes from './AppRoutes'; | ||||
| import { BrowserRouter } from 'react-router'; | ||||
| 
 | ||||
| const root = ReactDOM.createRoot( | ||||
|   document.getElementById('root') as HTMLElement | ||||
| ); | ||||
| root.render( | ||||
|   <React.StrictMode> | ||||
|     <BrowserRouter> | ||||
|       <ProvideAuth> | ||||
|       <ProvideSettings> | ||||
|         <SocketContext.Provider value={socket}> | ||||
|           <> | ||||
|             <Snowfall style={{ | ||||
|               zIndex: 2, | ||||
|               position: 'fixed', | ||||
|               width: '100vw', | ||||
|               height: '100vh'}} /> | ||||
|             <App /> | ||||
|           </> | ||||
|           <ToastContainer /> | ||||
|         </SocketContext.Provider> | ||||
|       </ProvideSettings> | ||||
|         <AppRoutes /> | ||||
|       </ProvideAuth> | ||||
|     </BrowserRouter> | ||||
|   </React.StrictMode> | ||||
| ); | ||||
|  | ||||
							
								
								
									
										16
									
								
								client/src/pages/StatsPage.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								client/src/pages/StatsPage.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| .stats-page { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   padding: 20px; | ||||
| 
 | ||||
|   .week-navigator { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     font-size: xx-large; | ||||
| 
 | ||||
|     .date-range { | ||||
|       margin: 5px 20px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										124
									
								
								client/src/pages/StatsPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								client/src/pages/StatsPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | ||||
| import { useCallback, useEffect, useState } from "react"; | ||||
| import Footer from "../components/Footer"; | ||||
| import Header from "../components/Header"; | ||||
| import { useAuth } from "../context/auth"; | ||||
| import Login from "../Login"; | ||||
| import { formatDate, getFirstWorkDayOfWeek, getHumanDate, getLastWorkDayOfWeek } from "../Utils"; | ||||
| import { getStats } from "../api/StatsApi"; | ||||
| import { WeeklyStats, LocationKey, Locations } 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"; | ||||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||||
| import './StatsPage.scss'; | ||||
| 
 | ||||
| const CHART_WIDTH = 1400; | ||||
| const CHART_HEIGHT = 700; | ||||
| const STROKE_WIDTH = 2.5; | ||||
| 
 | ||||
| const COLORS = [ | ||||
|   // Komentáře jsou kvůli vizualizaci barev ve VS Code
 | ||||
|   '#ff1493', // #ff1493
 | ||||
|   '#1e90ff', // #1e90ff
 | ||||
|   '#c5a700', // #c5a700
 | ||||
|   '#006400', // #006400
 | ||||
|   '#b300ff', // #b300ff
 | ||||
|   '#ff4500', // #ff4500
 | ||||
|   '#bc8f8f', // #bc8f8f
 | ||||
|   '#00ff00', // #00ff00
 | ||||
|   '#7c7c7c', // #7c7c7c
 | ||||
| ] | ||||
| 
 | ||||
| export default function StatsPage() { | ||||
|   const auth = useAuth(); | ||||
|   const [dateRange, setDateRange] = useState<Date[]>(); | ||||
|   const [data, setData] = useState<WeeklyStats>(); | ||||
| 
 | ||||
|   // Prvotní nastavení aktuálního týdne
 | ||||
|   useEffect(() => { | ||||
|     const today = new Date(); | ||||
|     setDateRange([getFirstWorkDayOfWeek(today), getLastWorkDayOfWeek(today)]); | ||||
|   }, []); | ||||
| 
 | ||||
|   // Přenačtení pro zvolený týden
 | ||||
|   useEffect(() => { | ||||
|     if (dateRange) { | ||||
|       getStats(formatDate(dateRange[0]), formatDate(dateRange[1])).then(setData); | ||||
|     } | ||||
|   }, [dateRange]); | ||||
| 
 | ||||
|   const renderLine = (location: Locations) => { | ||||
|     const index = Object.values(Locations).indexOf(location); | ||||
|     const key = Object.keys(Locations)[index]; | ||||
|     return <Line key={location} name={location} type="monotone" dataKey={data => data.locations[key] ?? 0} stroke={COLORS[index]} strokeWidth={STROKE_WIDTH} /> | ||||
|   } | ||||
| 
 | ||||
|   const handlePreviousWeek = () => { | ||||
|     if (dateRange) { | ||||
|       const previousStartDate = new Date(dateRange[0]); | ||||
|       previousStartDate.setDate(previousStartDate.getDate() - 7); | ||||
|       const previousEndDate = new Date(previousStartDate); | ||||
|       previousEndDate.setDate(previousEndDate.getDate() + 4); | ||||
|       setDateRange([previousStartDate, previousEndDate]); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const handleNextWeek = () => { | ||||
|     if (dateRange) { | ||||
|       const nextStartDate = new Date(dateRange[0]); | ||||
|       nextStartDate.setDate(nextStartDate.getDate() + 7); | ||||
|       const nextEndDate = new Date(nextStartDate); | ||||
|       nextEndDate.setDate(nextEndDate.getDate() + 4); | ||||
|       setDateRange([nextStartDate, nextEndDate]); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const handleKeyDown = useCallback((e: any) => { | ||||
|     if (e.keyCode === 37) { | ||||
|       handlePreviousWeek(); | ||||
|     } else if (e.keyCode === 39) { | ||||
|       handleNextWeek() | ||||
|     } | ||||
|   }, [dateRange]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     document.addEventListener('keydown', handleKeyDown); | ||||
|     return () => { | ||||
|       document.removeEventListener('keydown', handleKeyDown); | ||||
|     } | ||||
|   }, [handleKeyDown]); | ||||
| 
 | ||||
|   if (!auth?.login) { | ||||
|     return <Login />; | ||||
|   } | ||||
| 
 | ||||
|   if (!dateRange) { | ||||
|     return <Loader | ||||
|       icon={faGear} | ||||
|       description={'Načítám data...'} | ||||
|       animation={'fa-bounce'} | ||||
|     /> | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Header /> | ||||
|       <div className="stats-page"> | ||||
|         <h1>Statistiky</h1> | ||||
|         <div className="week-navigator"> | ||||
|           <FontAwesomeIcon title="Předchozí týden" icon={faChevronLeft} style={{ cursor: "pointer" }} onClick={handlePreviousWeek} /> | ||||
|           <h2 className="date-range">{getHumanDate(dateRange[0])} - {getHumanDate(dateRange[1])}</h2> | ||||
|           <FontAwesomeIcon title="Následující týden" icon={faChevronRight} style={{ cursor: "pointer" }} onClick={handleNextWeek} /> | ||||
|         </div> | ||||
|         <LineChart width={CHART_WIDTH} height={CHART_HEIGHT} data={data}> | ||||
|           {Object.values(Locations).map(location => renderLine(location))} | ||||
|           <XAxis dataKey="date" /> | ||||
|           <YAxis /> | ||||
|           <Tooltip /> | ||||
|           <Legend /> | ||||
|         </LineChart> | ||||
|       </div> | ||||
|       <Footer /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										243
									
								
								client/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										243
									
								
								client/yarn.lock
									
									
									
									
									
								
							| @ -725,6 +725,62 @@ | ||||
|   dependencies: | ||||
|     "@babel/types" "^7.20.7" | ||||
| 
 | ||||
| "@types/cookie@^0.6.0": | ||||
|   version "0.6.0" | ||||
|   resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" | ||||
|   integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== | ||||
| 
 | ||||
| "@types/d3-array@^3.0.3": | ||||
|   version "3.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" | ||||
|   integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== | ||||
| 
 | ||||
| "@types/d3-color@*": | ||||
|   version "3.1.3" | ||||
|   resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" | ||||
|   integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== | ||||
| 
 | ||||
| "@types/d3-ease@^3.0.0": | ||||
|   version "3.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" | ||||
|   integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== | ||||
| 
 | ||||
| "@types/d3-interpolate@^3.0.1": | ||||
|   version "3.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" | ||||
|   integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== | ||||
|   dependencies: | ||||
|     "@types/d3-color" "*" | ||||
| 
 | ||||
| "@types/d3-path@*": | ||||
|   version "3.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" | ||||
|   integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== | ||||
| 
 | ||||
| "@types/d3-scale@^4.0.2": | ||||
|   version "4.0.9" | ||||
|   resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" | ||||
|   integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== | ||||
|   dependencies: | ||||
|     "@types/d3-time" "*" | ||||
| 
 | ||||
| "@types/d3-shape@^3.1.0": | ||||
|   version "3.1.7" | ||||
|   resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" | ||||
|   integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== | ||||
|   dependencies: | ||||
|     "@types/d3-path" "*" | ||||
| 
 | ||||
| "@types/d3-time@*", "@types/d3-time@^3.0.0": | ||||
|   version "3.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" | ||||
|   integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== | ||||
| 
 | ||||
| "@types/d3-timer@^3.0.0": | ||||
|   version "3.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" | ||||
|   integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== | ||||
| 
 | ||||
| "@types/estree@1.0.6": | ||||
|   version "1.0.6" | ||||
|   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" | ||||
| @ -885,7 +941,7 @@ classnames@^2.3.2: | ||||
|   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" | ||||
|   integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== | ||||
| 
 | ||||
| clsx@^2.1.0: | ||||
| clsx@^2.0.0, clsx@^2.1.0: | ||||
|   version "2.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" | ||||
|   integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== | ||||
| @ -907,11 +963,87 @@ convert-source-map@^2.0.0: | ||||
|   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" | ||||
|   integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== | ||||
| 
 | ||||
| cookie@^1.0.1: | ||||
|   version "1.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" | ||||
|   integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== | ||||
| 
 | ||||
| csstype@^3.0.2: | ||||
|   version "3.1.3" | ||||
|   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" | ||||
|   integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== | ||||
| 
 | ||||
| "d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: | ||||
|   version "3.2.4" | ||||
|   resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" | ||||
|   integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== | ||||
|   dependencies: | ||||
|     internmap "1 - 2" | ||||
| 
 | ||||
| "d3-color@1 - 3": | ||||
|   version "3.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" | ||||
|   integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== | ||||
| 
 | ||||
| d3-ease@^3.0.1: | ||||
|   version "3.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" | ||||
|   integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== | ||||
| 
 | ||||
| "d3-format@1 - 3": | ||||
|   version "3.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" | ||||
|   integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== | ||||
| 
 | ||||
| "d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: | ||||
|   version "3.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" | ||||
|   integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== | ||||
|   dependencies: | ||||
|     d3-color "1 - 3" | ||||
| 
 | ||||
| d3-path@^3.1.0: | ||||
|   version "3.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" | ||||
|   integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== | ||||
| 
 | ||||
| d3-scale@^4.0.2: | ||||
|   version "4.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" | ||||
|   integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== | ||||
|   dependencies: | ||||
|     d3-array "2.10.0 - 3" | ||||
|     d3-format "1 - 3" | ||||
|     d3-interpolate "1.2.0 - 3" | ||||
|     d3-time "2.1.1 - 3" | ||||
|     d3-time-format "2 - 4" | ||||
| 
 | ||||
| d3-shape@^3.1.0: | ||||
|   version "3.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" | ||||
|   integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== | ||||
|   dependencies: | ||||
|     d3-path "^3.1.0" | ||||
| 
 | ||||
| "d3-time-format@2 - 4": | ||||
|   version "4.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" | ||||
|   integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== | ||||
|   dependencies: | ||||
|     d3-time "1 - 3" | ||||
| 
 | ||||
| "d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: | ||||
|   version "3.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" | ||||
|   integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== | ||||
|   dependencies: | ||||
|     d3-array "2 - 3" | ||||
| 
 | ||||
| d3-timer@^3.0.1: | ||||
|   version "3.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" | ||||
|   integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== | ||||
| 
 | ||||
| debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: | ||||
|   version "4.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" | ||||
| @ -926,6 +1058,11 @@ debug@~4.3.1, debug@~4.3.2: | ||||
|   dependencies: | ||||
|     ms "^2.1.3" | ||||
| 
 | ||||
| decimal.js-light@^2.4.1: | ||||
|   version "2.5.1" | ||||
|   resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" | ||||
|   integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== | ||||
| 
 | ||||
| dequal@^2.0.3: | ||||
|   version "2.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" | ||||
| @ -1010,6 +1147,11 @@ escape-string-regexp@^2.0.0: | ||||
|   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" | ||||
|   integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== | ||||
| 
 | ||||
| eventemitter3@^4.0.1: | ||||
|   version "4.0.7" | ||||
|   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" | ||||
|   integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== | ||||
| 
 | ||||
| exenv@^1.2.0: | ||||
|   version "1.2.2" | ||||
|   resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" | ||||
| @ -1026,6 +1168,11 @@ expect@^29.0.0: | ||||
|     jest-message-util "^29.7.0" | ||||
|     jest-util "^29.7.0" | ||||
| 
 | ||||
| fast-equals@^5.0.1: | ||||
|   version "5.2.2" | ||||
|   resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.2.2.tgz#885d7bfb079fac0ce0e8450374bce29e9b742484" | ||||
|   integrity sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw== | ||||
| 
 | ||||
| fill-range@^7.1.1: | ||||
|   version "7.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" | ||||
| @ -1068,6 +1215,11 @@ immutable@^5.0.2: | ||||
|   resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1" | ||||
|   integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw== | ||||
| 
 | ||||
| "internmap@1 - 2": | ||||
|   version "2.0.3" | ||||
|   resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" | ||||
|   integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== | ||||
| 
 | ||||
| invariant@^2.2.4: | ||||
|   version "2.2.4" | ||||
|   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" | ||||
| @ -1159,6 +1311,11 @@ json5@^2.2.3: | ||||
|   resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" | ||||
|   integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== | ||||
| 
 | ||||
| lodash@^4.17.21: | ||||
|   version "4.17.21" | ||||
|   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" | ||||
|   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== | ||||
| 
 | ||||
| loose-envify@^1.0.0, loose-envify@^1.4.0: | ||||
|   version "1.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" | ||||
| @ -1291,7 +1448,7 @@ react-is@^16.13.1, react-is@^16.3.2: | ||||
|   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" | ||||
|   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== | ||||
| 
 | ||||
| react-is@^18.0.0: | ||||
| react-is@^18.0.0, react-is@^18.3.1: | ||||
|   version "18.3.1" | ||||
|   resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" | ||||
|   integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== | ||||
| @ -1323,11 +1480,37 @@ react-refresh@^0.14.2: | ||||
|   resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" | ||||
|   integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== | ||||
| 
 | ||||
| react-router-dom@^7.2.0: | ||||
|   version "7.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.2.0.tgz#b8a7eae7827cd5207cf91e89807d01217737797d" | ||||
|   integrity sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q== | ||||
|   dependencies: | ||||
|     react-router "7.2.0" | ||||
| 
 | ||||
| react-router@7.2.0, react-router@^7.2.0: | ||||
|   version "7.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.2.0.tgz#a8f2729d39f634a7a870d14dd906a1b406f39d6f" | ||||
|   integrity sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ== | ||||
|   dependencies: | ||||
|     "@types/cookie" "^0.6.0" | ||||
|     cookie "^1.0.1" | ||||
|     set-cookie-parser "^2.6.0" | ||||
|     turbo-stream "2.4.0" | ||||
| 
 | ||||
| react-select-search@^4.1.6: | ||||
|   version "4.1.8" | ||||
|   resolved "https://registry.yarnpkg.com/react-select-search/-/react-select-search-4.1.8.tgz#435bdd7893685d45332813ad65b000e0dafcfbac" | ||||
|   integrity sha512-mzHhYzpaAJ3iYDjayydfb+grvvP59VWlLUWLqP26Trvm4xj2rzLnksopWZdkwWORB3dhAneqmXos3RWOMjFOxQ== | ||||
| 
 | ||||
| react-smooth@^4.0.4: | ||||
|   version "4.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4" | ||||
|   integrity sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q== | ||||
|   dependencies: | ||||
|     fast-equals "^5.0.1" | ||||
|     prop-types "^15.8.1" | ||||
|     react-transition-group "^4.4.5" | ||||
| 
 | ||||
| react-snowfall@^2.2.0: | ||||
|   version "2.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/react-snowfall/-/react-snowfall-2.2.0.tgz#0856a72c4d29d27a6a14301c0f5deb51296fb4df" | ||||
| @ -1362,6 +1545,27 @@ readdirp@^4.0.1: | ||||
|   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" | ||||
|   integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== | ||||
| 
 | ||||
| recharts-scale@^0.4.4: | ||||
|   version "0.4.5" | ||||
|   resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" | ||||
|   integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== | ||||
|   dependencies: | ||||
|     decimal.js-light "^2.4.1" | ||||
| 
 | ||||
| recharts@^2.15.1: | ||||
|   version "2.15.1" | ||||
|   resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.1.tgz#0941adf0402528d54f6d81997eb15840c893aa3c" | ||||
|   integrity sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q== | ||||
|   dependencies: | ||||
|     clsx "^2.0.0" | ||||
|     eventemitter3 "^4.0.1" | ||||
|     lodash "^4.17.21" | ||||
|     react-is "^18.3.1" | ||||
|     react-smooth "^4.0.4" | ||||
|     recharts-scale "^0.4.4" | ||||
|     tiny-invariant "^1.3.1" | ||||
|     victory-vendor "^36.6.8" | ||||
| 
 | ||||
| regenerator-runtime@^0.14.0: | ||||
|   version "0.14.1" | ||||
|   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" | ||||
| @ -1416,6 +1620,11 @@ semver@^6.3.1: | ||||
|   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" | ||||
|   integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== | ||||
| 
 | ||||
| set-cookie-parser@^2.6.0: | ||||
|   version "2.7.1" | ||||
|   resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" | ||||
|   integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== | ||||
| 
 | ||||
| slash@^3.0.0: | ||||
|   version "3.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" | ||||
| @ -1458,6 +1667,11 @@ supports-color@^7.1.0: | ||||
|   dependencies: | ||||
|     has-flag "^4.0.0" | ||||
| 
 | ||||
| tiny-invariant@^1.3.1: | ||||
|   version "1.3.3" | ||||
|   resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" | ||||
|   integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== | ||||
| 
 | ||||
| to-regex-range@^5.0.1: | ||||
|   version "5.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" | ||||
| @ -1475,6 +1689,11 @@ tslib@^2.8.0: | ||||
|   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" | ||||
|   integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== | ||||
| 
 | ||||
| turbo-stream@2.4.0: | ||||
|   version "2.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.4.0.tgz#1e4fca6725e90fa14ac4adb782f2d3759a5695f0" | ||||
|   integrity sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g== | ||||
| 
 | ||||
| typescript@^5.3.3: | ||||
|   version "5.7.2" | ||||
|   resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" | ||||
| @ -1513,6 +1732,26 @@ update-browserslist-db@^1.1.1: | ||||
|     escalade "^3.2.0" | ||||
|     picocolors "^1.1.0" | ||||
| 
 | ||||
| victory-vendor@^36.6.8: | ||||
|   version "36.9.2" | ||||
|   resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801" | ||||
|   integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ== | ||||
|   dependencies: | ||||
|     "@types/d3-array" "^3.0.3" | ||||
|     "@types/d3-ease" "^3.0.0" | ||||
|     "@types/d3-interpolate" "^3.0.1" | ||||
|     "@types/d3-scale" "^4.0.2" | ||||
|     "@types/d3-shape" "^3.1.0" | ||||
|     "@types/d3-time" "^3.0.0" | ||||
|     "@types/d3-timer" "^3.0.0" | ||||
|     d3-array "^3.1.6" | ||||
|     d3-ease "^3.0.1" | ||||
|     d3-interpolate "^3.0.1" | ||||
|     d3-scale "^4.0.2" | ||||
|     d3-shape "^3.1.0" | ||||
|     d3-time "^3.0.0" | ||||
|     d3-timer "^3.0.1" | ||||
| 
 | ||||
| vite-tsconfig-paths@^5.1.4: | ||||
|   version "5.1.4" | ||||
|   resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz#d9a71106a7ff2c1c840c6f1708042f76a9212ed4" | ||||
|  | ||||
| @ -12,6 +12,7 @@ import pizzaDayRoutes from "./routes/pizzaDayRoutes"; | ||||
| import foodRoutes from "./routes/foodRoutes"; | ||||
| import votingRoutes from "./routes/votingRoutes"; | ||||
| import easterEggRoutes from "./routes/easterEggRoutes"; | ||||
| import statsRoutes from "./routes/statsRoutes"; | ||||
| 
 | ||||
| const ENVIRONMENT = process.env.NODE_ENV || 'production'; | ||||
| dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) }); | ||||
| @ -132,6 +133,7 @@ app.use("/api/pizzaDay", pizzaDayRoutes); | ||||
| app.use("/api/food", foodRoutes); | ||||
| app.use("/api/voting", votingRoutes); | ||||
| app.use("/api/easterEggs", easterEggRoutes); | ||||
| app.use("/api/stats", statsRoutes); | ||||
| app.use(express.static('public')) | ||||
| 
 | ||||
| // Middleware pro zpracování chyb
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { getDayOfWeekIndex } from "./utils"; | ||||
| import { WeeklyStats, Locations } from "../../types"; | ||||
| 
 | ||||
| // Mockovací data pro podporované podniky, na jeden týden
 | ||||
| const MOCK_DATA = { | ||||
| @ -1384,3 +1384,79 @@ export const getMenuSenkSerikovaMock = () => { | ||||
| export const getPizzaListMock = () => { | ||||
|     return MOCK_PIZZA_LIST; | ||||
| } | ||||
| 
 | ||||
| export const getStatsMock = (): WeeklyStats => { | ||||
|     // TODO stačilo by iterovat ten enum, jako to už děláme jinde
 | ||||
|     return [ | ||||
|         { | ||||
|             date: '24.02.', | ||||
|             locations: { | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             date: '25.02.', | ||||
|             locations: { | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             date: '26.02.', | ||||
|             locations: { | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             date: '27.02.', | ||||
|             locations: { | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             date: '28.02.', | ||||
|             locations: { | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SLADOVNICKA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.TECHTOWER)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ZASTAVKAUMICHALA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SENKSERIKOVA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.SPSE)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.PIZZA)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.OBJEDNAVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.NEOBEDVAM)]]: Math.floor(Math.random() * 10), | ||||
|                 [Object.keys(Locations)[Object.values(Locations).indexOf(Locations.ROZHODUJI)]]: Math.floor(Math.random() * 10), | ||||
|             } | ||||
|         } | ||||
|     ]; | ||||
| } | ||||
							
								
								
									
										22
									
								
								server/src/routes/statsRoutes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								server/src/routes/statsRoutes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| import express, { Request, Response } from "express"; | ||||
| import { getLogin } from "../auth"; | ||||
| import { parseToken } from "../utils"; | ||||
| import { WeeklyStats } from "../../../types"; | ||||
| import { getStats } from "../stats"; | ||||
| 
 | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request<{}, any, undefined>, res: Response<WeeklyStats>) => { | ||||
|     getLogin(parseToken(req)); | ||||
|     if (typeof req.query.startDate === 'string' && typeof req.query.endDate === 'string') { | ||||
|         try { | ||||
|             const data = await getStats(req.query.startDate, req.query.endDate); | ||||
|             return res.status(200).json(data); | ||||
|         } catch (e) { | ||||
|             // necháme to zatím spadnout na 400
 | ||||
|         } | ||||
|     } | ||||
|     res.sendStatus(400); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
							
								
								
									
										44
									
								
								server/src/stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								server/src/stats.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| import { WeeklyStats, DayData, Locations, DailyStats, LocationKey } from "../../types"; | ||||
| import { getStatsMock } from "./mock"; | ||||
| import getStorage from "./storage"; | ||||
| import { formatDate } from "./utils"; | ||||
| 
 | ||||
| const storage = getStorage(); | ||||
| 
 | ||||
| /** | ||||
|  * Vypočte a vrátí statistiky jednotlivých možností pro předaný rozsah dat. | ||||
|  *  | ||||
|  * @param startDate počáteční datum | ||||
|  * @param endDate koncové datum | ||||
|  * @returns statistiky pro zadaný rozsah dat | ||||
|  */ | ||||
| export async function getStats(startDate: string, endDate: string): Promise<WeeklyStats> { | ||||
|     if (process.env.MOCK_DATA === 'true') { | ||||
|         return getStatsMock(); | ||||
|     } | ||||
|     const start = new Date(startDate); | ||||
|     const end = new Date(endDate); | ||||
| 
 | ||||
|     // Dočasná validace, aby to někdo ručně neshodil
 | ||||
|     const daysDiff = ((end as any) - (start as any)) / (1000 * 60 * 60 * 24); | ||||
|     if (daysDiff > 4) { | ||||
|         throw Error('Neplatný rozsah'); | ||||
|     } | ||||
| 
 | ||||
|     const result = []; | ||||
|     for (const date = start; date <= end; date.setDate(date.getDate() + 1)) { | ||||
|         const locationsStats: DailyStats = { | ||||
|             // TODO vytáhnout do utils funkce
 | ||||
|             date: `${String(date.getDate()).padStart(2, "0")}.${String(date.getMonth() + 1).padStart(2, "0")}.`, | ||||
|             locations: {} | ||||
|         } | ||||
|         const data: DayData = await storage.getData(formatDate(date)); | ||||
|         if (data?.choices) { | ||||
|             Object.keys(data.choices).forEach(locationKey => { | ||||
|                 locationsStats.locations[locationKey as LocationKey] = Object.keys(data.choices[locationKey as LocationKey]!).length; | ||||
|             }) | ||||
|         } | ||||
|         result.push(locationsStats); | ||||
|     } | ||||
|     return result as WeeklyStats; | ||||
| } | ||||
| @ -221,3 +221,12 @@ export type AnimationPosition = { | ||||
|     "--end-bottom"?: string, | ||||
|     rotate?: string, | ||||
| } | ||||
| 
 | ||||
| /** Statistiky pro jeden konkrétní den. */ | ||||
| export type DailyStats = { | ||||
|     date: string, // zkrácené datum v human-readable formátu (např. 24.02.)
 | ||||
|     locations: { [location in LocationKey]?: number } | ||||
| } | ||||
| 
 | ||||
| /** Statistiky pro jeden konkrétní týden (pondělí-pátek) */ | ||||
| export type WeeklyStats = [DailyStats, DailyStats, DailyStats, DailyStats, DailyStats]; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user