diff --git a/.env b/.env index 66bbe2ab8b4c7b3006dfa6640bb96066a95796cf..3096b9a1b57dc92f62c74a46ab03fc7f3c026f34 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 c92643b4690815096e81d7d8eef22a0db3302513..75b4a90839b0ac2778a40dd8391053a39d60ffb6 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 5fc7b32161b77b63f2fb421876a9821ab7346c8c..f872dfd04e8980e48d1a04697589551f23a12fd0 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 4238d2d43970df23307b9bc582d24f54e595546c..747c6c449669ac21d125dee98a4dcb33293f4a75 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 fe88505c298c04652412b0bd52861bd22420dc96..92ea09cde20da32cb897392cb5a1456be3707c35 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 0000000000000000000000000000000000000000..2c4584258cdd1d1463c126e524d0cc8c243891eb --- /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 612654057a5162f37a661e5c27683de81d678b3f..6365546746d3dacc2fe1151764bf6d198aaf0eeb 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 97fadafde097688c36a0b25e83bae8e14dd05825..4221673a2bac5dc34cc3d97406ab198b68d721bc 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 e59111b39c83510f7ffe96e941196064362ce480..bf31cf18ac699a07fef0e349f2041df32f1329b8 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 9a295d99a8142096e5a31a0a62987d9cd0d187f8..aaf73535f8d6d27cb9a161b7ab134701b22393a2 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 61895755e2b0eb4fa05e0a4bdde237a16606aae5..539ebe85d42af6e44678d49a8a2b6ebf073b3c6f 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[];