Add register form, missing proper feedback on error or success

This commit is contained in:
pikiou 2022-03-17 00:22:34 +01:00
parent cef7c5f7b0
commit 7fc3ec08ba
26 changed files with 350 additions and 260 deletions

View File

@ -4,15 +4,16 @@ import { toast } from "react-toastify"
import _ from "lodash" import _ from "lodash"
import styles from "./styles.module.scss" import styles from "./styles.module.scss"
import { fetchPreVolunteerAdd } from "../../store/preVolunteerAdd" import { fetchPostulantAdd } from "../../store/postulantAdd"
import { AppDispatch, AppState } from "../../store" import { AppDispatch, AppState } from "../../store"
import { fetchVolunteerPartialAdd } from "../../store/volunteerPartialAdd"
interface Props { interface Props {
dispatch: AppDispatch dispatch: AppDispatch
preVolunteerCount: number | undefined
} }
const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => { const RegisterForm = ({ dispatch }: Props): JSX.Element => {
const [potentialVolunteer, setPotentialVolunteer] = useState(true)
const [firstname, setFirstname] = useState("") const [firstname, setFirstname] = useState("")
const [lastname, setLastname] = useState("") const [lastname, setLastname] = useState("")
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
@ -21,31 +22,60 @@ const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element =>
const [comment, setComment] = useState("") const [comment, setComment] = useState("")
const [sending, setSending] = useState(false) const [sending, setSending] = useState(false)
const onNewVolunteer = (e: React.ChangeEvent<HTMLInputElement>) =>
setPotentialVolunteer(!e.target.value)
const onPotentialVolunteer = (e: React.ChangeEvent<HTMLInputElement>) =>
setPotentialVolunteer(!!e.target.value)
const onFirstnameChanged = (e: React.ChangeEvent<HTMLInputElement>) => const onFirstnameChanged = (e: React.ChangeEvent<HTMLInputElement>) =>
setFirstname(e.target.value) setFirstname(e.target.value)
const onLastnameChanged = (e: React.ChangeEvent<HTMLInputElement>) => const onLastnameChanged = (e: React.ChangeEvent<HTMLInputElement>) =>
setLastname(e.target.value) setLastname(e.target.value)
const onEmailChanged = (e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value) const onEmailChanged = (e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)
const onMobileChanged = (e: React.ChangeEvent<HTMLInputElement>) => setMobile(e.target.value) const onMobileChanged = (e: React.ChangeEvent<HTMLInputElement>) => setMobile(e.target.value)
const onAlreadyVolunteer = (e: React.ChangeEvent<HTMLInputElement>) => const onAlreadyVolunteer = (e: React.ChangeEvent<HTMLInputElement>) =>
setAlreadyVolunteer(!!e.target.value) setAlreadyVolunteer(!!e.target.value)
const onNotYesVolunteer = (e: React.ChangeEvent<HTMLInputElement>) => const onNotYesVolunteer = (e: React.ChangeEvent<HTMLInputElement>) =>
setAlreadyVolunteer(!e.target.value) setAlreadyVolunteer(!e.target.value)
const onCommentChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => const onCommentChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) =>
setComment(e.target.value) setComment(e.target.value)
const onSubmit = () => { const onSubmit = () => {
if (firstname && lastname && email && mobile && !sending) { if (firstname && lastname && email && mobile && !sending) {
if (potentialVolunteer) {
dispatch( dispatch(
fetchPreVolunteerAdd({ fetchPostulantAdd({
firstname, firstname,
lastname, lastname,
email, email,
mobile, mobile,
alreadyVolunteer, potential: true,
comment, comment,
}) })
) )
} else {
dispatch(
fetchPostulantAdd({
firstname,
lastname,
email,
mobile,
potential: false,
comment,
})
)
dispatch(
fetchVolunteerPartialAdd({
firstname,
lastname,
email,
mobile,
})
)
}
setSending(true) setSending(true)
} else { } else {
@ -61,13 +91,13 @@ const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element =>
} }
} }
const { error, entities: preVolunteer } = useSelector( const { error, entities: postulant } = useSelector(
(state: AppState) => state.preVolunteerAdd, (state: AppState) => state.postulantAdd,
shallowEqual shallowEqual
) )
let sendSuccess let sendSuccess
if (!_.isEmpty(preVolunteer)) { if (!_.isEmpty(postulant)) {
if (sending) { if (sending) {
setSending(false) setSending(false)
} }
@ -75,7 +105,7 @@ const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element =>
} }
let sendError let sendError
if (error && _.isEmpty(preVolunteer)) { if (error && _.isEmpty(postulant)) {
if (sending) { if (sending) {
setSending(false) setSending(false)
} }
@ -140,31 +170,53 @@ const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element =>
confort avant tout ! confort avant tout !
</p> </p>
<p> <p>
Certains bénévoles sont visiteurs le samedi ou le dimanche pour vivre le La majorité d&apos;entre nous sommes bénévoles les <b>samedi et dimanche</b>
festival de l&apos;intérieur. Les deux jours avant et le jour après le , mais certains bénévoles ne sont pas disponibles les deux jours. On leur
festival, ceux qui le peuvent viennent préparer et ranger. Bref, chacun demande alors d&apos;aider à la mise en place jeudi ou vendredi, ou au
participe à la hauteur de ses wishes et disponibilités ! rangement le lundi. Bref, chacun participe comme il peut mais deux jours
minimum !
</p> </p>
<p> <p>
Le samedi soir quand les visiteurs sont partis, nous prolongeons la fête en Le samedi soir quand les visiteurs sont partis, nous prolongeons la fête en
dînant avec les auteurs, illustrateurs et éditeurs présents sur le festival. dînant avec les exposants présents sur le festival.
</p> </p>
</dd> </dd>
<dt> <dt>
Si l&apos;expérience pourrait vous tenter, remplissez le formulaire suivant pour Si l&apos;expérience vous tente, remplissez le formulaire suivant pour devenir
en discuter lors d&apos;un des gros apéros mensuels !<br /> bénévoles !<br />
Cette inscription ne vous oblige en rien il s&apos;agit juste d&apos;une prise Vous pouvez aussi juste nous rencontrer avant de vous décider à devenir
de contact. bénévole, on comprend qu&apos;un saut dans l&apos;inconnu soit difficile.
<br /> <br />
Les prochains sont les 21 décembre et 27 janvier, mais nous vous appelerons Dans les deux cas, venez nous rencontrer mardi 22 mars près de Châtelet, détails
d&apos;ici pour les détails :) après l&apos;inscription :)
<br /> <br />
{/* */}
<span className={styles.lightTitle} hidden={(preVolunteerCount || 0) < 3}>
(Déjà {preVolunteerCount} inscrits !)
</span>
</dt> </dt>
<dd> <dd>
<div className={styles.formLine} key="line-potential-volunteer">
<div>
Je veux devenir bénévole
<input
type="radio"
name="potentialVolunteer"
id="potentialVolunteer-yes"
className={styles.inputRadio}
checked={!potentialVolunteer}
onChange={onNewVolunteer}
/>
<label htmlFor="potentialVolunteer-yes">Tout de suite !</label>
<input
type="radio"
name="potentialVolunteer"
id="potentialVolunteer-no"
className={styles.inputRadio}
checked={potentialVolunteer}
onChange={onPotentialVolunteer}
/>
<label htmlFor="potentialVolunteer-no">
Peut-être après une rencontre avec des bénévoles
</label>
</div>
</div>
<div className={styles.formLine} key="line-firstname"> <div className={styles.formLine} key="line-firstname">
<label htmlFor="firstname">Prénom</label> <label htmlFor="firstname">Prénom</label>
<input <input
@ -232,7 +284,7 @@ const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element =>
<textarea <textarea
name="message" name="message"
id="message" id="message"
placeholder="Des petits mots sympas, questions, wishes, des infos sur toi, des compétences dont tu aimerais te servir... ou rien de tout ça et nous en discuterons au téléphone :)" placeholder="Dis-nous ici comment tu as connu le festival, ce qui te motive à nous rejoindre, quelles compétences tu aimerais développer ou utiliser... ou tu n'y as pas trop réfléchi et tu trouveras en discutant avec nous :)"
value={comment} value={comment}
onChange={onCommentChanged} onChange={onCommentChanged}
/> />
@ -253,4 +305,4 @@ const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element =>
) )
} }
export default memo(PreRegisterForm) export default memo(RegisterForm)

