Add custom db read support

This commit is contained in:
pikiou 2021-12-11 04:34:36 +01:00
parent 7fb466d91c
commit 0391fdccd9
17 changed files with 249 additions and 86 deletions

View File

@ -9,9 +9,10 @@ import { AppDispatch, AppState } from "../../store"
interface Props { interface Props {
dispatch: AppDispatch dispatch: AppDispatch
preVolunteerCount: number | undefined
} }
const RegisterForm = ({ dispatch }: Props): JSX.Element => { const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => {
const [firstname, setFirstname] = useState("") const [firstname, setFirstname] = useState("")
const [lastname, setLastname] = useState("") const [lastname, setLastname] = useState("")
const [email, setEmail] = 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 Les prochains sont les 21 décembre et 27 janvier, mais nous vous appelerons
d'ici pour les détails :) d'ici pour les détails :)
<br /> <br />
<span className={styles.lightTitle}>(Déjà au moins 8 inscrits !)</span> {/* */}
<span className={styles.lightTitle} hidden={(preVolunteerCount || 0) < 3}>
(Déjà {preVolunteerCount} inscrits !)
</span>
</dt> </dt>
<dd> <dd>
<div className={styles.formLine} key="line-firstname"> <div className={styles.formLine} key="line-firstname">

View File

@ -5,5 +5,15 @@ import VolunteerSet from "./VolunteerSet"
import ErrorBoundary from "./ErrorBoundary" import ErrorBoundary from "./ErrorBoundary"
import Loading from "./Loading" import Loading from "./Loading"
import WishAdd from "./WishAdd" 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,
}

View File

@ -50,10 +50,6 @@ const Home: FC<Props> = (): JSX.Element => {
} }
// Fetch server-side data here // Fetch server-side data here
export const loadData = (): AppThunk[] => [ export const loadData = (): AppThunk[] => [fetchWishListIfNeed(), fetchJavGameListIfNeed()]
fetchWishListIfNeed(),
fetchJavGameListIfNeed(),
// More pre-fetched actions...
]
export default memo(Home) export default memo(Home)

View File

@ -1,22 +1,48 @@
import { FC, useEffect, memo } from "react"
import { RouteComponentProps } from "react-router-dom" import { RouteComponentProps } from "react-router-dom"
import { useDispatch } from "react-redux" import { useDispatch, useSelector, shallowEqual } from "react-redux"
import { FC, memo } from "react"
import { Helmet } from "react-helmet" 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 styles from "./styles.module.scss"
import RegisterForm from "../../components/RegisterForm/RegisterForm"
export type Props = RouteComponentProps export type Props = RouteComponentProps
const RegisterPage: FC<Props> = (): JSX.Element => { function useList(
stateToProp: (state: AppState) => ValueRequest<number | undefined>,
fetchDataIfNeed: () => AppThunk
) {
const dispatch = useDispatch() const dispatch = useDispatch()
return ( const { readyStatus, value } = useSelector(stateToProp, shallowEqual)
<div className={styles.registerPage}>
<div className={styles.registerContent}> // Fetch client-side data here
<Helmet title="RegisterPage" /> useEffect(() => {
<RegisterForm dispatch={dispatch} /> dispatch(fetchDataIfNeed())
</div> // eslint-disable-next-line react-hooks/exhaustive-deps
</div> }, [dispatch])
)
return () => {
if (!readyStatus || readyStatus === "idle" || readyStatus === "request")
return <p>Loading...</p>
if (readyStatus === "failure") return <p>Oops, Failed to load!</p>
return <RegisterForm dispatch={dispatch} preVolunteerCount={value} />
}
} }
const RegisterPage: FC<Props> = (): JSX.Element => (
<div className={styles.registerPage}>
<div className={styles.registerContent}>
<Helmet title="RegisterPage" />
{useList((state: AppState) => state.preVolunteerCount, fetchPreVolunteerCountIfNeed)()}
</div>
</div>
)
// Fetch server-side data here
export const loadData = (): AppThunk[] => [fetchPreVolunteerCountIfNeed()]
export default memo(RegisterPage) export default memo(RegisterPage)

View File

