From 998691b54297315904cadf22852e7ab11368259f Mon Sep 17 00:00:00 2001 From: pikiou Date: Mon, 7 Feb 2022 17:21:22 +0100 Subject: [PATCH] Add announcements page, i.e gazettes and comptes rendus --- .eslintignore | 1 + .gitignore | 6 +- .prettierignore | 2 +- src/components/AnnouncementLink/index.tsx | 66 ++++++++++++++++++ .../AnnouncementLink/styles.module.scss | 16 +++++ src/components/index.ts | 2 + src/pages/Announcements/Announcements.tsx | 69 +++++++++++++++++++ src/pages/Announcements/index.tsx | 16 +++++ src/pages/Announcements/styles.module.scss | 21 ++++++ src/routes/index.ts | 6 ++ src/server/gsheets/announcements.ts | 17 +++++ src/server/gsheets/localDb.ts | 2 + src/server/index.ts | 2 + src/services/accessors.ts | 26 +++++++ src/services/announcement.ts | 23 +++++++ src/services/announcementAccessors.ts | 10 +++ src/store/announcementList.ts | 51 ++++++++++++++ src/store/rootReducer.ts | 2 + src/store/utils.ts | 27 ++++---- 19 files changed, 347 insertions(+), 18 deletions(-) create mode 100644 src/components/AnnouncementLink/index.tsx create mode 100755 src/components/AnnouncementLink/styles.module.scss create mode 100644 src/pages/Announcements/Announcements.tsx create mode 100755 src/pages/Announcements/index.tsx create mode 100755 src/pages/Announcements/styles.module.scss create mode 100644 src/server/gsheets/announcements.ts create mode 100644 src/services/announcement.ts create mode 100644 src/services/announcementAccessors.ts create mode 100644 src/store/announcementList.ts diff --git a/.eslintignore b/.eslintignore index a48cf0d..157c89f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ public +announces/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index db28de7..73a4726 100644 --- a/.gitignore +++ b/.gitignore @@ -14,10 +14,8 @@ public/* # Access access/* -# Gazettes -gazettes/* - # Misc .DS_Store *.log -.idea \ No newline at end of file +.idea +announces/* diff --git a/.prettierignore b/.prettierignore index 0c69b25..d5651d6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,4 +9,4 @@ public/* *.log node_modules/* access/* -gazettes/* +announces/* diff --git a/src/components/AnnouncementLink/index.tsx b/src/components/AnnouncementLink/index.tsx new file mode 100644 index 0000000..d2846e5 --- /dev/null +++ b/src/components/AnnouncementLink/index.tsx @@ -0,0 +1,66 @@ +import { memo } from "react" +import { Announcement } from "../../services/announcement" +import styles from "./styles.module.scss" + +interface Props { + // eslint-disable-next-line react/require-default-props + announcement: Announcement +} + +const AnnouncementLink = ({ announcement }: Props): JSX.Element | null => { + const { id, type, title, url } = announcement + const icon = { + gazette: ( + + ), + + "compte rendu": ( + + ), + }[type] + + const typeName = { + gazette: "Gazzette - ", + "compte rendu": "Compte rendu - ", + }[type] + + return ( +
+ {icon} {typeName} + {title} +
+ ) +} + +export default memo(AnnouncementLink) diff --git a/src/components/AnnouncementLink/styles.module.scss b/src/components/AnnouncementLink/styles.module.scss new file mode 100755 index 0000000..39840ac --- /dev/null +++ b/src/components/AnnouncementLink/styles.module.scss @@ -0,0 +1,16 @@ +@import "../../theme/variables"; +@import "../../theme/mixins"; + +.announcementLink { + margin: 0.9em; +} + +.gazette { + margin-left: 0.22em; + margin-right: 0.5em; + vertical-align: middle; +} +.report { + margin-right: 0.4em; + vertical-align: middle; +} diff --git a/src/components/index.ts b/src/components/index.ts index e38540b..1ec55a0 100755 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,4 @@ +import AnnouncementLink from "./AnnouncementLink" import LoginForm from "./LoginForm" import Notifications from "./Notifications" import VolunteerList from "./VolunteerList" @@ -10,6 +11,7 @@ import WishAdd from "./WishAdd" import PreRegisterForm from "./PreRegisterForm" export { + AnnouncementLink, LoginForm, Notifications, VolunteerList, diff --git a/src/pages/Announcements/Announcements.tsx b/src/pages/Announcements/Announcements.tsx new file mode 100644 index 0000000..f3db4cb --- /dev/null +++ b/src/pages/Announcements/Announcements.tsx @@ -0,0 +1,69 @@ +import { FC, memo } from "react" +import * as _ from "lodash" +import { RouteComponentProps, Link } from "react-router-dom" +import { useSelector, shallowEqual } from "react-redux" +import { Helmet } from "react-helmet" +import { EntityState } from "@reduxjs/toolkit" + +import { AppState, AppThunk } from "../../store" +import { AnnouncementLink, LoginForm } from "../../components" +import styles from "./styles.module.scss" +import { fetchAnnouncementListIfNeed } from "../../store/announcementList" +import { Announcement } from "../../services/announcement" +import { selectUserJwtToken } from "../../store/auth" + +export type Props = RouteComponentProps + +let prevAnnouncements: EntityState | undefined + +const AnnouncementsPage: FC = (): JSX.Element => { + const jwtToken = useSelector(selectUserJwtToken) + + const announcementList = useSelector((state: AppState) => { + if (!state.announcementList) { + return undefined + } + const announcements = _.pick(state.announcementList, "entities", "ids") + if (announcements) { + prevAnnouncements = announcements + return announcements + } + return prevAnnouncements + }, shallowEqual) + + if (jwtToken === undefined) return

Loading...

+ + if (jwtToken && announcementList) { + const list = _.orderBy(announcementList.ids, _.identity, "desc") + const listElements = list.map((id) => { + const announcement = announcementList.entities[id] as Announcement + return announcement && + }) + + return ( +
+
{listElements}
+
+ ) + } + return ( +
+
+
+ + +
+
+
+
+ S'informer sur le bénévolat +
+
+
+ ) +} + +// Fetch server-side data here +export const loadData = (): AppThunk[] => [fetchAnnouncementListIfNeed()] + +export default memo(AnnouncementsPage) diff --git a/src/pages/Announcements/index.tsx b/src/pages/Announcements/index.tsx new file mode 100755 index 0000000..e31f516 --- /dev/null +++ b/src/pages/Announcements/index.tsx @@ -0,0 +1,16 @@ +import loadable from "@loadable/component" + +import { Loading, ErrorBoundary } from "../../components" +import { Props, loadData } from "./Announcements" + +const Announcements = loadable(() => import("./Announcements"), { + fallback: , +}) + +export default (props: Props): JSX.Element => ( + + + +) + +export { loadData } diff --git a/src/pages/Announcements/styles.module.scss b/src/pages/Announcements/styles.module.scss new file mode 100755 index 0000000..6a061a9 --- /dev/null +++ b/src/pages/Announcements/styles.module.scss @@ -0,0 +1,21 @@ +@import "../../theme/mixins"; + +.announcements { + @include page-wrapper-center; +} + +.announcementsContent { + @include page-content-wrapper; + + text-align: left; +} + +.loginContent { + @include page-content-wrapper; +} + +.navigationLink { + @include page-content-wrapper; + + text-align: center; +} diff --git a/src/routes/index.ts b/src/routes/index.ts index 9ec7afb..b1b6d5d 100755 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -2,6 +2,7 @@ import { RouteConfig } from "react-router-config" import App from "../app" import AsyncHome, { loadData as loadHomeData } from "../pages/Home" +import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements" import AsyncPreRegisterPage, { loadData as loadPreRegisterPage } from "../pages/PreRegister" import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams" import AsyncBoard, { loadData as loadBoardData } from "../pages/Board" @@ -66,6 +67,11 @@ export default [ component: AsyncBoard, loadData: loadBoardData, }, + { + path: "/annonces", + component: AsyncAnnouncements, + loadData: loadAnnouncementsData, + }, { component: NotFound, }, diff --git a/src/server/gsheets/announcements.ts b/src/server/gsheets/announcements.ts new file mode 100644 index 0000000..65d4fd3 --- /dev/null +++ b/src/server/gsheets/announcements.ts @@ -0,0 +1,17 @@ +import ExpressAccessors from "./expressAccessors" +import { + Announcement, + AnnouncementWithoutId, + translationAnnouncement, +} from "../../services/announcement" + +const expressAccessor = new ExpressAccessors( + "Announcements", + new Announcement(), + translationAnnouncement +) + +export const announcementListGet = expressAccessor.listGet() +// export const announcementGet = expressAccessor.get() +// export const announcementAdd = expressAccessor.add() +// export const announcementSet = expressAccessor.set() diff --git a/src/server/gsheets/localDb.ts b/src/server/gsheets/localDb.ts index a2e43ff..ff703c1 100644 --- a/src/server/gsheets/localDb.ts +++ b/src/server/gsheets/localDb.ts @@ -10,6 +10,8 @@ const DB_TO_LOAD_PATH = path.resolve(process.cwd(), "access/dbToLoad.json") const ANONYMIZED_DB_PATH = path.resolve(process.cwd(), "access/dbAnonymized.json") export class SheetNames { + Announcements = "Annonces" + JavGames = "Jeux JAV" PreVolunteers = "PreMembres" diff --git a/src/server/index.ts b/src/server/index.ts index 0b58b3c..085f7c2 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -17,6 +17,7 @@ import ssr from "./ssr" import certbotRouter from "../routes/certbot" import { hasSecret, secure } from "./secure" +import { announcementListGet } from "./gsheets/announcements" import { javGameListGet } from "./gsheets/javGames" import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers" import { teamListGet } from "./gsheets/teams" @@ -76,6 +77,7 @@ app.post("/VolunteerLogin", volunteerLogin) app.post("/VolunteerForgot", volunteerForgot) // Secured APIs +app.get("/AnnouncementListGet", secure as RequestHandler, announcementListGet) app.post("/VolunteerSet", secure as RequestHandler, volunteerSet) app.get("/TeamListGet", teamListGet) // UNSAFE app.post("/VolunteerGet", secure as RequestHandler, volunteerGet) diff --git a/src/services/accessors.ts b/src/services/accessors.ts index d84b151..d256ae7 100644 --- a/src/services/accessors.ts +++ b/src/services/accessors.ts @@ -58,6 +58,32 @@ export default class ServiceAccessors< } } + secureListGet(): (jwt: string) => Promise<{ + data?: Element[] + error?: Error + }> { + interface ElementListGetResponse { + data?: Element[] + error?: Error + } + return async (jwt: string): Promise => { + try { + const auth = { headers: { Authorization: `Bearer ${jwt}` } } + const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig) + const { data } = await axios.get( + `${config.API_URL}/${this.elementName}ListGet`, + fullAxiosConfig + ) + if (data.error) { + throw Error(data.error) + } + return { data } + } catch (error) { + return { error: error as Error } + } + } + } + // eslint-disable-next-line @typescript-eslint/ban-types add(): (volunteerWithoutId: ElementNoId) => Promise<{ data?: Element diff --git a/src/services/announcement.ts b/src/services/announcement.ts new file mode 100644 index 0000000..f3ebb54 --- /dev/null +++ b/src/services/announcement.ts @@ -0,0 +1,23 @@ +export class Announcement { + id = 0 + + created = new Date() + + type = "" + + title = "" + + url = "" +} + +export const translationAnnouncement: { [k in keyof Announcement]: string } = { + id: "id", + created: "creation", + type: "type", + title: "titre", + url: "url", +} + +export const elementName = "Announcement" + +export type AnnouncementWithoutId = Omit diff --git a/src/services/announcementAccessors.ts b/src/services/announcementAccessors.ts new file mode 100644 index 0000000..460cf42 --- /dev/null +++ b/src/services/announcementAccessors.ts @@ -0,0 +1,10 @@ +import ServiceAccessors from "./accessors" +import { elementName, Announcement, AnnouncementWithoutId } from "./announcement" + +const serviceAccessors = new ServiceAccessors(elementName) + +// export const announcementGet = serviceAccessors.get() +// export const announcementAdd = serviceAccessors.add() +// export const announcementSet = serviceAccessors.set() + +export const announcementListGet = serviceAccessors.secureListGet() diff --git a/src/store/announcementList.ts b/src/store/announcementList.ts new file mode 100644 index 0000000..01abca8 --- /dev/null +++ b/src/store/announcementList.ts @@ -0,0 +1,51 @@ +import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit" + +import { StateRequest, toastError, elementListFetch } from "./utils" +import { Announcement } from "../services/announcement" +import { AppThunk, AppState } from "." +import { announcementListGet } from "../services/announcementAccessors" + +const announcementAdapter = createEntityAdapter() + +export const initialState = announcementAdapter.getInitialState({ + readyStatus: "idle", +} as StateRequest) + +const announcementList = createSlice({ + name: "announcementList", + initialState, + reducers: { + getRequesting: (state) => { + state.readyStatus = "request" + }, + getSuccess: (state, { payload }: PayloadAction) => { + state.readyStatus = "success" + announcementAdapter.setAll(state, payload) + }, + getFailure: (state, { payload }: PayloadAction) => { + state.readyStatus = "failure" + state.error = payload + }, + }, +}) + +export default announcementList.reducer +export const { getRequesting, getSuccess, getFailure } = announcementList.actions + +export const fetchAnnouncementList = elementListFetch( + announcementListGet, + getRequesting, + getSuccess, + getFailure, + (error: Error) => toastError(`Erreur lors du chargement des announcements: ${error.message}`) +) + +const shouldFetchAnnouncementList = (state: AppState) => + state.announcementList.readyStatus !== "success" + +export const fetchAnnouncementListIfNeed = (): AppThunk => (dispatch, getState) => { + const { jwt } = getState().auth + if (shouldFetchAnnouncementList(getState())) return dispatch(fetchAnnouncementList(jwt)) + + return null +} diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index beb3aa8..b30ac58 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -3,6 +3,7 @@ import { connectRouter } from "connected-react-router" import auth from "./auth" import javGameList from "./javGameList" +import announcementList from "./announcementList" import preVolunteerAdd from "./preVolunteerAdd" import preVolunteerCount from "./preVolunteerCount" import teamList from "./teamList" @@ -25,6 +26,7 @@ import wishList from "./wishList" export default (history: History) => ({ auth, javGameList, + announcementList, preVolunteerAdd, preVolunteerCount, teamList, diff --git a/src/store/utils.ts b/src/store/utils.ts index ed19b4f..fa28b9c 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -91,8 +91,8 @@ export function elementAddFetch( } } -export function elementListFetch( - elementListService: () => Promise<{ +export function elementListFetch>( + elementListService: (...idArgs: ServiceInput) => Promise<{ data?: Element[] | undefined error?: Error | undefined }>, @@ -101,20 +101,21 @@ export function elementListFetch( getFailure: ActionCreatorWithPayload, errorMessage?: (error: Error) => void, successMessage?: () => void -): () => AppThunk { - return (): AppThunk => async (dispatch) => { - dispatch(getRequesting()) +): (...idArgs: ServiceInput) => AppThunk { + return (...idArgs: ServiceInput): AppThunk => + async (dispatch) => { + dispatch(getRequesting()) - const { error, data } = await elementListService() + const { error, data } = await elementListService(...idArgs) - if (error) { - dispatch(getFailure(error.message)) - errorMessage?.(error) - } else { - dispatch(getSuccess(data as Element[])) - successMessage?.() + if (error) { + dispatch(getFailure(error.message)) + errorMessage?.(error) + } else { + dispatch(getSuccess(data as Element[])) + successMessage?.() + } } - } } export function elementSet(