diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..1923d4101aa52161105a6f9d54e29f68ace4b19b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/package.json b/package.json index 45a1787108f036d8611f765fb210c214da6aedc5..585c6a78c78f9865ec20317e6be8523fe9ae0475 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "classnames": "^2.2.6", "date-fns": "^2.16.1", "keycloak-js": "^10.0.2", + "lodash": "^4.17.20", "pullstate": "^1.20.4", "react": "^16.13.1", "react-device-detect": "^1.13.1", @@ -51,7 +52,7 @@ "^@?\\w" ], [ - "^(components|containers|pages|utils)(/.*|$)" + "^(components|containers|pages|utils|stores|keycloak)(/.*|$)" ], [ "^(test-utils)(/.*|$)" diff --git a/src/App.jsx b/src/App.jsx index a2e7648d1891d535e0e300b8148168de18524f14..848f3da3ad726e9583dce37352d6817469205177 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,13 +1,14 @@ -import React, { useCallback, useEffect } from "react"; +import React, { Suspense } from "react"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import { KeycloakProvider } from "@react-keycloak/web"; import * as Sentry from "@sentry/react"; -import { AuthStore } from "stores"; +import { loadGroupMappings } from "actions/misc"; import Footer from "components/Footer"; import Navbar from "components/Navbar"; import Home from "pages/Home"; import Program from "pages/Program"; +import { AuthStore } from "stores"; import keycloak from "./keycloak"; @@ -30,6 +31,7 @@ const onKeycloakEvent = (event) => { state.user = { name: keycloak.tokenParsed.name, groups: keycloak.tokenParsed.groups, + accessToken: keycloak.token, }; }); } @@ -50,18 +52,7 @@ const LoadingComponent = ( ); const BaseApp = () => { - const loadGroupMappings = useCallback(async () => { - const resp = await fetch("https://iapi.pirati.cz/v1/groups"); - const mappings = await resp.json(); - - AuthStore.update((state) => { - state.groupMappings = mappings; - }); - }, []); - - useEffect(() => { - loadGroupMappings(); - }, [loadGroupMappings]); + loadGroupMappings.read(); return ( <Router> @@ -91,29 +82,33 @@ const AuthenticatedApp = () => { LoadingComponent={LoadingComponent} onEvent={onKeycloakEvent} > - <BaseApp /> + <Suspense fallback={LoadingComponent}> + <BaseApp /> + </Suspense> </KeycloakProvider> </> ); }; -const ErrorBoundaryFallback = () => ( - <div className="h-screen w-screen flex justify-center items-center"> - <div className="text-center"> - <h1 className="head-alt-xl text-red-600 mb-4"> - V aplikaci došlo k chybě :( - </h1> - <p className="text-lg leading-normal"> - Naši vývojáři o tom již byli informování a opraví to co nejdříve. - <br /> - Omlouváme se za tuto nepříjemnost. - </p> - <a href="/" className="btn mt-8"> - <div className="btn__body">Načíst znovu</div> - </a> +const ErrorBoundaryFallback = ({ error }) => { + return ( + <div className="h-screen w-screen flex justify-center items-center"> + <div className="text-center"> + <h1 className="head-alt-xl text-red-600 mb-4"> + V aplikaci došlo k chybě :( + </h1> + <p className="text-lg leading-normal"> + Naši vývojáři o tom již byli informování a opraví to co nejdříve. + <br /> + Omlouváme se za tuto nepříjemnost. + </p> + <a href="/" className="btn mt-8"> + <div className="btn__body">Načíst znovu</div> + </a> + </div> </div> - </div> -); + ); +}; const App = Sentry.withProfiler(() => { return ( diff --git a/src/actions/misc.js b/src/actions/misc.js new file mode 100644 index 0000000000000000000000000000000000000000..fe88505c298c04652412b0bd52861bd22420dc96 --- /dev/null +++ b/src/actions/misc.js @@ -0,0 +1,24 @@ +import { createAsyncAction, errorResult, successResult } from "pullstate"; + +import { AuthStore } from "stores"; + +export const loadGroupMappings = createAsyncAction( + async () => { + try { + const resp = await fetch("https://iapi.pirati.cz/v1/groups"); + const mappings = await resp.json(); + return successResult(mappings); + } catch (err) { + return errorResult([], err.toString()); + } + }, + { + postActionHook: ({ result }) => { + if (!result.error) { + AuthStore.update((state) => { + state.groupMappings = result.payload; + }); + } + }, + } +); diff --git a/src/components/Chip.jsx b/src/components/Chip.jsx index dd4603bf47980eeb6c49a5036c19690aaacfdb5d..4e9895cc1f8d2aa00d0a80658882f2fe9d0ad0a0 100644 --- a/src/components/Chip.jsx +++ b/src/components/Chip.jsx @@ -1,17 +1,29 @@ import React from "react"; import classNames from "classnames"; -const Chip = ({ className, color = "grey-125", condensed, children }) => { +const Chip = ({ + className, + color = "grey-125", + condensed, + hoveractive = false, + children, + ...props +}) => { const chipClass = classNames( "chip", { "chip--condensed": !!condensed, + "chip--hoveractive": !!hoveractive, }, `chip--${color}`, className ); - return <span className={chipClass}>{children}</span>; + return ( + <span className={chipClass} {...props}> + {children} + </span> + ); }; export default Chip; diff --git a/src/components/Dropdown.jsx b/src/components/Dropdown.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f7e1a49e0d0bdf8ad1f7bac134c2ba373462be34 --- /dev/null +++ b/src/components/Dropdown.jsx @@ -0,0 +1,28 @@ +import React from "react"; +import classNames from "classnames"; + +const Dropdown = ({ value, options, onChange, className }) => { + const onSelectChanged = (evt) => { + onChange(evt.target.value); + }; + + return ( + <span + className={classNames( + "chip chip--grey-125 chip--select chip--hoveractive", + className + )} + > + <select onChange={onSelectChanged} value={value}> + {options.map((opt) => ( + <option key={opt.value} value={opt.value}> + {opt.title} + </option> + ))} + </select> + <span className="chip__icon ico--chevron-down"></span> + </span> + ); +}; + +export default Dropdown; diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 838698c82636fb171aef3bb12ad7551e38147ac0..1a908b253eefb0329e987cf8ecd23ad73dbe431e 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -2,9 +2,9 @@ import React, { useCallback, useState } from "react"; import { isBrowser } from "react-device-detect"; import { NavLink } from "react-router-dom"; import { useKeycloak } from "@react-keycloak/web"; -import { AuthStore } from "stores"; import Button from "components/Button"; +import { AuthStore } from "stores"; const Navbar = () => { const [showMenu, setShowMenu] = useState(isBrowser); diff --git a/src/components/Thumbs.jsx b/src/components/Thumbs.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c267eeee91f4b075f3e59c0401d90bedf14480c7 --- /dev/null +++ b/src/components/Thumbs.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import classNames from "classnames"; + +const Thumbs = ({ likes, dislikes, onLike, onDislike, canThumb }) => { + return ( + <div> + <div className="space-x-2 text-sm flex items-center"> + <button + className={classNames("text-blue-300 flex items-center space-x-1", { + "cursor-not-allowed": !canThumb, + })} + disabled={!canThumb} + onClick={onLike} + > + <span className="font-bold">{likes}</span> + <i className="ico--thumbs-up"></i> + </button> + <button + className={classNames("text-red-600 flex items-center space-x-1", { + "cursor-not-allowed": !canThumb, + })} + disabled={!canThumb} + onClick={onDislike} + > + <i className="ico--thumbs-down transform -scale-x-1"></i> + <span className="font-bold">{dislikes}</span> + </button> + </div> + </div> + ); +}; + +export default React.memo(Thumbs); diff --git a/src/components/annoucements/Announcement.jsx b/src/components/annoucements/Announcement.jsx index 39b769b8b62e16aef917a7f8df92c54dfd4c8d89..392f7da5a512c1810d3c3eb80e13f2790d0c976d 100644 --- a/src/components/annoucements/Announcement.jsx +++ b/src/components/annoucements/Announcement.jsx @@ -34,7 +34,7 @@ const Announcement = ({ "accepted-procedure-proposal": "green-400", voting: "red-600", announcement: "cyan-500", - "user-bank": "black", + "user-ban": "black", }[type]; const chipLabel = { diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx index 2e8a892e5a27d66ecd0e322aeb18b380fe1165b4..675642415e283cfb7d8e6722bbb0a007acbd6b27 100644 --- a/src/components/posts/Post.jsx +++ b/src/components/posts/Post.jsx @@ -3,6 +3,7 @@ import classNames from "classnames"; import { format } from "date-fns"; import Chip from "components/Chip"; +import Thumbs from "components/Thumbs"; const Post = ({ className, @@ -15,12 +16,16 @@ const Post = ({ archived, state, historyLog, + onLike, + onDislike, + dimIfArchived = true, }) => { const wrapperClassName = classNames( - "flex items-start p-4 lg:p-2 lg:py-4 lg:-mx-2", + "flex items-start p-4 lg:p-2 lg:py-3 lg:-mx-2", { "bg-yellow-100 bg-opacity-50": !seen, - "opacity-25 hover:opacity-100 transition-opacity duration-200": !!archived, + "opacity-25 hover:opacity-100 transition-opacity duration-200": + dimIfArchived && !!archived, }, className ); @@ -127,16 +132,13 @@ const Post = ({ </div> </div> <div className="flex items-center space-x-4"> - <div className="space-x-2 text-sm flex items-center"> - <button className="text-blue-300 flex items-center space-x-1"> - <span className="font-bold">{ranking.likes}</span> - <i className="ico--thumbs-up"></i> - </button> - <button className="text-red-600 flex items-center space-x-1"> - <i className="ico--thumbs-down transform -scale-x-1"></i> - <span className="font-bold">{ranking.dislikes}</span> - </button> - </div> + <Thumbs + likes={ranking.likes} + dislikes={ranking.dislikes} + onLike={onLike} + onDislike={onDislike} + canThumb={ranking.myVote === "none"} + /> </div> </div> </div> diff --git a/src/components/posts/PostFilters.jsx b/src/components/posts/PostFilters.jsx new file mode 100644 index 0000000000000000000000000000000000000000..376cd32c4b70d1ecde6d7b9ad893e01ab1bc5042 --- /dev/null +++ b/src/components/posts/PostFilters.jsx @@ -0,0 +1,114 @@ +import React, { useCallback } from "react"; +import pick from "lodash/pick"; + +import Chip from "components/Chip"; +import Dropdown from "components/Dropdown"; +import { PostStore } from "stores"; +import { filterPosts } from "utils"; + +const PostFilters = () => { + const { window, filters, items } = PostStore.useState((state) => + pick(state, ["window", "filters", "items"]) + ); + + const flagsOptions = [ + { title: "Vše", value: "all" }, + { title: "Jen aktivní", value: "active" }, + { title: "Jen archivované", value: "archived" }, + ]; + const sortOptions = [ + { title: "Podle času", value: "byDate" }, + { title: "Podle podpory", value: "byScore" }, + ]; + const typeOptions = [ + { title: "Návrhy i příspěvky", value: "all" }, + { title: "Jen návrhy", value: "proposalsOnly" }, + { title: "Jen příspěvky", value: "discussionOnly" }, + ]; + const hasNextPage = window.page * window.perPage < window.itemCount; + const hasPrevPage = window.page > 1; + + const setFilter = (prop, newValue, resetPage = true) => { + PostStore.update((state) => { + state.filters[prop] = newValue; + state.window.items = filterPosts(state.filters, items); + state.window.itemCount = state.window.items.length; + + if (resetPage) { + state.window.page = 1; + } + }); + }; + + const onFlagsChange = (newValue) => setFilter("flags", newValue); + const onSortChange = (newValue) => setFilter("sort", newValue, false); + const onTypeChange = (newValue) => setFilter("type", newValue); + + const onNextPage = useCallback(() => { + if (hasNextPage) { + PostStore.update((state) => { + state.window.page = state.window.page + 1; + }); + } + }, [hasNextPage]); + const onPrevPage = useCallback(() => { + if (hasPrevPage) { + PostStore.update((state) => { + state.window.page = state.window.page - 1; + }); + } + }, [hasPrevPage]); + + const enabledPaginatorClass = "cursor-pointer text-xs"; + const disabledPaginatorClass = "opacity-25 cursor-not-allowed text-xs"; + + return ( + <div className="flex flex-col space-y-2 xl:space-y-0 xl:space-x-8 xl:flex-row xl:items-center"> + <div className="-mx-1"> + <Dropdown + value={filters.flags} + onChange={onFlagsChange} + options={flagsOptions} + className="text-xs ml-1 mt-2 xl:mt-0" + /> + <Dropdown + value={filters.sort} + onChange={onSortChange} + options={sortOptions} + className="text-xs ml-1 mt-2 xl:mt-0" + /> + <Dropdown + value={filters.type} + onChange={onTypeChange} + options={typeOptions} + className="text-xs ml-1 mt-2 xl:mt-0" + /> + </div> + + <div> + <Chip + color="grey-125" + className={ + hasPrevPage ? enabledPaginatorClass : disabledPaginatorClass + } + hoveractive + onClick={onPrevPage} + > + <span className="ico--chevron-left"></span> + </Chip> + <Chip + color="grey-125" + className={ + hasNextPage ? enabledPaginatorClass : disabledPaginatorClass + } + hoveractive + onClick={onNextPage} + > + <span className="ico--chevron-right"></span> + </Chip> + </div> + </div> + ); +}; + +export default PostFilters; diff --git a/src/components/posts/PostList.jsx b/src/components/posts/PostList.jsx index efdb9ccc332f773edeb11c8efd9211a02508f261..b76ebc4212649e3101a3c80f68f8499b21701a0f 100644 --- a/src/components/posts/PostList.jsx +++ b/src/components/posts/PostList.jsx @@ -3,7 +3,18 @@ import classNames from "classnames"; import Post from "./Post"; -const PostList = ({ className, items }) => { +const PostList = ({ className, items, onLike, onDislike, dimArchived }) => { + const onPostLike = (post) => { + return (evt) => { + onLike(post); + }; + }; + const onPostDislike = (post) => { + return (evt) => { + onDislike(post); + }; + }; + return ( <div className={classNames("space-y-px", className)}> {items @@ -20,6 +31,9 @@ const PostList = ({ className, items }) => { historyLog={item.historyLog} seen={item.seen} archived={item.archived} + onLike={onPostLike(item)} + onDislike={onPostDislike(item)} + dimIfArchived={dimArchived} /> ))} </div> diff --git a/src/containers/AnnoucementsContainer.jsx b/src/containers/AnnoucementsContainer.jsx index f75e8ff0efee520f418ef2cac24116670adbf1c5..a2937a3360051dcfa04cca0d6eb0a60836411159 100644 --- a/src/containers/AnnoucementsContainer.jsx +++ b/src/containers/AnnoucementsContainer.jsx @@ -1,59 +1,10 @@ import React from "react"; import AnnouncementList from "components/annoucements/AnnouncementList"; +import { AnnouncementStore } from "stores"; const AnnoucementsContainer = () => { - /** @type {CF2021.Announcement[]} */ - const items = [ - { - id: "1", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - seen: false, - type: "rejected-procedure-proposal", - }, - { - id: "2", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - seen: false, - type: "accepted-procedure-proposal", - }, - { - id: "3", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - seen: true, - type: "suggested-procedure-proposal", - }, - { - id: "4", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - seen: true, - type: "voting", - }, - { - id: "5", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - seen: true, - type: "announcement", - }, - { - id: "6", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - seen: true, - type: "user-ban", - }, - ]; + const items = AnnouncementStore.useState((state) => state.items); return <AnnouncementList items={items} />; }; diff --git a/src/containers/PostsContainer.jsx b/src/containers/PostsContainer.jsx index caeea1740f1456a8cff25338f746166ca8f5a058..b0166c73b3ebf791cf6d6845e9476adb1e7c4db1 100644 --- a/src/containers/PostsContainer.jsx +++ b/src/containers/PostsContainer.jsx @@ -1,224 +1,29 @@ import React from "react"; import PostList from "components/posts/PostList"; +import { PostStore } from "stores"; const PostsContainer = ({ className }) => { - /** @type {CF2021.Post[]} */ - const items = [ - { - id: "1", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "cf", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: false, - archived: false, - type: "post", - }, - { - id: "2", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "cf", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: false, - archived: false, - type: "post", - }, - { - id: "3", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "cf", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: true, - archived: false, - type: "post", - }, - { - id: "4", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "KS Pardubický kraj", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: true, - archived: false, - type: "post", - }, - { - id: "5", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "KS Pardubický kraj", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: true, - archived: false, - type: "post", - }, - { - id: "6", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "KS Pardubický kraj", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: true, - archived: true, - type: "post", - }, - { - id: "7", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "KS Pardubický kraj", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: true, - archived: true, - type: "procedure-proposal", - state: "pending", - }, - { - id: "8", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "KS Pardubický kraj", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: true, - archived: false, - type: "procedure-proposal", - state: "announced", - historyLog: [ - { - attribute: "content", - datetime: new Date(), - newValue: "Lemme know", - originator: "chairman", - }, - ], - }, - { - id: "9", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "KS Pardubický kraj", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: true, - archived: false, - type: "procedure-proposal", - state: "accepted", - }, - { - id: "10", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "KS Pardubický kraj", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: true, - archived: false, - type: "procedure-proposal", - state: "rejected", - }, - { - id: "11", - content: - "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", - datetime: new Date(), - author: { - name: "John Doe", - group: "KS Pardubický kraj", - }, - ranking: { - likes: 5, - dislikes: 1, - score: 4, - }, - seen: true, - archived: true, - type: "procedure-proposal", - state: "rejected-by-chairman", - }, - ]; + const window = PostStore.useState((state) => state.window); + const showingArchivedOnly = PostStore.useState( + (state) => state.filters.flags === "archived" + ); - return <PostList items={items} className={className} />; + const onLike = (post) => console.log("like", post); + const onDislike = (post) => console.log("dislike", post); + + const sliceStart = (window.page - 1) * window.perPage; + const sliceEnd = window.page * window.perPage; + + return ( + <PostList + items={window.items.slice(sliceStart, sliceEnd)} + onLike={onLike} + onDislike={onDislike} + className={className} + dimArchived={!showingArchivedOnly} + /> + ); }; export default PostsContainer; diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 48c1b810db89a09893ea049933e39166995af831..964f3354c9cf08537951e506165d4e91e73365c5 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,5 +1,6 @@ import React from "react"; +import PostFilters from "components/posts/PostFilters"; import AnnouncementsContainer from "containers/AnnoucementsContainer"; import PostsContainer from "containers/PostsContainer"; @@ -60,44 +61,7 @@ const Home = () => { <h2 className="head-heavy-sm whitespace-no-wrap"> Příspěvky v rozpravě </h2> - <div className="flex flex-col space-y-2 xl:space-y-0 xl:space-x-8 xl:flex-row xl:items-center"> - <div className="-mx-1"> - <span className="chip chip--grey-125 chip--select chip--hoveractive text-xs ml-1 mt-2 xl:mt-0"> - <select> - <option value="">Jen nezpracované</option> - <option value="">Vše</option> - <option value="">Jen aktivní</option> - <option value="">Jen archivované</option> - </select> - <span className="chip__icon ico--chevron-down"></span> - </span> - <span className="chip chip--grey-125 chip--select chip--hoveractive text-xs ml-1 mt-2 xl:mt-0"> - <select> - <option value="">Podle času</option> - <option value="">Podle podpory</option> - </select> - <span className="chip__icon ico--chevron-down"></span> - </span> - <span className="chip chip--grey-125 chip--select chip--hoveractive text-xs ml-1 mt-2 xl:mt-0"> - <select> - <option value="">Návrhy i příspěvky</option> - <option value="">Jen návrhy</option> - <option value="">Jen příspěvky</option> - </select> - <span className="chip__icon ico--chevron-down"></span> - </span> - </div> - - <div> - <span className="chip chip--grey-125 chip--hoveractive text-xs"> - <span className="ico--chevron-left"></span> - </span> - - <span className="chip chip--grey-125 chip--hoveractive ml-1"> - <span className="ico--chevron-right"></span> - </span> - </div> - </div> + <PostFilters /> </div> <PostsContainer className="container-padding--zero lg:container-padding--auto" /> diff --git a/src/stores.js b/src/stores.js index cda15d259c0460aa45970bd53b9f877473dc9df8..58f433b89e31c122a84d9e3f6b7d778a3fd192fa 100644 --- a/src/stores.js +++ b/src/stores.js @@ -1,5 +1,8 @@ +import memoize from "lodash/memoize"; import { Store } from "pullstate"; +import { filterPosts } from "utils"; + /** @type {CF2021.AuthStorePayload} */ const authStoreInitial = { isAuthenticated: false, @@ -10,14 +13,315 @@ export const AuthStore = new Store(authStoreInitial); /** @type {CF2021.AnnouncementStorePayload} */ const announcementStoreInitial = { - items: [], + items: [ + { + id: "1", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + seen: false, + type: "rejected-procedure-proposal", + }, + { + id: "2", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + seen: false, + type: "accepted-procedure-proposal", + }, + { + id: "3", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + seen: true, + type: "suggested-procedure-proposal", + }, + { + id: "4", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + seen: true, + type: "voting", + }, + { + id: "5", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + seen: true, + type: "announcement", + }, + { + id: "6", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + seen: true, + type: "user-ban", + }, + ], }; export const AnnouncementStore = new Store(announcementStoreInitial); +const allPosts = [ + { + id: "1", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "cf", + }, + ranking: { + likes: 0, + dislikes: 0, + score: 0, + myVote: "none", + }, + seen: false, + archived: false, + type: "post", + }, + { + id: "2", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "cf", + }, + ranking: { + likes: 1, + dislikes: 0, + score: 1, + myVote: "none", + }, + seen: false, + archived: false, + type: "post", + }, + { + id: "3", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "cf", + }, + ranking: { + likes: 5, + dislikes: 5, + score: 0, + myVote: "none", + }, + seen: true, + archived: false, + type: "post", + }, + { + id: "4", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "KS Pardubický kraj", + }, + ranking: { + likes: 0, + dislikes: 10, + score: -10, + myVote: "none", + }, + seen: true, + archived: false, + type: "post", + }, + { + id: "5", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "KS Pardubický kraj", + }, + ranking: { + likes: 1, + dislikes: 1, + score: 0, + myVote: "none", + }, + seen: true, + archived: false, + type: "post", + }, + { + id: "6", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "KS Pardubický kraj", + }, + ranking: { + likes: 5, + dislikes: 3, + score: 2, + myVote: "none", + }, + seen: true, + archived: true, + type: "post", + }, + { + id: "7", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "KS Pardubický kraj", + }, + ranking: { + likes: 5, + dislikes: 8, + score: -3, + myVote: "none", + }, + seen: true, + archived: true, + type: "procedure-proposal", + state: "pending", + }, + { + id: "8", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "KS Pardubický kraj", + }, + ranking: { + likes: 2, + dislikes: 1, + score: 1, + myVote: "like", + }, + seen: true, + archived: false, + type: "procedure-proposal", + state: "announced", + historyLog: [ + { + attribute: "content", + datetime: new Date(), + newValue: "Lemme know", + originator: "chairman", + }, + ], + }, + { + id: "9", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "KS Pardubický kraj", + }, + ranking: { + likes: 5, + dislikes: 0, + score: 5, + myVote: "dislike", + }, + seen: true, + archived: false, + type: "procedure-proposal", + state: "accepted", + }, + { + id: "10", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "KS Pardubický kraj", + }, + ranking: { + likes: 5, + dislikes: 8, + score: -3, + myVote: "none", + }, + seen: true, + archived: false, + type: "procedure-proposal", + state: "rejected", + }, + { + id: "11", + content: + "Shizz fo shizzle mah nizzle fo rizzle, mah home g-dizzle, gravida vizzle, arcu. Pellentesque crunk tortizzle. Sed erizzle. Black izzle sheezy telliv.", + datetime: new Date(), + author: { + name: "John Doe", + group: "KS Pardubický kraj", + }, + ranking: { + likes: 10, + dislikes: 1, + score: 9, + myVote: "none", + }, + seen: true, + archived: true, + type: "procedure-proposal", + state: "rejected-by-chairman", + }, +]; + +const initialPostFilters = { + flags: "all", + sort: "byDate", + type: "all", +}; + +const filteredPosts = filterPosts(initialPostFilters, allPosts); + /** @type {CF2021.PostStorePayload} */ const postStoreInitial = { - items: [], + items: allPosts, + itemCount: allPosts.length, + window: { + items: filteredPosts, + itemCount: filteredPosts.length, + page: 1, + perPage: 5, + }, + filters: initialPostFilters, }; export const PostStore = new Store(postStoreInitial); + +export const getGroupByCode = memoize( + (groupMappings, groupCode) => { + return groupMappings.find((gm) => gm.code === groupCode); + }, + (groupMappings, groupCode) => [groupMappings, groupCode] +); + +export const getGroupsByCode = memoize((groupMappings, groupCodes) => { + return groupCodes.map((code) => getGroupByCode(groupMappings, code)); +}); diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..9e9bd0904343707d1b9e75fb99fe1c166be04db2 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,27 @@ +import filter from "lodash/filter"; + +export const filterPosts = (filters, allItems) => { + const predicate = {}; + + if (filters.flags === "active") { + predicate.archived = false; + } + if (filters.flags === "archived") { + predicate.archived = true; + } + + if (filters.type === "proposalsOnly") { + predicate.type = "procedure-proposal"; + } + if (filters.type === "discussionOnly") { + predicate.type = "post"; + } + + let filteredItems = filter(allItems, predicate); + + if (filters.sort === "byDate") { + return filteredItems.sort((a, b) => b.datetime - a.datetime); + } + + return filteredItems.sort((a, b) => b.ranking.score - a.ranking.score); +}; diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts index 5df54146d88029a954d035d05bdfa30f26ca75f5..b560ff8dae57fe00d5cdc12ae24ddbbcbd77104a 100644 --- a/typings/cf2021.d.ts +++ b/typings/cf2021.d.ts @@ -29,6 +29,7 @@ declare namespace CF2021 { user: { name: string; groups: string[]; + accessToken: string; }; } @@ -73,6 +74,7 @@ declare namespace CF2021 { score: number; likes: number; dislikes: number; + myVote: "like" | "dislike" | "none"; }; historyLog: { attribute: string; @@ -104,5 +106,17 @@ declare namespace CF2021 { export interface PostStorePayload { items: Post[]; + itemCount: number; + window: { + items: Post[]; + itemCount: number; + page: number; + perPage: number; + }; + filters: { + flags: "all" | "active" | "archived"; + sort: "byDate" | "byScore"; + type: "all" | "proposalsOnly" | "discussionOnly"; + }; } }