@ -8,7 +8,7 @@ import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadshee
const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json") const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json")
const REMOTE_SAVE_DELAY = 20000 const REMOTE_UPDATE_DELAY = 20000
export type ElementWithId<ElementNoId> = { id: number } & ElementNoId export type ElementWithId<ElementNoId> = { id: number } & ElementNoId
@ -32,7 +32,7 @@ setInterval(
Object.values(sheetList).forEach((sheet: Sheet<object, ElementWithId<object>>) => Object.values(sheetList).forEach((sheet: Sheet<object, ElementWithId<object>>) =>
sheet.dbUpdate() sheet.dbUpdate()
), ),
REMOTE_SAVE_DELAY REMOTE_UPDATE_DELAY
) )
export function getSheet< export function getSheet<
@ -51,7 +51,7 @@ export function getSheet<
return sheetList[sheetName] as Sheet<ElementNoId, Element> return sheetList[sheetName] as Sheet<ElementNoId, Element>
} }
class Sheet< export class Sheet<
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
ElementNoId extends object, ElementNoId extends object,
Element extends ElementWithId<ElementNoId> Element extends ElementWithId<ElementNoId>
@ -88,7 +88,7 @@ class Sheet<
return JSON.parse(JSON.stringify(this._state)) return JSON.parse(JSON.stringify(this._state))
} }
setList(newState: Element[] | undefined) { setList(newState: Element[] | undefined): void {
this._state = JSON.parse(JSON.stringify(newState)) this._state = JSON.parse(JSON.stringify(newState))
this.modifiedSinceSave = true this.modifiedSinceSave = true
} }

View File