View File

@ -11,7 +11,7 @@ import Asks, { fetchFor as fetchForAsks } from "./Asks"
import ParticipationDetailsForm, { import ParticipationDetailsForm, {
fetchFor as fetchForParticipationDetailsForm, fetchFor as fetchForParticipationDetailsForm,
} from "./VolunteerBoard/ParticipationDetailsForm/ParticipationDetailsForm" } from "./VolunteerBoard/ParticipationDetailsForm/ParticipationDetailsForm"
import PreRegisterForm from "./PreRegisterForm" import RegisterForm from "./RegisterForm"
import TeamWishesForm, { import TeamWishesForm, {
fetchFor as fetchForTeamWishesForm, fetchFor as fetchForTeamWishesForm,
} from "./VolunteerBoard/TeamWishesForm/TeamWishesForm" } from "./VolunteerBoard/TeamWishesForm/TeamWishesForm"
@ -34,7 +34,7 @@ export {
fetchForAsks, fetchForAsks,
ParticipationDetailsForm, ParticipationDetailsForm,
fetchForParticipationDetailsForm, fetchForParticipationDetailsForm,
PreRegisterForm, RegisterForm,
TeamWishesForm, TeamWishesForm,
fetchForTeamWishesForm, fetchForTeamWishesForm,
VolunteerInfo, VolunteerInfo,

View File

@ -1,48 +0,0 @@
import { FC, useEffect, memo } from "react"
import { RouteComponentProps } from "react-router-dom"
import { useDispatch, useSelector, shallowEqual } from "react-redux"
import { Helmet } from "react-helmet"
import { AppState, AppThunk, ValueRequest } from "../../store"
import { fetchPreVolunteerCountIfNeed } from "../../store/preVolunteerCount"
import { PreRegisterForm } from "../../components"
import styles from "./styles.module.scss"
export type Props = RouteComponentProps
function useList(
stateToProp: (state: AppState) => ValueRequest<number | undefined>,
fetchDataIfNeed: () => AppThunk
) {
const dispatch = useDispatch()
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 <PreRegisterForm dispatch={dispatch} preVolunteerCount={value} />
}
}
const PreRegisterPage: FC<Props> = (): JSX.Element => (
<div className={styles.preRegisterPage}>
<div className={styles.preRegisterContent}>
<Helmet title="PreRegisterPage" />
{useList((state: AppState) => state.preVolunteerCount, fetchPreVolunteerCountIfNeed)()}
</div>
</div>
)
// Fetch server-side data here
export const loadData = (): AppThunk[] => [fetchPreVolunteerCountIfNeed()]
export default memo(PreRegisterPage)

