From c2fe36f7e578d04c46b83121ebec997d206ac151 Mon Sep 17 00:00:00 2001 From: xaralis <filip.varecha@fragaria.cz> Date: Wed, 16 Dec 2020 10:49:39 +0100 Subject: [PATCH] feat: add schedule page, load schedule and use it --- .env | 1 + package-lock.json | 5 ++++ package.json | 3 +- src/App.jsx | 4 ++- src/actions/misc.js | 1 + src/actions/program.js | 62 +++++++++++++++++++++++++++++++++++++++ src/components/Button.jsx | 32 ++++++++++++++------ src/pages/Home.jsx | 29 ++++++++++++++++-- src/pages/Program.jsx | 51 +++++++++++++++++++++++++++++++- src/stores.js | 8 +++++ typings/cf2021.d.ts | 9 ++++-- 11 files changed, 188 insertions(+), 17 deletions(-) create mode 100644 src/actions/program.js diff --git a/.env b/.env index 66bbe2a..3096b9a 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ REACT_APP_STYLEGUIDE_URL=http://localhost:3001 +REACT_APP_API_BASE_URL=https://cf2021.pirati.cz/api diff --git a/package-lock.json b/package-lock.json index c92643b..75b4a90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13783,6 +13783,11 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.22.tgz", "integrity": "sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==" }, + "unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", diff --git a/package.json b/package.json index 5fc7b32..f872dfd 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "react-dom": "^16.13.1", "react-modal": "^3.12.1", "react-router-dom": "^5.2.0", - "react-scripts": "3.4.3" + "react-scripts": "3.4.3", + "unfetch": "^4.2.0" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.jsx b/src/App.jsx index 4238d2d..747c6c4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import { KeycloakProvider } from "@react-keycloak/web"; import * as Sentry from "@sentry/react"; import { loadGroupMappings } from "actions/misc"; +import { loadProgram } from "actions/program"; import Footer from "components/Footer"; import Navbar from "components/Navbar"; import Home from "pages/Home"; @@ -42,7 +43,7 @@ const LoadingComponent = ( <div className="h-screen w-screen flex justify-center items-center"> <div className="text-center"> <img - src={`${process.env.REACT_APP_STYLEGUIDE_URL}images/logo-round-black.svg`} + src={`${process.env.REACT_APP_STYLEGUIDE_URL}/images/logo-round-black.svg`} className="w-20 mb-2 inline-block" alt="Pirátská strana" /> @@ -54,6 +55,7 @@ const LoadingComponent = ( const BaseApp = () => { loadGroupMappings.read(); + loadProgram.read(); return ( <Router> diff --git a/src/actions/misc.js b/src/actions/misc.js index fe88505..92ea09c 100644 --- a/src/actions/misc.js +++ b/src/actions/misc.js @@ -1,4 +1,5 @@ import { createAsyncAction, errorResult, successResult } from "pullstate"; +import fetch from "unfetch"; import { AuthStore } from "stores"; diff --git a/src/actions/program.js b/src/actions/program.js new file mode 100644 index 0000000..2c45842 --- /dev/null +++ b/src/actions/program.js @@ -0,0 +1,62 @@ +import pick from "lodash/pick"; +import { createAsyncAction, errorResult, successResult } from "pullstate"; +import fetch from "unfetch"; + +import { ProgramStore } from "stores"; + +export const loadProgram = createAsyncAction( + async () => { + try { + const resp = await fetch(`${process.env.REACT_APP_API_BASE_URL}/program`); + const mappings = await resp.json(); + return successResult(mappings); + } catch (err) { + return errorResult([], err.toString()); + } + }, + { + postActionHook: ({ result }) => { + if (!result.error) { + const entries = result.payload + .map( + /** + * + * @param {any} entry + * @returns {CF2021.ProgramScheduleEntry} + */ + (entry) => { + return { + ...pick(entry, [ + "id", + "number", + "title", + "description", + "proposer", + ]), + expectedStartAt: new Date(entry.expectedStartAt), + expectedFinishAt: entry.expectedFinishAt + ? new Date(entry.expectedFinishAt) + : undefined, + }; + } + ) + .sort((a, b) => a.expectedStartAt - b.expectedStartAt); + + const currentEntry = result.payload.find((entry) => entry.is_live); + + ProgramStore.update((state) => { + state.schedule = entries; + + if (currentEntry) { + state.current = state.schedule.find( + (scheduleEntry) => scheduleEntry.id === currentEntry.id + ); + } else { + // TODO: for testing only + state.current = state.schedule[1]; + } + }); + } + }, + } +); diff --git a/src/components/Button.jsx b/src/components/Button.jsx index 6126540..6365546 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -1,4 +1,5 @@ import React from "react"; +import { NavLink } from "react-router-dom"; import classNames from "classnames"; const Button = ({ @@ -10,6 +11,7 @@ const Button = ({ hoverActive = true, fullwidth = false, children, + routerTo, ...props }) => { const btnClass = classNames( @@ -25,17 +27,29 @@ const Button = ({ const iconWrapperClass = classNames("btn__icon", iconWrapperClassName); + const inner = ( + <div className="btn__body-wrap"> + <div className="btn__body">{children}</div> + {!!icon && ( + <div className={iconWrapperClass}> + <i className={icon}></i> + {iconChildren} + </div> + )} + </div> + ); + + if (routerTo) { + return ( + <NavLink to={routerTo} className={btnClass} {...props}> + {inner} + </NavLink> + ); + } + return ( <button className={btnClass} {...props}> - <div className="btn__body-wrap"> - <div className="btn__body">{children}</div> - {!!icon && ( - <div className={iconWrapperClass}> - <i className={icon}></i> - {iconChildren} - </div> - )} - </div> + {inner} </button> ); }; diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 97fadaf..4221673 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; +import Button from "components/Button"; import { DropdownMenu, DropdownMenuItem } from "components/dropdown-menu"; import ModalConfirm from "components/modals/ModalConfirm"; import AddAnnouncementForm from "containers/AddAnnouncementForm"; @@ -7,12 +8,33 @@ import AddPostForm from "containers/AddPostForm"; import AnnouncementsContainer from "containers/AnnoucementsContainer"; import PostFilters from "containers/PostFilters"; import PostsContainer from "containers/PostsContainer"; +import { ProgramStore } from "stores"; + +const noCurrentDiscussion = ( + <article className="container container--wide pt-8 py-8 lg:py-32"> + <div className="hidden md:inline-block flag bg-violet-400 text-white head-alt-base mb-4 py-4 px-5"> + Jejda ... + </div> + <h1 className="head-alt-base md:head-alt-md lg:head-alt-xl mb-2"> + Jednání ještě nebylo zahájeno :( + </h1> + <p className="text-xl leading-snug mb-8"> + Jednání celostátního fóra v tuto chvíli neprobíhá. Můžete si ale zobrazit + program. + </p> + <Button routerTo="/program" className="md:text-lg lg:text-xl" hoverActive> + Zobrazit program + </Button> + </article> +); const Home = () => { const [showEndDiscussionConfirm, setShowEndDiscussionConfirm] = useState( false ); + const { current } = ProgramStore.useState(); + const onRenameProgramPoint = () => { console.log("renameProgramPoint"); }; @@ -24,14 +46,17 @@ const Home = () => { console.log("endProgramPoint"); }; + if (!current) { + return noCurrentDiscussion; + } + return ( <> <article className="container container--wide pt-8 lg:py-24 cf2021"> <section className="cf2021__video space-y-8"> <div className="flex items-center justify-between mb-4 lg:mb-8"> <h1 className="head-alt-md lg:head-alt-lg mb-0"> - Bod č. 1: Programové priority Pirátské strany pro sněmovní volby - 2021 + Bod č. {current.number}: {current.title} </h1> <DropdownMenu right triggerSize="lg"> <DropdownMenuItem diff --git a/src/pages/Program.jsx b/src/pages/Program.jsx index e59111b..bf31cf1 100644 --- a/src/pages/Program.jsx +++ b/src/pages/Program.jsx @@ -1,7 +1,56 @@ import React from "react"; +import { Link } from "react-router-dom"; +import classNames from "classnames"; +import { format } from "date-fns"; + +import Chip from "components/Chip"; +import { ProgramStore } from "stores"; const Schedule = () => { - return <>Schedule</>; + const { current, schedule } = ProgramStore.useState(); + return ( + <article className="container container--wide 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"> + {schedule.map((entry) => { + const isCurrent = entry === current; + return ( + <div + className={classNames( + "flex flex-col md:flex-row my-4 hover:opacity-100 transition duration-300", + { + "text-black": isCurrent, + "text-black opacity-50": !isCurrent, + } + )} + key={entry.id} + > + <div className="w-28 md:text-right"> + {isCurrent && ( + <Chip condensed className="mt-2 mr-2" color="red-600"> + Právě probíhá + </Chip> + )} + </div> + <div className="w-32 flex items-start head-heavy-xs md:head-heavy-base text-center"> + <span>{format(entry.expectedStartAt, "H:mm")}</span> + </div> + <div className="flex-grow w-full"> + <h2 className="head-heavy-xs md:head-heavy-base mb-1"> + <Link to="/">{entry.title}</Link> + </h2> + <div className="flex space-x-2"> + <strong>Navrhovatel:</strong> + <span>{entry.proposer}</span> + </div> + {entry.description && <p>{entry.description}</p>} + </div> + </div> + ); + })} + </div> + </article> + ); }; export default Schedule; diff --git a/src/stores.js b/src/stores.js index 9a295d9..aaf7353 100644 --- a/src/stores.js +++ b/src/stores.js @@ -13,6 +13,14 @@ const authStoreInitial = { export const AuthStore = new Store(authStoreInitial); +/** @type {CF2021.ProgramStorePayload} */ +const programStoreInitial = { + current: undefined, + schedule: [], +}; + +export const ProgramStore = new Store(programStoreInitial); + /** @type {CF2021.AnnouncementStorePayload} */ const announcementStoreInitial = { items: [ diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts index 6189575..539ebe8 100644 --- a/typings/cf2021.d.ts +++ b/typings/cf2021.d.ts @@ -1,13 +1,16 @@ declare namespace CF2021 { interface ProgramScheduleEntry { - id: string; + id: number; + number: string; title: string; + proposer: string; + description?: string; expectedStartAt: Date; - expectedFinishAt: Date; + expectedFinishAt?: Date; } export interface ProgramStorePayload { - current: ProgramScheduleEntry & { + current?: ProgramScheduleEntry & { discussionOpened: boolean; } schedule: ProgramScheduleEntry[]; -- GitLab