mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-08 08:34:20 +02:00
Add custom db read support
This commit is contained in:
parent
7fb466d91c
commit
0391fdccd9
@ -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 :)
|
||||
<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">
|
@ -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,
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
45
src/store/preVolunteerCount.ts
Normal file
45
src/store/preVolunteerCount.ts
Normal 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
|
||||
}
|
@ -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...
|
||||
})
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user