View File

@ -0,0 +1,27 @@
import { FC, memo } from "react"
import { RouteComponentProps } from "react-router-dom"
import { useDispatch } from "react-redux"
import { Helmet } from "react-helmet"
import { AppThunk } from "../../store"
import { RegisterForm } from "../../components"
import styles from "./styles.module.scss"
export type Props = RouteComponentProps
const RegisterPage: FC<Props> = (): JSX.Element => {
const dispatch = useDispatch()
return (
<div className={styles.registerPage}>
<div className={styles.registerContent}>
<Helmet title="RegisterPage" />
<RegisterForm dispatch={dispatch} />
</div>
</div>
)
}
// Fetch server-side data here
export const loadData = (): AppThunk[] => []
export default memo(RegisterPage)

View File

@ -1,15 +1,15 @@
import loadable from "@loadable/component" import loadable from "@loadable/component"
import { Loading, ErrorBoundary } from "../../components" import { Loading, ErrorBoundary } from "../../components"
import { Props, loadData } from "./PreRegister" import { Props, loadData } from "./Register"
const PreRegister = loadable(() => import("./PreRegister"), { const Register = loadable(() => import("./Register"), {
fallback: <Loading />, fallback: <Loading />,
}) })
export default (props: Props): JSX.Element => ( export default (props: Props): JSX.Element => (
<ErrorBoundary> <ErrorBoundary>
<PreRegister {...props} /> <Register {...props} />
</ErrorBoundary> </ErrorBoundary>
) )

View File

@ -1,9 +1,9 @@
@import "../../theme/mixins"; @import "../../theme/mixins";
.preRegisterPage { .registerPage {
@include page-wrapper-center; @include page-wrapper-center;
} }
.preRegisterContent { .registerContent {
@include page-content-wrapper(600px); @include page-content-wrapper(600px);
} }

View File

