diff --git a/src/components/RegisterForm/RegisterForm.tsx b/src/components/RegisterForm/index.tsx similarity index 97% rename from src/components/RegisterForm/RegisterForm.tsx rename to src/components/RegisterForm/index.tsx index b498ce6..53dbc0a 100644 --- a/src/components/RegisterForm/RegisterForm.tsx +++ b/src/components/RegisterForm/index.tsx @@ -9,9 +9,10 @@ import { AppDispatch, AppState } from "../../store" interface Props { dispatch: AppDispatch + preVolunteerCount: number | undefined } -const RegisterForm = ({ dispatch }: Props): JSX.Element => { +const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => { const [firstname, setFirstname] = useState("") const [lastname, setLastname] = useState("") const [email, setEmail] = useState("") @@ -158,7 +159,10 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => { Les prochains sont les 21 décembre et 27 janvier, mais nous vous appelerons d'ici là pour les détails :)
- (Déjà au moins 8 inscrits !) + {/* */} +
diff --git a/src/components/index.ts b/src/components/index.ts index d36abe7..114c892 100755 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,5 +5,15 @@ import VolunteerSet from "./VolunteerSet" import ErrorBoundary from "./ErrorBoundary" import Loading from "./Loading" import WishAdd from "./WishAdd" +import RegisterForm from "./RegisterForm" -export { VolunteerList, JavGameList, VolunteerInfo, VolunteerSet, ErrorBoundary, Loading, WishAdd } +export { + VolunteerList, + JavGameList, + VolunteerInfo, + VolunteerSet, + ErrorBoundary, + Loading, + WishAdd, + RegisterForm, +} diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 0b261cc..aeabd4f 100755 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -50,10 +50,6 @@ const Home: FC = (): JSX.Element => { } // Fetch server-side data here -export const loadData = (): AppThunk[] => [ - fetchWishListIfNeed(), - fetchJavGameListIfNeed(), - // More pre-fetched actions... -] +export const loadData = (): AppThunk[] => [fetchWishListIfNeed(), fetchJavGameListIfNeed()] export default memo(Home) diff --git a/src/pages/Register/RegisterPage.tsx b/src/pages/Register/RegisterPage.tsx index 539917f..f2ffd66 100644 --- a/src/pages/Register/RegisterPage.tsx +++ b/src/pages/Register/RegisterPage.tsx @@ -1,22 +1,48 @@ +import { FC, useEffect, memo } from "react" import { RouteComponentProps } from "react-router-dom" -import { useDispatch } from "react-redux" -import { FC, memo } from "react" +import { useDispatch, useSelector, shallowEqual } from "react-redux" import { Helmet } from "react-helmet" + +import { AppState, AppThunk, ValueRequest } from "../../store" +import { fetchPreVolunteerCountIfNeed } from "../../store/preVolunteerCount" +import { RegisterForm } from "../../components" import styles from "./styles.module.scss" -import RegisterForm from "../../components/RegisterForm/RegisterForm" export type Props = RouteComponentProps -const RegisterPage: FC = (): JSX.Element => { +function useList( + stateToProp: (state: AppState) => ValueRequest, + fetchDataIfNeed: () => AppThunk +) { const dispatch = useDispatch() - return ( -
-
- - -
-
- ) + const { readyStatus, value } = useSelector(stateToProp, shallowEqual) + + // Fetch client-side data here + useEffect(() => { + dispatch(fetchDataIfNeed()) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch]) + + return () => { + if (!readyStatus || readyStatus === "idle" || readyStatus === "request") + return

Loading...

+ + if (readyStatus === "failure") return

Oops, Failed to load!

+ + return + } } +const RegisterPage: FC = (): JSX.Element => ( +
+
+ + {useList((state: AppState) => state.preVolunteerCount, fetchPreVolunteerCountIfNeed)()} +
+
+) + +// Fetch server-side data here +export const loadData = (): AppThunk[] => [fetchPreVolunteerCountIfNeed()] + export default memo(RegisterPage) diff --git a/src/server/gsheets/accessors.ts b/src/server/gsheets/accessors.ts index 769d8d0..60efa5e 100644 --- a/src/server/gsheets/accessors.ts +++ b/src/server/gsheets/accessors.ts @@ -8,7 +8,7 @@ import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadshee const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json") -const REMOTE_SAVE_DELAY = 20000 +const REMOTE_UPDATE_DELAY = 20000 export type ElementWithId = { id: number } & ElementNoId @@ -32,7 +32,7 @@ setInterval( Object.values(sheetList).forEach((sheet: Sheet>) => sheet.dbUpdate() ), - REMOTE_SAVE_DELAY + REMOTE_UPDATE_DELAY ) export function getSheet< @@ -51,7 +51,7 @@ export function getSheet< return sheetList[sheetName] as Sheet } -class Sheet< +export class Sheet< // eslint-disable-next-line @typescript-eslint/ban-types ElementNoId extends object, Element extends ElementWithId @@ -88,7 +88,7 @@ class Sheet< return JSON.parse(JSON.stringify(this._state)) } - setList(newState: Element[] | undefined) { + setList(newState: Element[] | undefined): void { this._state = JSON.parse(JSON.stringify(newState)) this.modifiedSinceSave = true } diff --git a/src/server/gsheets/expressAccessors.ts b/src/server/gsheets/expressAccessors.ts index 314d8b2..6e19a08 100644 --- a/src/server/gsheets/expressAccessors.ts +++ b/src/server/gsheets/expressAccessors.ts @@ -1,25 +1,29 @@ import { Request, Response, NextFunction } from "express" -import { SheetNames, ElementWithId, getSheet } from "./accessors" +import { SheetNames, ElementWithId, getSheet, Sheet } from "./accessors" -export default function getExpressAccessors< +export default class ExpressAccessors< // eslint-disable-next-line @typescript-eslint/ban-types ElementNoId extends object, Element extends ElementWithId ->( - sheetName: keyof SheetNames, - specimen: Element, - translation: { [k in keyof Element]: string } -): any { - const sheet = getSheet(sheetName, specimen, translation) +> { + sheet: Sheet - function listGetRequest() { + constructor( + readonly sheetName: keyof SheetNames, + readonly specimen: Element, + readonly translation: { [k in keyof Element]: string } + ) { + this.sheet = getSheet(sheetName, specimen, translation) + } + + listGet() { return async ( _request: Request, response: Response, _next: NextFunction ): Promise => { try { - const elements = await sheet.getList() + const elements = await this.sheet.getList() if (elements) { response.status(200).json(elements) } @@ -29,11 +33,11 @@ export default function getExpressAccessors< } } - function getRequest() { + get() { return async (request: Request, response: Response, _next: NextFunction): Promise => { try { const id = parseInt(request.query.id as string, 10) || -1 - const elements = await sheet.getList() + const elements = await this.sheet.getList() if (elements) { const element = elements.find((e: Element) => e.id === id) response.status(200).json(element) @@ -44,14 +48,14 @@ export default function getExpressAccessors< } } - function addRequest() { + add() { return async (request: Request, response: Response, _next: NextFunction): Promise => { try { - sheet.add(request.body) - const elements: Element[] = (await sheet.getList()) || [] - const element: Element = { id: await sheet.nextId(), ...request.body } + this.sheet.add(request.body) + const elements: Element[] = (await this.sheet.getList()) || [] + const element: Element = { id: await this.sheet.nextId(), ...request.body } elements.push(element) - await sheet.setList(elements) + await this.sheet.setList(elements) if (element) { response.status(200).json(element) } @@ -61,10 +65,10 @@ export default function getExpressAccessors< } } - function setRequest() { + set() { return async (request: Request, response: Response, _next: NextFunction): Promise => { try { - await sheet.set(request.body) + await this.sheet.set(request.body) response.status(200) } catch (e: unknown) { response.status(400).json(e) @@ -72,5 +76,18 @@ export default function getExpressAccessors< } } - return { getRequest, addRequest, listGetRequest, setRequest } + customGet(transformer: (list?: Element[]) => any) { + return async ( + _request: Request, + response: Response, + _next: NextFunction + ): Promise => { + try { + const elements = await this.sheet.getList() + response.status(200).json(transformer(elements)) + } catch (e: unknown) { + response.status(400).json(e) + } + } + } } diff --git a/src/server/gsheets/javGames.ts b/src/server/gsheets/javGames.ts index fd6ff24..fc02994 100644 --- a/src/server/gsheets/javGames.ts +++ b/src/server/gsheets/javGames.ts @@ -1,15 +1,16 @@ -import getExpressAccessors from "./expressAccessors" +import ExpressAccessors from "./expressAccessors" import { JavGame, JavGameWithoutId, translationJavGame } from "../../services/javGames" -const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< - JavGameWithoutId, - JavGame ->("JavGames", new JavGame(), translationJavGame) +const expressAccessor = new ExpressAccessors( + "JavGames", + new JavGame(), + translationJavGame +) -export const javGameListGet = listGetRequest() +export const javGameListGet = expressAccessor.listGet() -export const javGameGet = getRequest() +export const javGameGet = expressAccessor.get() -export const javGameAdd = addRequest() +export const javGameAdd = expressAccessor.add() -export const javGameSet = setRequest() +export const javGameSet = expressAccessor.set() diff --git a/src/server/gsheets/preVolunteers.ts b/src/server/gsheets/preVolunteers.ts index 0969fda..036e7af 100644 --- a/src/server/gsheets/preVolunteers.ts +++ b/src/server/gsheets/preVolunteers.ts @@ -1,19 +1,24 @@ -import getExpressAccessors from "./expressAccessors" +import ExpressAccessors from "./expressAccessors" import { PreVolunteer, PreVolunteerWithoutId, translationPreVolunteer, } from "../../services/preVolunteers" -const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< - PreVolunteerWithoutId, - PreVolunteer ->("PreVolunteers", new PreVolunteer(), translationPreVolunteer) +const expressAccessor = new ExpressAccessors( + "PreVolunteers", + new PreVolunteer(), + translationPreVolunteer +) -export const preVolunteerListGet = listGetRequest() +export const preVolunteerListGet = expressAccessor.listGet() -export const preVolunteerGet = getRequest() +export const preVolunteerGet = expressAccessor.get() -export const preVolunteerAdd = addRequest() +export const preVolunteerAdd = expressAccessor.add() -export const preVolunteerSet = setRequest() +export const preVolunteerSet = expressAccessor.set() + +export const preVolunteerCountGet = expressAccessor.customGet( + (list?: PreVolunteer[]) => (list && list.length) || 0 +) diff --git a/src/server/gsheets/volunteers.ts b/src/server/gsheets/volunteers.ts index e0227cf..6043b67 100644 --- a/src/server/gsheets/volunteers.ts +++ b/src/server/gsheets/volunteers.ts @@ -1,15 +1,16 @@ -import getExpressAccessors from "./expressAccessors" +import ExpressAccessors from "./expressAccessors" import { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers" -const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< - VolunteerWithoutId, - Volunteer ->("Volunteers", new Volunteer(), translationVolunteer) +const expressAccessor = new ExpressAccessors( + "Volunteers", + new Volunteer(), + translationVolunteer +) -export const volunteerListGet = listGetRequest() +export const volunteerListGet = expressAccessor.listGet() -export const volunteerGet = getRequest() +export const volunteerGet = expressAccessor.get() -export const volunteerAdd = addRequest() +export const volunteerAdd = expressAccessor.add() -export const volunteerSet = setRequest() +export const volunteerSet = expressAccessor.set() diff --git a/src/server/gsheets/wishes.ts b/src/server/gsheets/wishes.ts index 2ca20bf..3787dcd 100644 --- a/src/server/gsheets/wishes.ts +++ b/src/server/gsheets/wishes.ts @@ -1,15 +1,16 @@ -import getExpressAccessors from "./expressAccessors" +import ExpressAccessors from "./expressAccessors" import { Wish, WishWithoutId, translationWish } from "../../services/wishes" -const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< - WishWithoutId, - Wish ->("Wishes", new Wish(), translationWish) +const expressAccessor = new ExpressAccessors( + "Wishes", + new Wish(), + translationWish +) -export const wishListGet = listGetRequest() +export const wishListGet = expressAccessor.listGet() -export const wishGet = getRequest() +export const wishGet = expressAccessor.get() -export const wishAdd = addRequest() +export const wishAdd = expressAccessor.add() -export const wishSet = setRequest() +export const wishSet = expressAccessor.set() diff --git a/src/server/index.ts b/src/server/index.ts index 2dfa449..9d79f7b 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -18,7 +18,7 @@ import certbotRouter from "../routes/certbot" import { secure } from "./secure" import { javGameListGet } from "./gsheets/javGames" import { wishListGet, wishAdd } from "./gsheets/wishes" -import { preVolunteerAdd } from "./gsheets/preVolunteers" +import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers" import { volunteerGet, volunteerSet } from "./gsheets/volunteers" import loginHandler from "./userManagement/login" import config from "../config" @@ -58,6 +58,7 @@ app.get("/JavGameListGet", javGameListGet) app.get("/WishListGet", wishListGet) app.post("/WishAdd", wishAdd) app.post("/PreVolunteerAdd", preVolunteerAdd) +app.get("/PreVolunteerCountGet", preVolunteerCountGet) // Secured APIs app.get("/VolunteerGet", secure as RequestHandler, volunteerGet) diff --git a/src/services/accessors.ts b/src/services/accessors.ts index d5e8a6c..0b9f714 100644 --- a/src/services/accessors.ts +++ b/src/services/accessors.ts @@ -147,5 +147,26 @@ export default function getServiceAccessors< } } - return { listGet, get, set, add } + function countGet(): () => Promise<{ + data?: number + error?: Error + }> { + interface ElementCountGetResponse { + data?: number + error?: Error + } + return async (): Promise => { + try { + const { data } = await axios.get( + `${config.API_URL}/${elementName}CountGet`, + axiosConfig + ) + return { data } + } catch (error) { + return { error: error as Error } + } + } + } + + return { listGet, get, set, add, countGet } } diff --git a/src/services/preVolunteers.ts b/src/services/preVolunteers.ts index 14d9cad..54b28eb 100644 --- a/src/services/preVolunteers.ts +++ b/src/services/preVolunteers.ts @@ -30,12 +30,13 @@ const elementName = "PreVolunteer" export type PreVolunteerWithoutId = Omit -const { listGet, get, set, add } = getServiceAccessors( - elementName, - translationPreVolunteer -) +const { listGet, get, set, add, countGet } = getServiceAccessors< + PreVolunteerWithoutId, + PreVolunteer +>(elementName, translationPreVolunteer) export const preVolunteerListGet = listGet() export const preVolunteerGet = get() export const preVolunteerAdd = add() export const preVolunteerSet = set() +export const preVolunteerCountGet = countGet() diff --git a/src/store/index.ts b/src/store/index.ts index c287487..191b432 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -41,4 +41,6 @@ export type AppThunk = ThunkAction> export type EntitiesRequest = EntityState & StateRequest +export type ValueRequest = { value?: T } & StateRequest + export default createStore diff --git a/src/store/preVolunteerCount.ts b/src/store/preVolunteerCount.ts new file mode 100644 index 0000000..268afd4 --- /dev/null +++ b/src/store/preVolunteerCount.ts @@ -0,0 +1,45 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit" + +import { StateRequest, toastError, elementValueFetch } from "./utils" +import { preVolunteerCountGet } from "../services/preVolunteers" +import { AppThunk, AppState } from "." + +export const initialState: StateRequest & { value?: number } = { readyStatus: "idle" } + +const preVolunteerCount = createSlice({ + name: "preVolunteerCount", + initialState, + reducers: { + getRequesting: (state) => { + state.readyStatus = "request" + }, + getSuccess: (state, { payload }: PayloadAction) => { + state.readyStatus = "success" + state.value = payload + }, + getFailure: (state, { payload }: PayloadAction) => { + state.readyStatus = "failure" + state.error = payload + }, + }, +}) + +export default preVolunteerCount.reducer +export const { getRequesting, getSuccess, getFailure } = preVolunteerCount.actions + +export const fetchPreVolunteerCount = elementValueFetch( + preVolunteerCountGet, + getRequesting, + getSuccess, + getFailure, + (error: Error) => toastError(`Erreur lors du chargement des volunteers: ${error.message}`) +) + +const shouldFetchPreVolunteerCount = (state: AppState) => + state.preVolunteerCount.readyStatus !== "success" + +export const fetchPreVolunteerCountIfNeed = (): AppThunk => (dispatch, getState) => { + if (shouldFetchPreVolunteerCount(getState())) return dispatch(fetchPreVolunteerCount()) + + return null +} diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 2c0a3f5..c8e70bc 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -9,6 +9,7 @@ import volunteerAdd from "./volunteerAdd" import volunteerList from "./volunteerList" import volunteerSet from "./volunteerSet" import preVolunteerAdd from "./preVolunteerAdd" +import preVolunteerCount from "./preVolunteerCount" // Use inferred return type for making correctly Redux types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -21,6 +22,7 @@ export default (history: History) => ({ volunteerList, volunteerSet, preVolunteerAdd, + preVolunteerCount, router: connectRouter(history) as any, // Register more reducers... }) diff --git a/src/store/utils.ts b/src/store/utils.ts index a93b24c..0db504d 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -154,3 +154,33 @@ export function elementSet( } } } + +export function elementValueFetch( + elementListService: () => Promise<{ + data?: Element | undefined + error?: Error | undefined + }>, + getRequesting: ActionCreatorWithoutPayload, + getSuccess: ActionCreatorWithPayload, + getFailure: ActionCreatorWithPayload, + errorMessage: (error: Error) => void = (_error) => { + /* Meant to be empty */ + }, + successMessage: () => void = () => { + /* Meant to be empty */ + } +): () => AppThunk { + return (): AppThunk => async (dispatch) => { + dispatch(getRequesting()) + + const { error, data } = await elementListService() + + if (error) { + dispatch(getFailure(error.message)) + errorMessage(error) + } else { + dispatch(getSuccess(data as Element)) + successMessage() + } + } +}