From ca400638d1706e7477754f19de1d74a66fc6f72b Mon Sep 17 00:00:00 2001 From: Martin Berka Date: Thu, 27 Feb 2025 00:22:34 +0100 Subject: [PATCH] =?UTF-8?q?P=C5=99id=C3=A1n=C3=AD=20z=C3=A1kladn=C3=ADch?= =?UTF-8?q?=20statistik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package.json | 3 + client/src/App.tsx | 4 +- client/src/AppRoutes.tsx | 33 +++++ client/src/Utils.tsx | 42 ++++++ client/src/api/StatsApi.ts | 8 + client/src/components/Header.tsx | 6 +- client/src/index.tsx | 27 +--- client/src/pages/StatsPage.scss | 16 ++ client/src/pages/StatsPage.tsx | 124 ++++++++++++++++ client/yarn.lock | 243 ++++++++++++++++++++++++++++++- server/src/index.ts | 2 + server/src/mock.ts | 78 +++++++++- server/src/routes/statsRoutes.ts | 22 +++ server/src/stats.ts | 44 ++++++ types/Types.ts | 11 +- 15 files changed, 637 insertions(+), 26 deletions(-) create mode 100644 client/src/AppRoutes.tsx create mode 100644 client/src/api/StatsApi.ts create mode 100644 client/src/pages/StatsPage.scss create mode 100644 client/src/pages/StatsPage.tsx create mode 100644 server/src/routes/statsRoutes.ts create mode 100644 server/src/stats.ts diff --git a/client/package.json b/client/package.json index 859efd8..c541fa5 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/App.tsx b/client/src/App.tsx index 26cb66f..f80daea 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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() { Poslední změny: {dayIndex != null && diff --git a/client/src/AppRoutes.tsx b/client/src/AppRoutes.tsx new file mode 100644 index 0000000..db8b4bf --- /dev/null +++ b/client/src/AppRoutes.tsx @@ -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 ( + + } /> + + + <> + + + + + + + } /> + + ); +} \ No newline at end of file diff --git a/client/src/Utils.tsx b/client/src/Utils.tsx index d43bb67..e79e77e 100644 --- a/client/src/Utils.tsx +++ b/client/src/Utils.tsx @@ -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}`; +} \ No newline at end of file diff --git a/client/src/api/StatsApi.ts b/client/src/api/StatsApi.ts new file mode 100644 index 0000000..03a3a71 --- /dev/null +++ b/client/src/api/StatsApi.ts @@ -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(`${STATS_API_PREFIX}?startDate=${startDate}&endDate=${endDate}`); +} diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 840e43e..c45377e 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -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(false); const [votingModalOpen, setVotingModalOpen] = useState(false); const [pizzaModalOpen, setPizzaModalOpen] = useState(false); @@ -108,7 +111,7 @@ export default function Header() { } return - Luncher + Luncher