@ -1,25 +1,29 @@
import { Request, Response, NextFunction } from "express" 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 // eslint-disable-next-line @typescript-eslint/ban-types
ElementNoId extends object, ElementNoId extends object,
Element extends ElementWithId<ElementNoId> Element extends ElementWithId<ElementNoId>
>( > {
sheetName: keyof SheetNames, sheet: Sheet<ElementNoId, Element>
specimen: Element,
translation: { [k in keyof Element]: string }
): any {
const sheet = getSheet<ElementNoId, Element>(sheetName, specimen, translation)
function listGetRequest() { constructor(
readonly sheetName: keyof SheetNames,
readonly specimen: Element,
readonly translation: { [k in keyof Element]: string }
) {
this.sheet = getSheet<ElementNoId, Element>(sheetName, specimen, translation)
}
listGet() {
return async ( return async (
_request: Request, _request: Request,
response: Response, response: Response,
_next: NextFunction _next: NextFunction
): Promise<void> => { ): Promise<void> => {
try { try {
const elements = await sheet.getList() const elements = await this.sheet.getList()
if (elements) { if (elements) {
response.status(200).json(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<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
const id = parseInt(request.query.id as string, 10) || -1 const id = parseInt(request.query.id as string, 10) || -1
const elements = await sheet.getList() const elements = await this.sheet.getList()
if (elements) { if (elements) {
const element = elements.find((e: Element) => e.id === id) const element = elements.find((e: Element) => e.id === id)
response.status(200).json(element) 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<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
sheet.add(request.body) this.sheet.add(request.body)
const elements: Element[] = (await sheet.getList()) || [] const elements: Element[] = (await this.sheet.getList()) || []
const element: Element = { id: await sheet.nextId(), ...request.body } const element: Element = { id: await this.sheet.nextId(), ...request.body }
elements.push(element) elements.push(element)
await sheet.setList(elements) await this.sheet.setList(elements)
if (element) { if (element) {
response.status(200).json(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<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
await sheet.set(request.body) await this.sheet.set(request.body)
response.status(200) response.status(200)
} catch (e: unknown) { } catch (e: unknown) {
response.status(400).json(e) 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<void> => {
try {
const elements = await this.sheet.getList()
response.status(200).json(transformer(elements))
} catch (e: unknown) {
response.status(400).json(e)
}
}
}
} }

View File

@ -1,15 +1,16 @@
import getExpressAccessors from "./expressAccessors" import ExpressAccessors from "./expressAccessors"
import { JavGame, JavGameWithoutId, translationJavGame } from "../../services/javGames" import { JavGame, JavGameWithoutId, translationJavGame } from "../../services/javGames"
const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< const expressAccessor = new ExpressAccessors<JavGameWithoutId, JavGame>(
JavGameWithoutId, "JavGames",
JavGame new JavGame(),
>("JavGames", new JavGame(), translationJavGame) 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()

View File

@ -1,19 +1,24 @@
import getExpressAccessors from "./expressAccessors" import ExpressAccessors from "./expressAccessors"
import { import {
PreVolunteer, PreVolunteer,
PreVolunteerWithoutId, PreVolunteerWithoutId,
translationPreVolunteer, translationPreVolunteer,
} from "../../services/preVolunteers" } from "../../services/preVolunteers"
const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< const expressAccessor = new ExpressAccessors<PreVolunteerWithoutId, PreVolunteer>(
PreVolunteerWithoutId, "PreVolunteers",
PreVolunteer new PreVolunteer(),
>("PreVolunteers", new PreVolunteer(), translationPreVolunteer) 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
)

View File

@ -1,15 +1,16 @@
import getExpressAccessors from "./expressAccessors" import ExpressAccessors from "./expressAccessors"
import { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers" import { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers"
const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
VolunteerWithoutId, "Volunteers",
Volunteer new Volunteer(),
>("Volunteers", new Volunteer(), translationVolunteer) 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()

View File

@ -1,15 +1,16 @@
import getExpressAccessors from "./expressAccessors" import ExpressAccessors from "./expressAccessors"
import { Wish, WishWithoutId, translationWish } from "../../services/wishes" import { Wish, WishWithoutId, translationWish } from "../../services/wishes"
const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors< const expressAccessor = new ExpressAccessors<WishWithoutId, Wish>(
WishWithoutId, "Wishes",
Wish new Wish(),
>("Wishes", new Wish(), translationWish) 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()

View File

@ -18,7 +18,7 @@ import certbotRouter from "../routes/certbot"
import { secure } from "./secure" import { secure } from "./secure"
import { javGameListGet } from "./gsheets/javGames" import { javGameListGet } from "./gsheets/javGames"
import { wishListGet, wishAdd } from "./gsheets/wishes" import { wishListGet, wishAdd } from "./gsheets/wishes"
import { preVolunteerAdd } from "./gsheets/preVolunteers" import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers"
import { volunteerGet, volunteerSet } from "./gsheets/volunteers" import { volunteerGet, volunteerSet } from "./gsheets/volunteers"
import loginHandler from "./userManagement/login" import loginHandler from "./userManagement/login"
import config from "../config" import config from "../config"
@ -58,6 +58,7 @@ app.get("/JavGameListGet", javGameListGet)
app.get("/WishListGet", wishListGet) app.get("/WishListGet", wishListGet)
app.post("/WishAdd", wishAdd) app.post("/WishAdd", wishAdd)
app.post("/PreVolunteerAdd", preVolunteerAdd) app.post("/PreVolunteerAdd", preVolunteerAdd)
app.get("/PreVolunteerCountGet", preVolunteerCountGet)
// Secured APIs // Secured APIs
app.get("/VolunteerGet", secure as RequestHandler, volunteerGet) app.get("/VolunteerGet", secure as RequestHandler, volunteerGet)

View File

@ -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<ElementCountGetResponse> => {
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 }
} }

View File

@ -30,12 +30,13 @@ const elementName = "PreVolunteer"
export type PreVolunteerWithoutId = Omit<PreVolunteer, "id"> export type PreVolunteerWithoutId = Omit<PreVolunteer, "id">
const { listGet, get, set, add } = getServiceAccessors<PreVolunteerWithoutId, PreVolunteer>( const { listGet, get, set, add, countGet } = getServiceAccessors<
elementName, PreVolunteerWithoutId,
translationPreVolunteer PreVolunteer
) >(elementName, translationPreVolunteer)
export const preVolunteerListGet = listGet() export const preVolunteerListGet = listGet()
export const preVolunteerGet = get() export const preVolunteerGet = get()
export const preVolunteerAdd = add() export const preVolunteerAdd = add()
export const preVolunteerSet = set() export const preVolunteerSet = set()
export const preVolunteerCountGet = countGet()

View File

@ -41,4 +41,6 @@ export type AppThunk = ThunkAction<void, AppState, unknown, Action<string>>
export type EntitiesRequest<T> = EntityState<T> & StateRequest export type EntitiesRequest<T> = EntityState<T> & StateRequest
export type ValueRequest<T> = { value?: T } & StateRequest
export default createStore export default createStore

View File

@ -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<number>) => {
state.readyStatus = "success"
state.value = payload
},
getFailure: (state, { payload }: PayloadAction<string>) => {
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
}

View File

@ -9,6 +9,7 @@ import volunteerAdd from "./volunteerAdd"
import volunteerList from "./volunteerList" import volunteerList from "./volunteerList"
import volunteerSet from "./volunteerSet" import volunteerSet from "./volunteerSet"
import preVolunteerAdd from "./preVolunteerAdd" import preVolunteerAdd from "./preVolunteerAdd"
import preVolunteerCount from "./preVolunteerCount"
// Use inferred return type for making correctly Redux types // Use inferred return type for making correctly Redux types
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -21,6 +22,7 @@ export default (history: History) => ({
volunteerList, volunteerList,
volunteerSet, volunteerSet,
preVolunteerAdd, preVolunteerAdd,
preVolunteerCount,
router: connectRouter(history) as any, router: connectRouter(history) as any,
// Register more reducers... // Register more reducers...
}) })

View File

@ -154,3 +154,33 @@ export function elementSet<Element>(
} }
} }
} }
export function elementValueFetch<Element>(
elementListService: () => Promise<{
data?: Element | undefined
error?: Error | undefined
}>,
getRequesting: ActionCreatorWithoutPayload<string>,
getSuccess: ActionCreatorWithPayload<Element, string>,
getFailure: ActionCreatorWithPayload<string, string>,
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()
}
}
}