diff --git a/package-lock.json b/package-lock.json index e60ca1144c862c4a5463903c03da863855b696d6..483e11c9f9cfc3c2029fc9e461982d74352f3a54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1421,6 +1421,11 @@ "prop-types": "^15.7.2" } }, + "@rooks/use-interval": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rooks/use-interval/-/use-interval-4.5.0.tgz", + "integrity": "sha512-As0DueIAGLJLYATKPPOCDGqoIlwbhPAcYP14TNTHaAj9/ODdvUYFXAP3jFCRzDNpjXCIgSe4oBuzVVmM526n+Q==" + }, "@rooks/use-window-size": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@rooks/use-window-size/-/use-window-size-4.5.0.tgz", diff --git a/package.json b/package.json index 1a9254ee2e8fd94d079f8f1548d943b2866b8c8c..e344b74b4cbb6446dd5aaade8f2c8fd01224159b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@react-keycloak/web": "^2.1.4", + "@rooks/use-interval": "^4.5.0", "@rooks/use-window-size": "^4.5.0", "@sentry/react": "^5.29.2", "classnames": "^2.2.6", diff --git a/src/App.jsx b/src/App.jsx index 0d4facf059b547b7a7482880b04793b2f13289f7..1ba3568f3ac2b6bcf3ed6f744f3e315ee7f891f0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,6 +11,7 @@ import Footer from "components/Footer"; import Navbar from "components/Navbar"; import Home from "pages/Home"; import Program from "pages/Program"; +import Protocol from "pages/Protocol"; import { AuthStore, PostStore } from "stores"; import { updateWindowPosts } from "utils"; @@ -82,12 +83,12 @@ const LoadingComponent = ( const NotFound = () => ( <article className="container container--default py-8 lg:py-24"> <h1 className="head-alt-base lg:head-alt-lg mb-8"> - 404ka: Tahle stránka tu není + 404ka: tak tahle stránka tu není </h1> <p className="text-base lg:text-xl mb-8"> - Dostali jste se na takzvanou „<strong>čtyřystačtyřku</strong>“, což - znamená, že stránka, kterou jste se pokusili navštívit, na tomhle webu - není. + Dostal/a ses na takzvanou „<strong>čtyřystačtyřku</strong>“, což znamená, + že stránka, kterou jsi se pokusil/a navštívit, na tomhle webu není. + Zkontroluj, zda máš správný odkaz. </p> <Button routerTo="/" className="text-base lg:text-xl" hoverActive fullwidth> Přejít na hlavní stránku @@ -104,6 +105,7 @@ const BaseApp = () => { <Switch> <Route exact path="/" children={<Home />} /> <Route exact path="/program" children={<Program />} /> + <Route exact path="/protocol" children={<Protocol />} /> <Route component={NotFound} /> </Switch> <Footer /> diff --git a/src/actions/global-info.js b/src/actions/global-info.js index 342d31ccdef346d7214e8d3feba9cc23b53e57c1..11b518f5838ca4de4186e039e0fc589e14335eee 100644 --- a/src/actions/global-info.js +++ b/src/actions/global-info.js @@ -1,7 +1,9 @@ import isArray from "lodash/isArray"; import { createAsyncAction, errorResult, successResult } from "pullstate"; +import baseFetch from "unfetch"; import { fetch } from "api"; +import { markdownConverter } from "markdown"; import { GlobalInfoStore } from "stores"; export const loadConfig = createAsyncAction( @@ -30,9 +32,39 @@ export const loadConfig = createAsyncAction( if (rawConfigItem.id === "websocket_url") { state.websocketUrl = rawConfigItem.value; } + if (rawConfigItem.id === "record_url") { + state.protocolUrl = rawConfigItem.value; + } }); }); } }, } ); + +export const loadProtocol = createAsyncAction( + async () => { + const { protocolUrl } = GlobalInfoStore.getRawState(); + + try { + const resp = await baseFetch(protocolUrl); + + if (resp.status !== 200) { + return errorResult([], `Unexpected status code ${resp.status}`); + } + + return successResult(await resp.text()); + } catch (err) { + return errorResult([], err.toString()); + } + }, + { + postActionHook: ({ result }) => { + if (!result.error) { + GlobalInfoStore.update((state) => { + state.protocol = markdownConverter.makeHtml(result.payload); + }); + } + }, + } +); diff --git a/src/components/Button.jsx b/src/components/Button.jsx index 3e7b79152138a6784b0478ddc1c2cb0c547a1efa..a2491343665889955ea1dd953bdcb61ebb27cff9 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -12,6 +12,7 @@ const Button = ({ loading = false, children, routerTo, + bodyProps = {}, ...props }) => { const btnClass = classNames( @@ -30,7 +31,9 @@ const Button = ({ const inner = ( <div className="btn__body-wrap"> - <div className={bodyClass}>{children}</div> + <div className={bodyClass} {...bodyProps}> + {children} + </div> {!!icon && ( <div className="btn__icon"> <i className={icon}></i> diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index e31b3a89e424685586c28c9730b3f3a21775c18e..3f11e6cc0f2b6f86e7fddbb02c772b035a1501ab 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -30,6 +30,9 @@ const Footer = () => { <li> <NavLink to="/program">Program</NavLink> </li> + <li> + <NavLink to="/protocol">Zápis</NavLink> + </li> </ul> </div> </div> diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 89679a817e7cf10def1c3da6ec8c4e1e05811ce5..ce7d9a0ec8d8c947d6326e9e032ad13af86619d5 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -100,6 +100,11 @@ const Navbar = () => { Program </NavLink> </li> + <li className="navbar-menu__item"> + <NavLink className="navbar-menu__link" to="/protocol"> + Zápis + </NavLink> + </li> </ul> </div> <div className="navbar__actions navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto self-start flex flex-row items-center"> diff --git a/src/pages/Program.jsx b/src/pages/Program.jsx index 964dd645af4b62948f22356f6a8a17ab49ad9d3c..7b5c1088499555379829916a0bdeb30303dc5b32 100644 --- a/src/pages/Program.jsx +++ b/src/pages/Program.jsx @@ -25,7 +25,7 @@ const Schedule = () => { ); return ( - <article className="container container--wide py-8 lg:py-24"> + <article className="container container--default py-8 lg:py-24"> <h1 className="head-alt-md lg:head-alt-lg mb-8">Program zasedání</h1> <div className="flex flex-col"> {scheduleIds.map((id) => { diff --git a/src/pages/Protocol.jsx b/src/pages/Protocol.jsx new file mode 100644 index 0000000000000000000000000000000000000000..35709933cd8945441ca8448d8539d857b9c9f8d9 --- /dev/null +++ b/src/pages/Protocol.jsx @@ -0,0 +1,110 @@ +import React, { useCallback, useState } from "react"; +import useInterval from "@rooks/use-interval"; + +import { loadProtocol } from "actions/global-info"; +import Button from "components/Button"; +import ErrorMessage from "components/ErrorMessage"; +import { useActionState } from "hooks"; +import { GlobalInfoStore } from "stores"; + +const Protocol = () => { + const { protocolUrl, protocol } = GlobalInfoStore.useState(); + const [protocolLoading, protocolLoadError] = useActionState(loadProtocol); + const [progressPercent, setProgressPercent] = useState(0); + const [paused, setPaused] = useState(false); + + const forceLoad = useCallback(async () => { + try { + setPaused(true); + setProgressPercent(1); + await loadProtocol.run(); + } finally { + setPaused(false); + } + }, [setPaused, setProgressPercent]); + + const tick = useCallback(async () => { + if (paused) { + return; + } + + if (progressPercent % 100 === 0) { + forceLoad(); + } else { + setProgressPercent(progressPercent + 1); + } + }, [forceLoad, paused, progressPercent, setProgressPercent]); + + useInterval(tick, 100, true); + + const htmlContent = protocol + ? { + __html: protocol, + } + : null; + + const progressStyle = { + position: "absolute", + width: `${progressPercent}%`, + height: "100%", + left: "0", + background: + "linear-gradient(142deg, rgba(2,0,36,1) 0%, rgba(51,51,51,1) 0%, rgba(255,255,255,1) 100%)", + opacity: "0.4", + }; + + return ( + <article className="container container--default py-8 lg:py-24"> + <h1 className="head-alt-md lg:head-alt-lg mb-8">Zápis z jednání</h1> + + <div className="flex items-start"> + <div className="lg:w-2/3"> + {!protocolUrl && ( + <ErrorMessage>Zápis momentálně není k dispozici.</ErrorMessage> + )} + {protocolLoadError && ( + <ErrorMessage> + Při stahování záznamu z jednání došlo k problému:{" "} + {protocolLoadError.toString()} + </ErrorMessage> + )} + {protocolUrl && <></>} + {htmlContent && ( + <div + className="leading-tight text-sm lg:text-base content-block" + dangerouslySetInnerHTML={htmlContent} + ></div> + )} + </div> + <div className="hidden lg:block card elevation-10 w-1/3"> + <div className="lg:card__body content-block"> + <h2>Jak to funguje?</h2> + <p> + Zápis se aktualizuje automaticky každých 10 sekund. Pokud chceš + aktualizaci vynutit ručně, můžeš využít tlačítko níže. + </p> + <Button + icon="ico--refresh" + loading={protocolLoading} + className="btn--fullwidth" + onClick={forceLoad} + color="black" + bodyProps={{ + style: { + position: "relative", + }, + }} + > + <span style={progressStyle}></span> + <span style={{ position: "relative" }}> + {protocolLoading ? "Aktualizace..." : "Aktualizovat"} + </span> + </Button> + </div> + </div> + </div> + </article> + ); +}; + +export default Protocol; diff --git a/src/stores.js b/src/stores.js index 731d36c01830aac73995e4ce9ed1f05ed6c0606c..ef240f8d7823ea62bcaff2b4ca1712c803602818 100644 --- a/src/stores.js +++ b/src/stores.js @@ -8,6 +8,8 @@ const globalInfoStoreInitial = { onlineUsers: 0, websocketUrl: null, streamUrl: null, + protocolUrl: null, + protocol: null, }; export const GlobalInfoStore = new Store(globalInfoStoreInitial); diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts index 2afcdfeda148eadf4efda2fbace8f708139a31cc..a2f97abef673408e75893b4bb36ca82294fc5ccc 100644 --- a/typings/cf2021.d.ts +++ b/typings/cf2021.d.ts @@ -4,6 +4,8 @@ declare namespace CF2021 { onlineUsers: number; websocketUrl: string; streamUrl?: string; + protocolUrl?: string; + protocol?: string; } interface ProgramScheduleEntry {