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 {
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 pour les détails :)
<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>
<dd>
<div className={styles.formLine} key="line-firstname">

View File

@ -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,
}

View File

@ -50,10 +50,6 @@ const Home: FC<Props> = (): 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)

View File

@ -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<Props> = (): JSX.Element => {
function useList(
stateToProp: (state: AppState) => ValueRequest<number | undefined>,
fetchDataIfNeed: () => AppThunk
) {
const dispatch = useDispatch()
return (
<div className={styles.registerPage}>
<div className={styles.registerContent}>
<Helmet title="RegisterPage" />
<RegisterForm dispatch={dispatch} />
</div>
</div>
)
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 <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)

View File

@ -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<ElementNoId> = { id: number } & ElementNoId
@ -32,7 +32,7 @@ setInterval(
Object.values(sheetList).forEach((sheet: Sheet<object, ElementWithId<object>>) =>
sheet.dbUpdate()
),
REMOTE_SAVE_DELAY
REMOTE_UPDATE_DELAY
)
export function getSheet<
@ -51,7 +51,7 @@ export function getSheet<
return sheetList[sheetName] as Sheet<ElementNoId, Element>
}
class Sheet<
export class Sheet<
// eslint-disable-next-line @typescript-eslint/ban-types
ElementNoId extends object,
Element extends ElementWithId<ElementNoId>
@ -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
}

View File

@ -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<ElementNoId>
>(
sheetName: keyof SheetNames,
specimen: Element,
translation: { [k in keyof Element]: string }
): any {
const sheet = getSheet<ElementNoId, Element>(sheetName, specimen, translation)
> {
sheet: Sheet<ElementNoId, Element>
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 (
_request: Request,
response: Response,
_next: NextFunction
): Promise<void> => {
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<void> => {
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<void> => {
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<void> => {
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<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"
const { listGetRequest, getRequest, setRequest, addRequest } = getExpressAccessors<
JavGameWithoutId,
JavGame
>("JavGames", new JavGame(), translationJavGame)
const expressAccessor = new ExpressAccessors<JavGameWithoutId, JavGame>(
"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()

View File

@ -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<PreVolunteerWithoutId, PreVolunteer>(
"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
)

View File

@ -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<VolunteerWithoutId, Volunteer>(
"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()

View File

@ -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<WishWithoutId, Wish>(
"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()

View File

@ -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)

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">
const { listGet, get, set, add } = getServiceAccessors<PreVolunteerWithoutId, PreVolunteer>(
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()

View File

@ -41,4 +41,6 @@ export type AppThunk = ThunkAction<void, AppState, unknown, Action<string>>
export type EntitiesRequest<T> = EntityState<T> & StateRequest
export type ValueRequest<T> = { value?: T } & StateRequest
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 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...
})

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()
}
}
}