@ -3,7 +3,7 @@ import { RouteConfig } from "react-router-config"
import App from "../app" import App from "../app"
import AsyncHome, { loadData as loadHomeData } from "../pages/Home" import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements" import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements"
import AsyncPreRegisterPage, { loadData as loadPreRegisterPage } from "../pages/PreRegister" import AsyncRegisterPage, { loadData as loadRegisterPage } from "../pages/Register"
import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams" import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board" import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers" import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
@ -25,13 +25,13 @@ export default [
}, },
{ {
path: "/preRegister", path: "/preRegister",
component: AsyncPreRegisterPage, component: AsyncRegisterPage,
loadData: loadPreRegisterPage, loadData: loadRegisterPage,
}, },
{ {
path: "/sinscrire", path: "/sinscrire",
component: AsyncPreRegisterPage, component: AsyncRegisterPage,
loadData: loadPreRegisterPage, loadData: loadRegisterPage,
}, },
{ {
path: "/VolunteerPage/:id", path: "/VolunteerPage/:id",

View File

@ -7,7 +7,7 @@ import { SheetNames, saveLocalDb, loadLocalDb } from "./localDb"
export { SheetNames } from "./localDb" export { SheetNames } from "./localDb"
// Test write attack with: wget --header='Content-Type:application/json' --post-data='{"prenom":"Pierre","nom":"SCELLES","email":"test@gmail.com","telephone":"0601010101","dejaBenevole":false,"commentaire":""}' http://localhost:3000/PreVolunteerAdd // Test write attack with: wget --header='Content-Type:application/json' --post-data='{"prenom":"Pierre","nom":"SCELLES","email":"test@gmail.com","telephone":"0601010101","dejaBenevole":false,"commentaire":""}' http://localhost:3000/PostulantAdd
const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json") const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json")
@ -122,7 +122,7 @@ export class Sheet<
return (_.max(ids) || 0) + 1 return (_.max(ids) || 0) + 1
} }
async add(elementWithoutId: ElementNoId): Promise<Element> { async add(elementWithoutId: Omit<Element, "id">): Promise<Element> {
const elements: Element[] = (await this.getList()) || [] const elements: Element[] = (await this.getList()) || []
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
const element: Element = { id: await this.nextId(), ...elementWithoutId } as Element const element: Element = { id: await this.nextId(), ...elementWithoutId } as Element

View File

@ -3,6 +3,7 @@ import { SheetNames, ElementWithId, getSheet, Sheet } from "./accessors"
export type RequestBody = Request["body"] export type RequestBody = Request["body"]
export type CustomSetReturn<Element> = { toDatabase: Element; toCaller: any } export type CustomSetReturn<Element> = { toDatabase: Element; toCaller: any }
export type CustomAddReturn<Element> = { toDatabase: Omit<Element, "id">; toCaller: any }
export default class ExpressAccessors< export default class ExpressAccessors<
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
@ -88,13 +89,44 @@ export default class ExpressAccessors<
} }
} }
add() { add(
custom?: (
list: Element[],
body: RequestBody,
id: number,
roles: string[]
) => Promise<CustomAddReturn<Element>> | CustomAddReturn<Element>
) {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
const sheet = await this.getSheet() const sheet = await this.getSheet()
const element: Element = await sheet.add(request.body) if (!custom) {
if (element) { await sheet.add(request.body)
response.status(200).json(element) response.status(200)
} else {
const memberId = response?.locals?.jwt?.id || -1
const roles: string[] = response?.locals?.jwt?.roles || []
const list = (await sheet.getList()) || []
const { toDatabase, toCaller } = await custom(
list,
request.body,
memberId,
roles
)
let toReturn = toCaller
if (toDatabase !== undefined) {
const element: Element = await sheet.add(toDatabase)
toCaller.id = element.id
if (!toCaller) {
toReturn = element
}
}
if (toReturn !== undefined) {
response.status(200).json(toReturn)
} else {
response.status(200)
}
} }
} catch (e: any) { } catch (e: any) {
response.status(200).json({ error: e.message }) response.status(200).json({ error: e.message })

View File

@ -3,7 +3,7 @@ import path from "path"
import _ from "lodash" import _ from "lodash"
import { promises as fs } from "fs" import { promises as fs } from "fs"
import { Volunteer } from "../../services/volunteers" import { Volunteer } from "../../services/volunteers"
import { PreVolunteer } from "../../services/preVolunteers" import { Postulant } from "../../services/postulants"
const DB_PATH = path.resolve(process.cwd(), "access/db.json") const DB_PATH = path.resolve(process.cwd(), "access/db.json")
const DB_TO_LOAD_PATH = path.resolve(process.cwd(), "access/dbToLoad.json") const DB_TO_LOAD_PATH = path.resolve(process.cwd(), "access/dbToLoad.json")
@ -14,7 +14,7 @@ export class SheetNames {
Games = "Jeux" Games = "Jeux"
PreVolunteers = "PreMembres" Postulants = "Postulants"
Teams = "Equipes" Teams = "Equipes"
@ -263,8 +263,8 @@ function anonimizedDb(_s: States): States {
anonimizedNotifs(v) anonimizedNotifs(v)
}) })
} }
if (s.PreVolunteers) { if (s.Postulants) {
;(s.PreVolunteers as PreVolunteer[]).forEach((v) => { ;(s.Postulants as Postulant[]).forEach((v) => {
anonimizedNameEmailMobile(v) anonimizedNameEmailMobile(v)
v.comment = v.id % 3 === 0 ? "Bonjour, j'adore l'initiative!" : "" v.comment = v.id % 3 === 0 ? "Bonjour, j'adore l'initiative!" : ""
}) })
@ -272,11 +272,11 @@ function anonimizedDb(_s: States): States {
return s return s
} }
function idADev(v: Volunteer | PreVolunteer): boolean { function idADev(v: Volunteer | Postulant): boolean {
return ((v as Volunteer)?.roles || []).includes("dev") return ((v as Volunteer)?.roles || []).includes("dev")
} }
function anonimizedNameEmailMobile(v: Volunteer | PreVolunteer): void { function anonimizedNameEmailMobile(v: Volunteer | Postulant): void {
if (idADev(v)) { if (idADev(v)) {
return return
} }

View File

@ -0,0 +1,44 @@
import _ from "lodash"
import ExpressAccessors from "./expressAccessors"
import { Postulant, PostulantWithoutId, translationPostulant } from "../../services/postulants"
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
const expressAccessor = new ExpressAccessors<PostulantWithoutId, Postulant>(
"Postulants",
new Postulant(),
translationPostulant
)
export const postulantListGet = expressAccessor.listGet()
export const postulantGet = expressAccessor.get()
export const postulantAdd = expressAccessor.add(async (list, body) => {
const params = body
const postulant = getByEmail(list, params.email)
if (postulant) {
throw Error("Il y a déjà quelqu'un avec cet email")
}
if (!validMobile(params.mobile)) {
throw Error("Numéro de téléphone invalide, contacter pierre.scelles@gmail.com")
}
const newPostulant = _.omit(new Postulant(), "id")
_.assign(newPostulant, {
lastname: trim(params.lastname),
firstname: trim(params.firstname),
email: trim(params.email),
mobile: canonicalMobile(params.mobile),
})
return {
toDatabase: newPostulant,
toCaller: {},
}
})
export const postulantSet = expressAccessor.set()
function getByEmail<T extends { email: string }>(list: T[], rawEmail: string): T | undefined {
const email = canonicalEmail(rawEmail || "")
const volunteer = list.find((v) => canonicalEmail(v.email) === email)
return volunteer
}

View File

@ -1,19 +0,0 @@
import ExpressAccessors from "./expressAccessors"
import {
PreVolunteer,
PreVolunteerWithoutId,
translationPreVolunteer,
} from "../../services/preVolunteers"
const expressAccessor = new ExpressAccessors<PreVolunteerWithoutId, PreVolunteer>(
"PreVolunteers",
new PreVolunteer(),
translationPreVolunteer
)
export const preVolunteerListGet = expressAccessor.listGet()
export const preVolunteerGet = expressAccessor.get()
export const preVolunteerAdd = expressAccessor.add()
export const preVolunteerSet = expressAccessor.set()
export const preVolunteerCountGet = expressAccessor.get((list) => list?.length || 0)

View File

@ -12,8 +12,9 @@ import {
translationVolunteer, translationVolunteer,
VolunteerDayWishes, VolunteerDayWishes,
VolunteerParticipationDetails, VolunteerParticipationDetails,
VolunteerPartialAddReturn,
} from "../../services/volunteers" } from "../../services/volunteers"
import { canonicalEmail } from "../../utils/standardization" import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
import { getJwt } from "../secure" import { getJwt } from "../secure"
const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>( const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
@ -23,9 +24,41 @@ const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
) )
export const volunteerListGet = expressAccessor.listGet() export const volunteerListGet = expressAccessor.listGet()
export const volunteerAdd = expressAccessor.add() // export const volunteerAdd = expressAccessor.add()
export const volunteerSet = expressAccessor.set() export const volunteerSet = expressAccessor.set()
export const volunteerPartialAdd = expressAccessor.add(async (list, body) => {
const params = body[0]
const volunteer = getByEmail(list, params.email)
if (volunteer) {
throw Error("Il y a déjà un bénévole avec cet email")
}
if (!validMobile(params.mobile)) {
throw Error("Numéro de téléphone invalide, contacter pierre.scelles@gmail.com")
}
const password = generatePassword()
const passwordHash = await bcrypt.hash(password, 10)
const newVolunteer = _.omit(new Volunteer(), "id")
_.assign(newVolunteer, {
lastname: trim(params.lastname),
firstname: trim(params.firstname),
email: trim(params.email),
mobile: canonicalMobile(params.mobile),
password1: passwordHash,
password2: passwordHash,
})
return {
toDatabase: newVolunteer,
toCaller: {
password,
} as VolunteerPartialAddReturn,
}
})
export const volunteerLogin = expressAccessor.get<VolunteerLogin>(async (list, bodyArray) => { export const volunteerLogin = expressAccessor.get<VolunteerLogin>(async (list, bodyArray) => {
const [body] = bodyArray const [body] = bodyArray
const volunteer = getByEmail(list, body.email) const volunteer = getByEmail(list, body.email)

View File

@ -19,16 +19,17 @@ import certbotRouter from "../routes/certbot"
import { hasSecret, secure } from "./secure" import { hasSecret, secure } from "./secure"
import { announcementListGet } from "./gsheets/announcements" import { announcementListGet } from "./gsheets/announcements"
import { gameListGet } from "./gsheets/games" import { gameListGet } from "./gsheets/games"
import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers" import { postulantAdd } from "./gsheets/postulants"
import { teamListGet } from "./gsheets/teams" import { teamListGet } from "./gsheets/teams"
import { import {
volunteerSet,
volunteerLogin,
volunteerForgot,
volunteerAsksSet,
volunteerParticipationDetailsSet,
volunteerTeamWishesSet,
volunteerDayWishesSet, volunteerDayWishesSet,
volunteerForgot,
volunteerLogin,
volunteerAsksSet,
volunteerPartialAdd,
volunteerParticipationDetailsSet,
volunteerSet,
volunteerTeamWishesSet,
} from "./gsheets/volunteers" } from "./gsheets/volunteers"
import { wishListGet, wishAdd } from "./gsheets/wishes" import { wishListGet, wishAdd } from "./gsheets/wishes"
import config from "../config" import config from "../config"
@ -82,8 +83,8 @@ app.get(
app.get("/GameListGet", gameListGet) app.get("/GameListGet", gameListGet)
app.get("/WishListGet", wishListGet) app.get("/WishListGet", wishListGet)
app.post("/WishAdd", wishAdd) app.post("/WishAdd", wishAdd)
app.post("/PreVolunteerAdd", preVolunteerAdd) app.post("/PostulantAdd", postulantAdd)
app.get("/PreVolunteerCountGet", preVolunteerCountGet) app.post("/VolunteerPartialAdd", volunteerPartialAdd)
app.post("/VolunteerLogin", volunteerLogin) app.post("/VolunteerLogin", volunteerLogin)
app.post("/VolunteerForgot", volunteerForgot) app.post("/VolunteerForgot", volunteerForgot)

View File

@ -1,4 +1,4 @@
export class PreVolunteer { export class Postulant {
id = 0 id = 0
firstname = "" firstname = ""
@ -9,25 +9,25 @@ export class PreVolunteer {
mobile = "" mobile = ""
alreadyVolunteer = false potential = false
comment = "" comment = ""
} }
export const translationPreVolunteer: { [k in keyof PreVolunteer]: string } = { export const translationPostulant: { [k in keyof Postulant]: string } = {
id: "id", id: "id",
firstname: "prenom", firstname: "prenom",
lastname: "nom", lastname: "nom",
email: "email", email: "email",
mobile: "telephone", mobile: "telephone",
alreadyVolunteer: "dejaBenevole", potential: "potentiel",
comment: "commentaire", comment: "commentaire",
} }
export const elementName = "PreVolunteer" export const elementName = "Postulant"
export const emailRegexp = export const emailRegexp =
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
export const passwordMinLength = 4 export const passwordMinLength = 4
export type PreVolunteerWithoutId = Omit<PreVolunteer, "id"> export type PostulantWithoutId = Omit<Postulant, "id">

View File

@ -0,0 +1,9 @@
import ServiceAccessors from "./accessors"
import { elementName, Postulant, PostulantWithoutId } from "./postulants"
const serviceAccessors = new ServiceAccessors<PostulantWithoutId, Postulant>(elementName)
export const postulantListGet = serviceAccessors.listGet()
export const postulantGet = serviceAccessors.get()
export const postulantAdd = serviceAccessors.add()
export const postulantSet = serviceAccessors.set()

View File

@ -1,10 +0,0 @@
import ServiceAccessors from "./accessors"
import { elementName, PreVolunteer, PreVolunteerWithoutId } from "./preVolunteers"
const serviceAccessors = new ServiceAccessors<PreVolunteerWithoutId, PreVolunteer>(elementName)
export const preVolunteerListGet = serviceAccessors.listGet()
export const preVolunteerGet = serviceAccessors.get()
export const preVolunteerAdd = serviceAccessors.add()
export const preVolunteerSet = serviceAccessors.set()
export const preVolunteerCountGet = serviceAccessors.countGet()

View File

@ -1,4 +1,5 @@
export class Volunteer { /* eslint-disable max-classes-per-file */
export class Volunteer implements VolunteerPartial {
id = 0 id = 0
lastname = "" lastname = ""
@ -9,7 +10,7 @@ export class Volunteer {
mobile = "" mobile = ""
photo = "" photo = "anonyme.png"
adult = 1 adult = 1
@ -27,7 +28,7 @@ export class Volunteer {
tshirtSize = "" tshirtSize = ""
food = "" food = "Aucune"
teamWishes: number[] = [] teamWishes: number[] = []
@ -72,6 +73,22 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
acceptsNotifs: "accepteLesNotifs", acceptsNotifs: "accepteLesNotifs",
} }
export class VolunteerPartial {
lastname = ""
firstname = ""
email = ""
mobile = ""
}
export class VolunteerPartialAddReturn {
id = 0
password = ""
}
export const elementName = "Volunteer" export const elementName = "Volunteer"
export const volunteerExample: Volunteer = { export const volunteerExample: Volunteer = {

View File

@ -13,7 +13,7 @@ const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(ele
export const volunteerListGet = serviceAccessors.listGet() export const volunteerListGet = serviceAccessors.listGet()
export const volunteerGet = serviceAccessors.get() export const volunteerGet = serviceAccessors.get()
export const volunteerAdd = serviceAccessors.add() export const volunteerPartialAdd = serviceAccessors.customPost<[Partial<Volunteer>]>("PartialAdd")
export const volunteerSet = serviceAccessors.set() export const volunteerSet = serviceAccessors.set()
export const volunteerLogin = export const volunteerLogin =

39
src/store/postulantAdd.ts Normal file
View File

@ -0,0 +1,39 @@
import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit"
import { StateRequest, elementAddFetch } from "./utils"
import { Postulant } from "../services/postulants"
import { postulantAdd } from "../services/postulantsAccessors"
const postulantAdapter = createEntityAdapter<Postulant>()
const postulantAddSlice = createSlice({
name: "addPostulant",
initialState: postulantAdapter.getInitialState({
readyStatus: "idle",
} as StateRequest),
reducers: {
getRequesting: (state) => {
state.readyStatus = "request"
},
getSuccess: (state, { payload }: PayloadAction<Postulant>) => {
state.readyStatus = "success"
postulantAdapter.addOne(state, payload)
},
getFailure: (state, { payload }: PayloadAction<string>) => {
state.readyStatus = "failure"
state.error = payload
},
},
})
export default postulantAddSlice.reducer
export const { getRequesting, getSuccess, getFailure } = postulantAddSlice.actions
export const fetchPostulantAdd = elementAddFetch(
postulantAdd,
getRequesting,
getSuccess,
getFailure,
() => null,
() => null
)

View File

@ -1,39 +0,0 @@
import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit"
import { StateRequest, elementAddFetch } from "./utils"
import { PreVolunteer } from "../services/preVolunteers"
import { preVolunteerAdd } from "../services/preVolunteersAccessors"
const preVolunteerAdapter = createEntityAdapter<PreVolunteer>()
const preVolunteerAddSlice = createSlice({
name: "addPreVolunteer",
initialState: preVolunteerAdapter.getInitialState({
readyStatus: "idle",
} as StateRequest),
reducers: {
getRequesting: (state) => {
state.readyStatus = "request"
},
getSuccess: (state, { payload }: PayloadAction<PreVolunteer>) => {
state.readyStatus = "success"
preVolunteerAdapter.addOne(state, payload)
},
getFailure: (state, { payload }: PayloadAction<string>) => {
state.readyStatus = "failure"
state.error = payload
},
},
})
export default preVolunteerAddSlice.reducer
export const { getRequesting, getSuccess, getFailure } = preVolunteerAddSlice.actions
export const fetchPreVolunteerAdd = elementAddFetch(
preVolunteerAdd,
getRequesting,
getSuccess,
getFailure,
() => null,
() => null
)

View File

@ -1,46 +0,0 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { StateRequest, toastError, elementValueFetch } from "./utils"
import { preVolunteerCountGet } from "../services/preVolunteersAccessors"
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 bénévoles potentiels: ${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

@ -4,12 +4,11 @@ import { connectRouter } from "connected-react-router"
import auth from "./auth" import auth from "./auth"
import gameList from "./gameList" import gameList from "./gameList"
import announcementList from "./announcementList" import announcementList from "./announcementList"
import preVolunteerAdd from "./preVolunteerAdd" import postulantAdd from "./postulantAdd"
import preVolunteerCount from "./preVolunteerCount"
import teamList from "./teamList" import teamList from "./teamList"
import ui from "./ui" import ui from "./ui"
import volunteer from "./volunteer" import volunteer from "./volunteer"
import volunteerAdd from "./volunteerAdd" import volunteerAdd from "./volunteerPartialAdd"
import volunteerList from "./volunteerList" import volunteerList from "./volunteerList"
import volunteerSet from "./volunteerSet" import volunteerSet from "./volunteerSet"
import volunteerLogin from "./volunteerLogin" import volunteerLogin from "./volunteerLogin"
@ -27,8 +26,7 @@ export default (history: History) => ({
auth, auth,
gameList, gameList,
announcementList, announcementList,
preVolunteerAdd, postulantAdd,
preVolunteerCount,
teamList, teamList,
ui, ui,
volunteer, volunteer,

View File

@ -65,7 +65,7 @@ export function elementFetch<Element, ServiceInput extends Array<any>>(
} }
export function elementAddFetch<Element>( export function elementAddFetch<Element>(
elementAddService: (volunteerWithoutId: Omit<Element, "id">) => Promise<{ elementAddService: (elementWithoutId: Omit<Element, "id">) => Promise<{
data?: Element | undefined data?: Element | undefined
error?: Error | undefined error?: Error | undefined
}>, }>,
@ -74,12 +74,12 @@ export function elementAddFetch<Element>(
getFailure: ActionCreatorWithPayload<string, string>, getFailure: ActionCreatorWithPayload<string, string>,
errorMessage?: (error: Error) => void, errorMessage?: (error: Error) => void,
successMessage?: () => void successMessage?: () => void
): (volunteerWithoutId: Omit<Element, "id">) => AppThunk { ): (elementWithoutId: Omit<Element, "id">) => AppThunk {
return (volunteerWithoutId: Omit<Element, "id">): AppThunk => return (elementWithoutId: Omit<Element, "id">): AppThunk =>
async (dispatch) => { async (dispatch) => {
dispatch(getRequesting()) dispatch(getRequesting())
const { error, data } = await elementAddService(volunteerWithoutId) const { error, data } = await elementAddService(elementWithoutId)
if (error) { if (error) {
dispatch(getFailure(error.message)) dispatch(getFailure(error.message))
@ -119,7 +119,7 @@ export function elementListFetch<Element, ServiceInput extends Array<any>>(
} }
export function elementSet<Element>( export function elementSet<Element>(
elementSetService: (volunteer: Element) => Promise<{ elementSetService: (element: Element) => Promise<{
data?: Element | undefined data?: Element | undefined
error?: Error | undefined error?: Error | undefined
}>, }>,

View File

@ -1,12 +1,12 @@
import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit" import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit"
import { StateRequest, toastError, toastSuccess, elementAddFetch } from "./utils" import { StateRequest, elementAddFetch } from "./utils"
import { Volunteer } from "../services/volunteers" import { Volunteer } from "../services/volunteers"
import { volunteerAdd } from "../services/volunteersAccessors" import { volunteerPartialAdd } from "../services/volunteersAccessors"
const volunteerAdapter = createEntityAdapter<Volunteer>() const volunteerAdapter = createEntityAdapter<Volunteer>()
const volunteerAddSlice = createSlice({ const volunteerPartialAddSlice = createSlice({
name: "addVolunteer", name: "addVolunteer",
initialState: volunteerAdapter.getInitialState({ initialState: volunteerAdapter.getInitialState({
readyStatus: "idle", readyStatus: "idle",
@ -26,14 +26,14 @@ const volunteerAddSlice = createSlice({
}, },
}) })
export default volunteerAddSlice.reducer export default volunteerPartialAddSlice.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerAddSlice.actions export const { getRequesting, getSuccess, getFailure } = volunteerPartialAddSlice.actions
export const fetchVolunteerAdd = elementAddFetch( export const fetchVolunteerPartialAdd = elementAddFetch(
volunteerAdd, volunteerPartialAdd,
getRequesting, getRequesting,
getSuccess, getSuccess,
getFailure, getFailure,
(error: Error) => toastError(`Erreur lors de l'ajout d'un bénévole: ${error.message}`), () => null,
() => toastSuccess("Volunteer ajoutée !") () => null
) )