Merge from registration branch which was in prod

This commit is contained in:
pikiou
2022-04-19 03:23:53 +02:00
50 changed files with 1584 additions and 820 deletions

BIN
src/app/img/barnums.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
src/app/img/bene2019.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
src/app/img/pel2016.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
src/app/img/pel2017.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
src/app/img/plan2019.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

View File

@@ -0,0 +1,67 @@
import { useCallback } from "react"
import { useSelector } from "react-redux"
import { fetchVolunteerAsksSet } from "../../store/volunteerAsksSet"
import styles from "./styles.module.scss"
import { useAskTools, addAsk } from "./utils"
import FormButton from "../Form/FormButton/FormButton"
import {
fetchVolunteerDiscordIdIfNeed,
selectVolunteerDiscordId,
} from "../../store/volunteerDiscordId"
export function AskDiscord(asks: JSX.Element[], id: number): void {
const { dispatch, jwtToken, volunteerAsks } = useAskTools()
const discordId: number | undefined = useSelector(selectVolunteerDiscordId)
const onSubmit = useCallback((): void => {
dispatch(
fetchVolunteerAsksSet(jwtToken, 0, {
hiddenAsks: [...(volunteerAsks?.hiddenAsks || []), id],
})
)
}, [dispatch, id, jwtToken, volunteerAsks?.hiddenAsks])
const needToShow = !discordId
addAsk(
asks,
id,
volunteerAsks,
true,
needToShow,
<div className={styles.formLine}>
<p>
Discord nous permet gratuitement et sans pub de s'écrire entre bénévoles via nos
navigateurs ou smartphones. Et donc de s'organiser super efficacement !<br />
C'est un peu déroutant au début, mais extrêmement pratique car à chaque sujet de
discussion correspond un salon différent que tu peux demander à suivre ou ignorer
totalement via la gestion des notifications.
<br />
Pour rejoindre le serveur PeL, voici le lien d'invitation à cliquer :{" "}
<a href="https://discord.gg/eXhjKxSBB4" onClick={onSubmit}>
https://discord.gg/eXhjKxSBB4
</a>{" "}
!
</p>
<p>
Prends le temps de le rejoindre maintenant, c'est via cet outil que la plupart des
équipes s'organisent !
</p>
<p>
Pour s'y retrouver tellement on est nombreux (plus de 120), il est nécessaire
d'avoir son prénom comme alias. Voir même d'avoir ensuite la première lettre de ton
nom de famille si un autre bénévole présent sur le serveur a le même prénom. Pour
changer ton alias uniquement sur le serveur PeL, il faut faire un clique droit sur
l'icône ronde du serveur en haut à gauche, et aller dans Modifier le profil du
serveur.
</p>
<div className={styles.formButtons}>
<FormButton onClick={onSubmit}>Ok, noté</FormButton>
</div>
</div>
)
}
// Fetch server-side data here
export const fetchFor = [fetchVolunteerDiscordIdIfNeed]

View File

@@ -191,7 +191,7 @@ export function AskPushNotif(asks: JSX.Element[], id: number): void {
volunteerAsks,
true,
needToShow,
<div className={styles.formLine} key="line-participation">
<div className={styles.formLine}>
<label>
Acceptes-tu de recevoir une alerte dans ton navigateur quand on en aura
d&apos;autres à t'afficher ici ?<br />

View File

@@ -3,19 +3,21 @@ import React, { memo } from "react"
import styles from "./styles.module.scss"
import { useAskTools } from "./utils"
import { AskWelcome } from "./AskWelcome"
import { AskPushNotif } from "./AskPushNotif"
import { AskDiscord, fetchFor as fetchForDiscord } from "./AskDiscord"
import { AskDayWishes, fetchFor as fetchForDayWishes } from "./AskDayWishes"
import { AskTeamWishes, fetchFor as fetchForTeamWishes } from "./AskTeamWishes"
import {
AskParticipationDetails,
fetchFor as fetchForParticipationDetails,
} from "./AskParticipationDetails"
import { AskPushNotif } from "./AskPushNotif"
const Asks = (): JSX.Element | null => {
const { volunteerAsks } = useAskTools()
const asks: JSX.Element[] = []
AskWelcome(asks, 1)
AskDiscord(asks, 3)
AskDayWishes(asks, 10)
AskTeamWishes(asks, 11)
@@ -28,7 +30,7 @@ const Asks = (): JSX.Element | null => {
<div key="pushNotifs">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<div className={styles.formLine} key="line-participation">
<div className={styles.formLine}>
<label>
Tu as fait le tour des dernières infos ou questions importantes,
merci ! :)
@@ -54,6 +56,7 @@ export default memo(Asks)
// Fetch server-side data here
export const fetchFor = [
...fetchForDiscord,
...fetchForDayWishes,
...fetchForTeamWishes,
...fetchForParticipationDetails,

View File

@@ -34,7 +34,7 @@ export function addAsk(
volunteerAsks: VolunteerAsks | undefined,
isNarrow: boolean,
needToShow: boolean,
children: JSX.Element
children: JSX.Element | undefined
): void {
const hidden = volunteerAsks?.hiddenAsks || []
if (_.includes(hidden, id) || !_.isEmpty(asks) || !needToShow) {

View File

@@ -1,24 +1,31 @@
import { FC, ReactNode } from "react"
import { toastError } from "../../../store/utils"
import styles from "./styles.module.scss"
type Props = {
type?: "grey"
disabled?: boolean
children: ReactNode
onClick?: () => void
}
const FormButton: FC<Props> = ({ type, children, onClick }): JSX.Element => (
<button
type="button"
className={type === "grey" ? styles.greyButton : styles.button}
onClick={onClick}
>
{children}
</button>
)
const FormButton: FC<Props> = ({ type, disabled, children, onClick }): JSX.Element => {
const onDisabledClick = () => toastError("Bouton désactivé")
return (
<button
type="button"
className={type === "grey" || disabled ? styles.greyButton : styles.button}
onClick={disabled ? onDisabledClick : onClick}
>
{children}
</button>
)
}
FormButton.defaultProps = {
type: undefined,
disabled: false,
onClick: undefined,
}

View File

@@ -1,256 +0,0 @@
import React, { memo, useState } from "react"
import { useSelector, shallowEqual } from "react-redux"
import { toast } from "react-toastify"
import _ from "lodash"
import styles from "./styles.module.scss"
import { fetchPreVolunteerAdd } from "../../store/preVolunteerAdd"
import { AppDispatch, AppState } from "../../store"
interface Props {
dispatch: AppDispatch
preVolunteerCount: number | undefined
}
const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => {
const [firstname, setFirstname] = useState("")
const [lastname, setLastname] = useState("")
const [email, setEmail] = useState("")
const [mobile, setMobile] = useState("")
const [alreadyVolunteer, setAlreadyVolunteer] = useState(false)
const [comment, setComment] = useState("")
const [sending, setSending] = useState(false)
const onFirstnameChanged = (e: React.ChangeEvent<HTMLInputElement>) =>
setFirstname(e.target.value)
const onLastnameChanged = (e: React.ChangeEvent<HTMLInputElement>) =>
setLastname(e.target.value)
const onEmailChanged = (e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)
const onMobileChanged = (e: React.ChangeEvent<HTMLInputElement>) => setMobile(e.target.value)
const onAlreadyVolunteer = (e: React.ChangeEvent<HTMLInputElement>) =>
setAlreadyVolunteer(!!e.target.value)
const onNotYesVolunteer = (e: React.ChangeEvent<HTMLInputElement>) =>
setAlreadyVolunteer(!e.target.value)
const onCommentChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) =>
setComment(e.target.value)
const onSubmit = () => {
if (firstname && lastname && email && mobile && !sending) {
dispatch(
fetchPreVolunteerAdd({
firstname,
lastname,
email,
mobile,
alreadyVolunteer,
comment,
})
)
setSending(true)
} else {
toast.warning("Il faut remplir tous les champs (sauf le dernier)", {
position: "top-center",
autoClose: 6000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
})
}
}
const { error, entities: preVolunteer } = useSelector(
(state: AppState) => state.preVolunteerAdd,
shallowEqual
)
let sendSuccess
if (!_.isEmpty(preVolunteer)) {
if (sending) {
setSending(false)
}
sendSuccess = <span className={styles.success}>Formulaire envoyé !</span>
}
let sendError
if (error && _.isEmpty(preVolunteer)) {
if (sending) {
setSending(false)
}
sendError = <span className={styles.error}>{error}</span>
}
let sendingElement
if (sending) {
sendingElement = <span className={styles.sending}>Envoi en cours...</span>
}
/*
firstname
lastname
mail
tel
j'ai déjà été bénévole pour PEL
un petit mot...
*/
return (
<form onSubmit={onSubmit}>
<dl className={styles.preRegisterIntro} key="preRegister-intro">
<dt>Qu&apos;est-ce que Paris est Ludique ?</dt>
<dd>
<p>
Un festival en plein air dédiée aux <b>jeux de société modernes</b> sous
toutes leurs formes.
</p>
<p>
En 2019 lors de la dernière édition, ce sont <b>16 000</b> joueurs qui se
sont réunis sous 300 chapiteaux et 2 000 tables.
</p>
<p>
Les 2 jours que durent le festival sont entièrement dédiés à ce que le
public <b>JOUE</b>, que ce soit sur les stands d&apos;éditeurs,
d&apos;associations, d&apos;animateurs bénévoles, du coin des petits
joueurs, de l&apos;espace tournois, ou de l&apos;espace prototypes.
</p>
</dd>
<dt>Et les bénévoles de PeL ?</dt>
<dd>
<p>
L&apos;organisation du festival est <b>entièrement gérée par nous</b>, les
bénévoles. À aucun moment ça ne ressemble à du travail : nous faisons tout
pour passer <b>un aussi bon moment que les visiteurs</b> :)
</p>
<p>
D&apos;ailleurs, un soir par mois nous nous réunissons pour un apéro ludique
discuter de l&apos;organisation ! On joue autant que les visiteurs, mais
sur toute l&apos;année ^^
</p>
<p>
Pendant le festival de 2019, nous étions <b>187 bénévoles</b> organisés en
équipes qui chouchoutent les visiteurs en les accueillant, en
s&apos;assurant que tout se passe bien, ou encore en expliquant des règles
de jeux.
</p>
<p>
Une équipe est même dédiée au bien être des bénévoles en leur servant à
boire et à manger dans un espace à part faire des pauses régulières. Et
puis nous hébergeons ceux d&apos;entre nous qui habitent loin de Paris. Le
confort avant tout !
</p>
<p>
Certains bénévoles sont visiteurs le samedi ou le dimanche pour vivre le
festival de l&apos;intérieur. Les deux jours avant et le jour après le
festival, ceux qui le peuvent viennent préparer et ranger. Bref, chacun
participe à la hauteur de ses wishes et disponibilités !
</p>
<p>
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.
</p>
</dd>
<dt>
Si l&apos;expérience pourrait vous tenter, remplissez le formulaire suivant pour
en discuter lors d&apos;un des gros apéros mensuels !<br />
Cette inscription ne vous oblige en rien il s&apos;agit juste d&apos;une prise
de contact.
<br />
Les prochains sont les 21 décembre et 27 janvier, mais nous vous appelerons
d&apos;ici pour les détails :)
<br />
{/* */}
<span className={styles.lightTitle} hidden={(preVolunteerCount || 0) < 3}>
(Déjà {preVolunteerCount} inscrits !)
</span>
</dt>
<dd>
<div className={styles.formLine} key="line-firstname">
<label htmlFor="firstname">Prénom</label>
<input
type="text"
id="firstname"
required
value={firstname}
onChange={onFirstnameChanged}
/>
</div>
<div className={styles.formLine} key="line-lastname">
<label htmlFor="lastname">Nom</label>
<input
type="text"
id="lastname"
required
value={lastname}
onChange={onLastnameChanged}
/>
</div>
<div className={styles.formLine} key="line-email">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
required
value={email}
onChange={onEmailChanged}
/>
</div>
<div className={styles.formLine} key="line-mobile">
<label htmlFor="mobile">Téléphone</label>
<input
type="text"
id="mobile"
required
value={mobile}
onChange={onMobileChanged}
/>
</div>
<div className={styles.formLine} key="line-already-volunteer">
<div>
J&apos;ai déjà é bénévole à PeL
<input
type="radio"
name="alreadyVolunteer"
id="alreadyVolunteer-yes"
className={styles.inputRadio}
checked={alreadyVolunteer}
onChange={onAlreadyVolunteer}
/>
<label htmlFor="alreadyVolunteer-yes">Oui</label>
<input
type="radio"
name="alreadyVolunteer"
id="alreadyVolunteer-no"
className={styles.inputRadio}
checked={!alreadyVolunteer}
onChange={onNotYesVolunteer}
/>
<label htmlFor="alreadyVolunteer-no">Non</label>
</div>
</div>
<div className={styles.formLine} key="line-message">
<textarea
name="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 :)"
value={comment}
onChange={onCommentChanged}
/>
</div>
<div className={styles.formButtons}>
<button type="button" onClick={onSubmit} disabled={sending}>
Envoyer
</button>
</div>
<div className={styles.formReactions}>
{sendingElement}
{sendSuccess}
{sendError}
</div>
</dd>
</dl>
</form>
)
}
export default memo(PreRegisterForm)

View File

@@ -1,76 +0,0 @@
@import "../../theme/variables";
@import "../../theme/mixins";
.preRegisterIntro {
dt {
font-weight: bold;
margin-top: 10px;
margin-bottom: 10px;
}
dd {
margin-bottom: 30px;
}
p {
margin-block-start: 0.3em;
margin-block-end: 0.3em;
}
}
.lightTitle {
font-weight: normal;
}
.formLine {
padding: 5px 0;
label {
display: block;
margin-left: 5px;
}
input,
textarea {
width: 100%;
padding: 4px;
border: 1px solid #333;
border-radius: 4px;
outline: 0;
}
textarea {
height: 100px;
}
.inputRadio {
margin-left: 12px;
width: inherit;
}
.inputRadio + label {
display: inline;
margin: 0 0 0 5px;
}
}
.formButtons {
margin-top: 10px;
padding: 5px 0;
text-align: center;
[disabled="true"] {
background-color: #333333c0;
color: #cccccce7;
}
}
.formReactions {
margin-top: 3px;
padding: 5px 0;
text-align: center;
.sending {
color: rgb(0, 0, 255);
}
.success {
color: rgb(0, 133, 0);
}
.error {
color: rgb(255, 0, 0);
}
}

View File

@@ -0,0 +1,793 @@
import React, { memo, useEffect, useState } from "react"
import { useSelector, shallowEqual } from "react-redux"
import { toast } from "react-toastify"
import _ from "lodash"
import classnames from "classnames"
import styles from "./styles.module.scss"
import { fetchPostulantAdd } from "../../store/postulantAdd"
import { AppDispatch, AppState } from "../../store"
import { fetchVolunteerPartialAdd } from "../../store/volunteerPartialAdd"
import FormButton from "../Form/FormButton/FormButton"
import { validEmail } from "../../utils/standardization"
import { toastError } from "../../store/utils"
interface Props {
dispatch: AppDispatch
}
const animations = [
[styles.imgTransitionDoHide, styles.imgTransitionShow],
[styles.imgTransitionHidden, styles.imgTransitionShow],
[styles.imgTransitionReset, styles.imgTransitionShow],
[styles.imgTransitionAbouToShow, styles.imgTransitionShow],
[styles.imgTransitionShow, styles.imgTransitionDoHide],
[styles.imgTransitionShow, styles.imgTransitionHidden],
[styles.imgTransitionShow, styles.imgTransitionReset],
[styles.imgTransitionShow, styles.imgTransitionAbouToShow],
]
const RegisterForm = ({ dispatch }: Props): JSX.Element => {
const [potentialVolunteer, setPotentialVolunteer] = useState(true)
const [firstname, setFirstname] = useState("")
const [lastname, setLastname] = useState("")
const [email, setEmail] = useState("")
const [mobile, setMobile] = useState("")
const [alreadyVolunteer, setAlreadyVolunteer] = useState(false)
const [comment, setComment] = useState("")
const [alreadyCame, setAlreadyCame] = useState(true)
const [firstMeeting, setFirstMeeting] = useState("")
const [commentFirstMeeting, setCommentFirstMeeting] = useState("")
const [canHelpBefore, setCanHelpBefore] = useState("")
const [pelMember, setPelMember] = useState(false)
const [howToContact, setHowToContact] = useState("Email")
const [sending, setSending] = useState(false)
const [changingBackground, setChangingBackground] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setChangingBackground((changingBackground + 1) % animations.length)
}, 60000 / animations.length)
return () => clearInterval(timer)
}, [changingBackground, setChangingBackground])
const transitionClass = (i: number) => animations[changingBackground][i - 1]
const sendTextDispatch =
(dispatchSetter: React.Dispatch<React.SetStateAction<string>>) =>
(e: React.ChangeEvent<HTMLInputElement>) =>
dispatchSetter(e.target.value)
const sendTextareaDispatch =
(dispatchSetter: React.Dispatch<React.SetStateAction<string>>) =>
(e: React.ChangeEvent<HTMLTextAreaElement>) =>
dispatchSetter(e.target.value)
const sendBooleanRadioboxDispatch =
(dispatchSetter: React.Dispatch<React.SetStateAction<boolean>>, isYes: boolean) =>
(e: React.ChangeEvent<HTMLInputElement>) =>
dispatchSetter(isYes ? !!e.target.value : !e.target.value)
const sendRadioboxDispatch =
(dispatchSetter: React.Dispatch<React.SetStateAction<string>>) =>
(e: React.ChangeEvent<HTMLInputElement>) =>
dispatchSetter(e.target.value)
const onSubmit = () => {
if (!validEmail(email)) {
toastError("Cet email est invalid ><")
return
}
if (!firstname || !lastname || !email || !mobile || sending) {
toast.warning("Il faut remplir les quelques infos sur toi ><", {
position: "top-center",
autoClose: 6000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
})
return
}
if (potentialVolunteer) {
dispatch(
fetchPostulantAdd({
potential: true,
firstname,
lastname,
email,
mobile,
howToContact,
alreadyCame,
firstMeeting,
commentFirstMeeting: firstMeeting ? "" : commentFirstMeeting,
comment,
})
)
} else {
dispatch(
fetchVolunteerPartialAdd({
firstname,
lastname,
email,
mobile,
howToContact,
canHelpBefore,
pelMember,
})
)
dispatch(
fetchPostulantAdd({
potential: false,
firstname,
lastname,
email,
mobile,
howToContact,
alreadyCame,
firstMeeting,
commentFirstMeeting: firstMeeting ? "" : commentFirstMeeting,
comment,
})
)
}
setSending(true)
}
const { error: postulantError, entities: postulant } = useSelector(
(state: AppState) => state.postulantAdd,
shallowEqual
)
const { error: volunteerError, entities: volunteer } = useSelector(
(state: AppState) => state.volunteerAdd,
shallowEqual
)
let sendSuccess
let sendError
let sendingElement
if (
!postulantError &&
!_.isEmpty(postulant) &&
(potentialVolunteer || (!volunteerError && !_.isEmpty(volunteer)))
) {
if (sending) {
setSending(false)
}
sendSuccess = <span className={styles.success}>Formulaire envoyé !</span>
} else if (postulantError && _.isEmpty(postulant)) {
if (sending) {
setSending(false)
}
sendError = <span className={styles.error}>{postulantError}</span>
} else if (volunteerError && _.isEmpty(volunteer)) {
if (sending) {
setSending(false)
}
sendError = <span className={styles.error}>{volunteerError}</span>
} else if (sending) {
sendingElement = (
<span className={styles.sending}>
Envoi en cours...
<br />
En cas de problème, écrire à contact@parisestludique.fr
</span>
)
}
const intro = (
<dl className={styles.registerIntro}>
<dt>Qu&apos;est-ce que Paris est Ludique ?</dt>
<dd>
<p>
Un festival en plein air dédié aux <b>jeux de société modernes</b> sous toutes
leurs formes. Les samedi 2 et dimanche 3 juillet 2022 !
</p>
<p>
En 2019 lors de la dernière édition, ce sont <b>16 000</b> joueurs qui se sont
réunis sous 300 chapiteaux et 2 000 tables.
</p>
<p>
Les 2 jours que durent le festival sont entièrement dédiés à ce que le public{" "}
<b>JOUE</b>, que ce soit sur les stands d&apos;éditeurs, d&apos;associations,
d&apos;animateurs bénévoles, du coin des petits joueurs, de l&apos;espace
tournois, ou de l&apos;espace prototypes.
</p>
<div id="pelImg" className={styles.pelImg}>
<div className={classnames(styles.pelImg1, transitionClass(1))}> </div>
<div className={classnames(styles.pelImg2, transitionClass(2))}> </div>
</div>
</dd>
<dt>Et les bénévoles de PeL ?</dt>
<dd>
<p>
L&apos;organisation du festival est <b>entièrement gérée par nous</b>, les
bénévoles. À aucun moment ça ne ressemble à du travail : nous faisons tout pour
passer <b>un aussi bon moment que les visiteurs</b> :)
</p>
<p>
D&apos;ailleurs, un soir par mois nous nous réunissons pour un apéro ludique
discuter de l&apos;organisation ! On joue autant que les visiteurs, mais sur
toute l&apos;année ^^
</p>
<p>
Pendant le festival de 2019, nous étions <b>187 bénévoles</b> organisés en
équipes qui chouchoutent les visiteurs en les accueillant, en s&apos;assurant
que tout se passe bien, ou encore en expliquant des règles de jeux.
</p>
<p>
Une équipe est même dédiée au bien être des bénévoles en leur servant à boire et
à manger dans un espace à part faire des pauses régulières. Et puis nous
hébergeons ceux d&apos;entre nous qui habitent loin de Paris. Le confort avant
tout !
</p>
<p>
La majorité d'entre nous sommes bénévoles les <b>samedi et dimanche</b>, mais
certains bénévoles ne sont pas disponibles les deux jours. On leur demande alors
d'aider à la mise en place jeudi ou vendredi, ou au rangement le lundi, à la
place d'un des jours du weekend. Bref, chacun participe comme il peut mais deux
jours minimum !
</p>
<p>
Le samedi soir quand les visiteurs sont partis, nous prolongeons la fête en
dînant avec les exposants présents sur le festival. Le dimanche rebelote juste
entre bénévoles.
</p>
<div className={styles.beneImg}> </div>
</dd>
<dt>
Si l&apos;expérience vous tente, remplissez le formulaire suivant pour devenir
bénévole !<br />
Vous pouvez aussi juste nous rencontrer avant de vous décider à devenir bénévole, on
comprend qu&apos;un saut pareil dans l&apos;inconnu soit difficile.
<br />
Dans les deux cas, venez rencontrer une poignée d'entre nous dans un bar/resto près
de Châtelet ! :) Sur inscription uniquement...
<br />
</dt>
</dl>
)
const potentialVolunteerQuestion = (
<div className={styles.inputWrapper}>
<div className={styles.leftCol}>
<div className={styles.multipleChoiceTitle}>Je veux devenir bénévole :</div>
</div>
<div className={styles.rightCol}>
{["Tout de suite !", "Peut-être après une rencontre avec des bénévoles"].map(
(option) => (
<label className={styles.longAnswerLabel} key={option}>
<input
type="radio"
name="potentialVolunteer"
onChange={sendBooleanRadioboxDispatch(
setPotentialVolunteer,
option !== "Tout de suite !"
)}
checked={potentialVolunteer === (option !== "Tout de suite !")}
/>{" "}
{option}
</label>
)
)}
</div>
</div>
)
const alreadyVolunteerQuestion = !potentialVolunteer && (
<>
<div className={styles.inputWrapper}>
<div className={styles.leftCol}>
<div className={styles.multipleChoiceTitle}>J'ai déjà été bénévole à PeL</div>
</div>
<div className={styles.rightCol}>
<div className={styles.rightColContainer}>
{["Oui", "Non"].map((option) => (
<label className={styles.shortAnswerLabel} key={option}>
<input
type="radio"
name="alreadyVolunteer"
onChange={sendBooleanRadioboxDispatch(
setAlreadyVolunteer,
option === "Oui"
)}
checked={alreadyVolunteer === (option === "Oui")}
/>{" "}
{option}
</label>
))}
</div>
</div>
</div>
{alreadyVolunteer && (
<dl className={styles.registerIntro}>
<dd>
<p>Dans ce cas pourquoi t'inscris-tu ici ? ^^</p>
<p>
Si tu te rappelles de l'email que tu avais utilisé à ta dernière
inscription sur le site Force Orange des bénévoles (même sur l'ancienne
version) tu peux{" "}
<a href="/sidentifier" target="_blank" rel="noreferrer">
t'identifier ici
</a>{" "}
avec ton ancien mot de passe, ou en{" "}
<a href="/sinscrire" target="_blank" rel="noreferrer">
demander un nouveau ici
</a>
.
</p>
<p>
Autrement, si tu as changé d'email, mieux vaut nous le communiquer à
benevoles@parisestludique.fr en précisant bien tes nom et prénom :)
</p>
</dd>
</dl>
)}
</>
)
const commentQuestion = (
<dl className={styles.inputWrapper}>
<dd className={styles.commentWrapper}>
<textarea
name="message"
id="message"
className={styles.inputWrapper}
placeholder="Peux-tu nous dire ici comment tu as connu le festival, ce qui te motive à nous rejoindre, quelles compétences tu aimerais éventuellement développer ou utiliser... ou tu n'y as pas trop réfléchi et tu trouveras en discutant avec nous :)"
value={comment}
onChange={sendTextareaDispatch(setComment)}
/>
</dd>
</dl>
)
const cameAsVisitor = (
<>
<div className={styles.inputWrapper}>
<div className={styles.leftCol}>
<div className={styles.multipleChoiceTitle}>
Es-tu déjà venu à PeL en tant que visiteur ?
</div>
</div>
<div className={styles.rightCol}>
<div className={styles.rightColContainer}>
{["Oui", "Non"].map((option) => (
<label className={styles.shortAnswerLabel} key={option}>
<input
type="radio"
name="alreadyCame"
onChange={sendBooleanRadioboxDispatch(
setAlreadyCame,
option === "Oui"
)}
checked={alreadyCame === (option === "Oui")}
/>{" "}
{option}
</label>
))}
</div>
</div>
</div>
{!alreadyCame && (
<dl className={styles.registerIntro}>
<dt>Dans ce cas laisse-moi t'en dire un peu plus sur le festival !</dt>
<dd>
<p>
Il a lieu peu après la Foire du Trône et au même emplacement, sur la{" "}
<a
href="https://www.google.com/maps/place/Pelouse+de+Reuilly,+75012+Paris/@48.8301639,2.4043365,17z/data=!3m1!4b1!4m5!3m4!1s0x47e67258d44aa311:0x2c6a08bb6aa88f4d!8m2!3d48.8301604!4d2.4065252?hl=en"
target="_blank"
rel="noreferrer"
>
"pelouse" de Reuilly
</a>
. En voici le plan de 2019, quand il s'étendait sur 2 hectares pour
accueillir 16 000 visiteurs. La plupart des rectangles colorés que tu
vois dessus sont d'énormes barnums, ou agglomérats de tonnelles.
</p>
<div className={styles.barnumsImg}> </div>
<div className={styles.planImg}> </div>
<p>
Les espaces jeux bleu, violet, gris, ou marron sont installés et animés
par des éditeurs professionnels. Des pros gèrent les zones de
restauration rouges. Tout le reste est tenu par des associations ou des
bénévoles du festival.
</p>
<p>
Après l'édition de 2019, on a fait cette petite vidéo qui donne une
bonne impression de l'ambiance :{" "}
<a
href="https://www.youtube.com/watch?v=eVuQaERb7EU"
target="_blank"
rel="noreferrer"
>
https://www.youtube.com/watch?v=eVuQaERb7EU
</a>
</p>
<p>
Et des visiteurs passionnés (
<a
href="https://www.youtube.com/c/RoadNTroll"
target="_blank"
rel="noreferrer"
>
Road N Troll
</a>
) ont fait cette présentation plus en détail des différentes zones en
2018. J'ai coupé les présentations de jeux mais tout le reste est encore
d'actualité :{" "}
<a
href="https://www.youtube.com/watch?v=jSCHWqjHJIQ"
target="_blank"
rel="noreferrer"
>
https://www.youtube.com/watch?v=jSCHWqjHJIQ
</a>
</p>
</dd>
</dl>
)}
</>
)
const meeting = (
<>
<dl className={styles.registerIntro}>
{!potentialVolunteer && <dt>Faisons connaissance !</dt>}
{potentialVolunteer && (
<dt>Se rencontrer avant de se décider à devenir bénévole ?</dt>
)}
<dd>
<p>
On organise des rencontres entre de nouvelles personnes comme toi, et des
bénévoles suffisamment expérimentés pour te parler en détail du festival et
répondre à toutes tes questions liées au bénévolat ou au festival.
</p>
<p>
Ces rencontres ont lieu dans un bar/resto calme à Châtelet, le{" "}
<a
href="https://goo.gl/maps/N5NYWDF66vNQDFMh8"
target="_blank"
rel="noreferrer"
>
Street Food Market
</a>
.
</p>
</dd>
</dl>
<div className={styles.inputWrapper}>
<div className={styles.leftCol}>
<div className={styles.multipleChoiceTitle}>
À quelle date pourrais-tu venir ?
</div>
</div>
<div className={styles.rightCol}>
<div className={styles.rightColContainer}>
{[
{ value: "20avril", desc: "Mercredi 20 avril à 19h" },
{ value: "13mai", desc: "Vendredi 13 mai à 19h30" },
{ value: "", desc: "Aucune date possible" },
].map((option) => (
<label className={styles.longAnswerLabel} key={option.value}>
<input
type="radio"
name="firstMeeting"
value={option.value}
onChange={sendRadioboxDispatch(setFirstMeeting)}
checked={firstMeeting === option.value}
/>{" "}
{option.desc}
</label>
))}
</div>
</div>
</div>
{firstMeeting !== "" && (
<dl className={styles.registerIntro}>
<dd>
<p>
Top ! On fait en sorte qu'il y ait assez de bénévoles expérimentés pour
les nombreux curieux comme toi, donc pour ne pas gâcher leur temps on
compte sur ta présence :)
</p>
<p>Si tu as un contre-temps, écris-nous à benevoles@parisestludique.fr</p>
<p>À très bientôt !</p>
</dd>
</dl>
)}
{firstMeeting === "" && (
<div className={styles.inputWrapper}>
<div className={styles.commentWrapper}>
<textarea
name="commentFirstMeeting"
id="commentFirstMeeting"
className={styles.inputWrapper}
placeholder={
potentialVolunteer
? "Mince. Quelles dates t'arrangeraient ? Ou si c'est plus simple, quels jours sont à éviter ? Est-ce trop loin de chez toi ? Préfères-tu nous rencontrer en visio ?"
: "Ce n'est pas obligé mais ça aurait été top ! Manques-tu de temps ? Préfères-tu une autre date ? Est-ce trop loin de chez toi ? Préfères-tu nous rencontrer en visio ?"
}
value={commentFirstMeeting}
onChange={sendTextareaDispatch(setCommentFirstMeeting)}
/>
</div>
</div>
)}
</>
)
const helpBefore = !potentialVolunteer && (
<>
<dl className={styles.registerIntro}>
<dt>Bénévolat en amont du festival</dt>
<dd>
<p>
En tant que bénévole, tu fais selon tes envies, tes disponibilités, ton
énergie. Si personne ne veut faire quelque chose de primordial pour le
festival, on paye quelqu'un de l'extérieur. Par exemple le transport+montage
des tentes+tables, ou la sécurité de nuit sont délégués à des prestataires.
Et si ce quelque chose n'est pas primordiale et que personne ne veut s'en
occuper, bah tant pis on le fait pas ^^
</p>
<p>
Après on essaye de faire plein de choses sans aide extérieure. Pour le
plaisir de collaborer à un projet entre bénévoles parfois devenus amis, pour
acquérir de nouvelles compétences, parce que chaque économie d'argent fait
baisser le prix d'entrée au festival et contribue à le rendre plus
accessible.
</p>
</dd>
</dl>
<div className={styles.inputWrapper}>
<div className={styles.leftCol}>
<div className={styles.multipleChoiceTitle}>
Bref, as-tu le temps et l'envie de voir si tu peux aider en amont du
festival ?
</div>
</div>
<div className={styles.rightCol}>
<div className={styles.rightColContainer}>
{[
{ value: "oui", desc: "Oui" },
{ value: "non", desc: "Non" },
{ value: "", desc: "Ne sais pas" },
].map((option) => (
<label className={styles.shortAnswerLabel} key={option.value}>
<input
type="radio"
name="canHelpBefore"
value={option.value}
onChange={sendRadioboxDispatch(setCanHelpBefore)}
checked={canHelpBefore === option.value}
/>{" "}
{option.desc}
</label>
))}
</div>
</div>
</div>
<dl className={styles.registerIntro}>
<dd>
{canHelpBefore === "oui" && (
<p>
Génial ! Quand tu auras fini de t'inscrire et que tu seras identifié sur
le site, nous t'en parlerons plus en détail.
</p>
)}
{canHelpBefore === "non" && (
<p>
Aucun souci tu nous seras d'une aide précieuse le jour J c'est déjà
énorme !
</p>
)}
<p>
Si tu changes d'avis, il sera possible de revenir sur cette décision dans
ton profil sur le site.
</p>
</dd>
</dl>
</>
)
const pelMemberQuestion = !potentialVolunteer && (
<>
<dl className={styles.registerIntro}>
<dt>Association Paris est Ludique</dt>
<dd>
<p>
Légalement il faut que le festival soit organisé par une structure, et c'est
l'association <i>Paris est Ludique !</i> qui s'en charge. Pour aider à
organiser bénévolement le festival il faut donc en faire partie. Ça n'engage
à rien et c'est gratuit, mais absolument nécessaire.
</p>
</dd>
</dl>
<div className={styles.inputWrapper}>
<div className={styles.leftCol}>
<div className={styles.multipleChoiceTitle}>
Acceptes-tu de devenir membre de l'association <i>Paris est Ludique !</i> ?
</div>
</div>
<div className={styles.rightCol}>
<div className={styles.rightColContainer}>
{["Oui", "Non"].map((option) => (
<label className={styles.shortAnswerLabel} key={option}>
<input
type="radio"
name="pelMember"
onChange={sendBooleanRadioboxDispatch(
setPelMember,
option === "Oui"
)}
checked={pelMember === (option === "Oui")}
/>{" "}
{option}
</label>
))}
</div>
</div>
</div>
{!pelMember && (
<dl className={styles.registerIntro}>
<dd>
<p>
Tant que tu n'as pas accepté cette condition je suis désolé on ne peut
pas continuer.
</p>
</dd>
</dl>
)}
</>
)
const nameMobileEmail = (
<>
<dl className={styles.registerIntro}>
<dt>Quelques infos sur toi pour finir</dt>
</dl>
<div className={styles.inputWrapper}>
<div className={styles.leftColTiny}>
<label htmlFor="firstname">Prénom</label>
</div>
<div className={styles.rightColLefter}>
<input
type="text"
id="firstname"
required
value={firstname}
onChange={sendTextDispatch(setFirstname)}
/>
</div>
</div>
<div className={styles.inputWrapper}>
<div className={styles.leftColTiny}>
<label htmlFor="lastname">Nom</label>
</div>
<div className={styles.rightColLefter}>
<input
type="text"
id="lastname"
required
value={lastname}
onChange={sendTextDispatch(setLastname)}
/>
</div>
</div>
<div className={styles.inputWrapper}>
<div className={styles.leftColTiny}>
<label htmlFor="email">Email</label>
</div>
<div className={styles.rightColLefter}>
<input
type="email"
id="email"
required
value={email}
onChange={sendTextDispatch(setEmail)}
/>
</div>
</div>
<div className={styles.inputWrapper}>
<div className={styles.leftColTiny}>
<label htmlFor="mobile">Téléphone</label>
</div>
<div className={styles.rightColLefter}>
<input
type="text"
id="mobile"
required
value={mobile}
onChange={sendTextDispatch(setMobile)}
/>
</div>
</div>
<div className={styles.inputWrapper}>
<div className={styles.leftCol}>
<div className={styles.multipleChoiceTitle}>
Par quel moyen fiable et rapide préfères-tu être contacté si on en a besoin
?
</div>
</div>
<div className={styles.rightCol}>
<div className={styles.rightColContainer}>
{["Email", "SMS", "WhatsApp", "Signal", "Appel", "Aucun"].map((option) => (
<label className={styles.shortAnswerLabel} key={option}>
<input
type="radio"
name="howToContact"
value={option}
onChange={sendRadioboxDispatch(setHowToContact)}
checked={howToContact === option}
/>{" "}
{option}
</label>
))}
</div>
</div>
</div>
{howToContact === "Aucun" && (
<dl className={styles.registerIntro}>
<dd>
<p>
Aïe ça va poser problème, je suis désolé. Il faut faire un effort en
choisissant un moyen de communication proposé.
</p>
<p>
Tu en connais un suffisamment répandu et meilleur que ceux proposés ?
Parle-nous en à benevoles@parisestludique.fr :)
</p>
</dd>
</dl>
)}
</>
)
const submitButton = (
<>
<div className={styles.buttonWrapper}>
<FormButton onClick={onSubmit} disabled={!potentialVolunteer && alreadyVolunteer}>
Envoyer
</FormButton>
</div>
<div className={styles.formReactions}>
{sendingElement}
{sendSuccess}
{sendError}
</div>
</>
)
return (
<form>
{intro}
{potentialVolunteerQuestion}
{alreadyVolunteerQuestion}
{(potentialVolunteer || !alreadyVolunteer) && (
<>
{commentQuestion}
{cameAsVisitor}
{meeting}
{helpBefore}
{pelMemberQuestion}
{(potentialVolunteer || pelMember) && (
<>
{nameMobileEmail}
{howToContact !== "Aucun" && submitButton}
</>
)}
</>
)}
</form>
)
}
export default memo(RegisterForm)

View File

@@ -0,0 +1,223 @@
@import "../../theme/variables";
@import "../../theme/mixins";
.registerIntro {
dt {
font-weight: bold;
margin-top: 10px;
margin-bottom: 10px;
}
dd {
margin-bottom: 30px;
}
p {
margin-block-start: 0.3em;
margin-block-end: 0.3em;
}
}
.lightTitle {
font-weight: normal;
}
.inputWrapper {
padding: 5px 0;
margin: 10px 0;
@include desktop {
display: flex;
}
input[type="text"] {
min-width: 175px;
border: 1px solid $color-grey-medium;
outline: 0;
}
}
.leftCol {
flex: 0 0 240px;
}
.leftColTiny {
flex: 0 0 120px;
}
.rightCol,
.rightColLefter {
width: 100%;
text-align: center;
}
.rightCol {
text-align: center;
}
.rightColLefter {
text-align: left;
}
.rightColContainer {
display: inline-block;
width: 300px;
text-align: left;
}
.multipleChoiceTitle {
display: inline-block;
width: 240px;
margin-bottom: 10px;
}
.longAnswerLabel {
text-align: left;
display: inline-block;
margin-bottom: 10px;
width: 300px;
}
.shortAnswerLabel {
text-align: left;
display: inline-block;
margin-bottom: 10px;
width: 100px;
}
.buttonWrapper {
margin-bottom: 10px;
text-align: center;
[disabled="true"] {
background-color: #333333c0;
color: #cccccce7;
}
}
.commentWrapper {
width: 100%;
margin: 0 0 14px;
textarea {
width: 100%;
height: 120px;
padding: 5px;
border: 1px solid $color-grey-light;
background-color: $color-grey-lighter;
outline: 0;
}
}
.formReactions {
margin-top: 3px;
padding: 5px 0;
text-align: center;
.sending {
color: rgb(0, 0, 255);
}
.success {
color: rgb(0, 133, 0);
}
.error {
color: rgb(255, 0, 0);
}
}
.imgTransitionReset {
left: calc(90vw);
transition: none;
@include desktop {
left: 552px;
}
}
.imgTransitionAbouToShow {
left: calc(90vw);
transition: left ease-in-out 1000ms;
@include desktop {
left: 552px;
}
}
.imgTransitionShow {
left: 0;
transition: left ease-in-out 1000ms;
}
.imgTransitionDoHide {
left: calc(-90vw);
transition: left ease-in-out 1000ms;
@include desktop {
left: -552px;
}
}
.imgTransitionHidden {
left: calc(-90vw);
transition: none;
@include desktop {
left: -552px;
}
}
.pelImg1 {
background: url("../../app/img/pel2016.jpg") no-repeat center center;
}
.pelImg2 {
background: url("../../app/img/pel2017.jpg") no-repeat center center;
}
.pelImg1,
.pelImg2 {
position: absolute;
width: calc(90vw);
height: calc(60vw);
background-size: cover;
@include desktop {
width: 552px;
height: 368px;
}
}
.pelImg {
position: relative;
width: calc(90vw);
height: calc(60vw);
overflow: hidden;
@include desktop {
width: 552px;
height: 368px;
}
}
.beneImg {
position: relative;
width: calc(90vw);
height: calc(40vw);
background: url("../../app/img/bene2019.jpg") no-repeat center center;
background-size: cover;
@include desktop {
width: 552px;
height: 249px;
}
}
.planImg {
position: relative;
width: calc(90vw);
height: calc(168vw);
background: url("../../app/img/plan2019.jpg") no-repeat center center;
background-size: cover;
@include desktop {
width: 552px;
height: 1028px;
}
}
.barnumsImg {
position: relative;
width: calc(90vw);
height: calc(18vw);
background: url("../../app/img/barnums.jpg") no-repeat center center;
background-size: cover;
@include desktop {
width: 552px;
height: 112px;
}
}

View File

@@ -11,8 +11,8 @@ import Asks, { fetchFor as fetchForAsks } from "./Asks"
import ParticipationDetailsForm, {
fetchFor as fetchForParticipationDetailsForm,
} from "./VolunteerBoard/ParticipationDetailsForm/ParticipationDetailsForm"
import PreRegisterForm from "./PreRegisterForm"
import TeamAssignment, { fetchFor as fetchForTeamAssignment } from "./TeamAssignment/TeamAssignment"
import RegisterForm from "./RegisterForm"
import TeamWishesForm, {
fetchFor as fetchForTeamWishesForm,
} from "./VolunteerBoard/TeamWishesForm/TeamWishesForm"
@@ -35,9 +35,9 @@ export {
fetchForAsks,
ParticipationDetailsForm,
fetchForParticipationDetailsForm,
PreRegisterForm,
TeamAssignment,
fetchForTeamAssignment,
RegisterForm,
TeamWishesForm,
fetchForTeamWishesForm,
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 { 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 />,
})
export default (props: Props): JSX.Element => (
<ErrorBoundary>
<PreRegister {...props} />
<Register {...props} />
</ErrorBoundary>
)

View File

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

View File

@@ -1,53 +0,0 @@
import { 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 } from "../../store"
import { fetchVolunteerIfNeed } from "../../store/volunteer"
import { VolunteerInfo, VolunteerSet } from "../../components"
import styles from "./styles.module.scss"
export type Props = RouteComponentProps<{ id: string }>
const VolunteerPage = ({ match }: Props): JSX.Element => {
const { id: rawId } = match.params
const id = +rawId
const dispatch = useDispatch()
const volunteer = useSelector((state: AppState) => state.volunteer, shallowEqual)
useEffect(() => {
dispatch(fetchVolunteerIfNeed(id))
}, [dispatch, id])
const renderInfo = () => {
const volunteerInfo = volunteer
if (!volunteerInfo || volunteerInfo.readyStatus === "request") return <p>Loading...</p>
if (volunteerInfo.readyStatus === "failure" || !volunteerInfo.entity)
return <p>Oops! Failed to load data.</p>
return (
<div>
<VolunteerInfo item={volunteerInfo.entity} />
<VolunteerSet dispatch={dispatch} volunteer={volunteerInfo.entity} />
</div>
)
}
return (
<div className={styles.VolunteerPage}>
<Helmet title="User Info" />
{renderInfo()}
</div>
)
}
interface LoadDataArgs {
params: { id: number }
}
export const loadData = ({ params }: LoadDataArgs): AppThunk[] => [fetchVolunteerIfNeed(params.id)]
export default memo(VolunteerPage)

View File

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

View File

@@ -1,3 +0,0 @@
.VolunteerPage {
padding: 0 15px;
}

View File

@@ -4,12 +4,11 @@ import App from "../app"
import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements"
import AsyncTeamAssignment, { loadData as loadTeamAssignmentData } from "../pages/TeamAssignment"
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 AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
import AsyncWish, { loadData as loadWishData } from "../pages/Wish"
import AsyncVolunteerPage, { loadData as loadVolunteerPageData } from "../pages/VolunteerPage"
import Login from "../pages/Login"
import Forgot from "../pages/Forgot"
import NotFound from "../pages/NotFound"
@@ -26,18 +25,13 @@ export default [
},
{
path: "/preRegister",
component: AsyncPreRegisterPage,
loadData: loadPreRegisterPage,
component: AsyncRegisterPage,
loadData: loadRegisterPage,
},
{
path: "/sinscrire",
component: AsyncPreRegisterPage,
loadData: loadPreRegisterPage,
},
{
path: "/VolunteerPage/:id",
component: AsyncVolunteerPage,
loadData: loadVolunteerPageData,
component: AsyncRegisterPage,
loadData: loadRegisterPage,
},
{
path: "/login",

View File

@@ -7,7 +7,7 @@ import { SheetNames, saveLocalDb, loadLocalDb } 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")
@@ -122,7 +122,7 @@ export class Sheet<
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()) || []
// eslint-disable-next-line @typescript-eslint/ban-types
const element: Element = { id: await this.nextId(), ...elementWithoutId } as Element
@@ -604,7 +604,7 @@ async function tryNTimes<T>(
return await func()
} catch (e: any) {
console.error(e?.error || e?.message || e)
console.error(`${repeatCount} attemps left every ${delayBetweenAttempts}`)
console.error(`${repeatCount} attempts left every ${delayBetweenAttempts}`)
await new Promise<void>((resolve) => {
setTimeout(() => resolve(), delayBetweenAttempts)
})

View File

@@ -3,6 +3,7 @@ import { SheetNames, ElementWithId, getSheet, Sheet } from "./accessors"
export type RequestBody = Request["body"]
export type CustomSetReturn<Element> = { toDatabase: Element; toCaller: any }
export type CustomAddReturn<Element> = { toDatabase: Omit<Element, "id">; toCaller: any }
export default class ExpressAccessors<
// 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> => {
try {
const sheet = await this.getSheet()
const element: Element = await sheet.add(request.body)
if (element) {
response.status(200).json(element)
if (!custom) {
await sheet.add(request.body)
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) {
response.status(200).json({ error: e.message })

View File

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

View File

@@ -0,0 +1,50 @@
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),
howToContact: trim(params.howToContact),
potential: params.potential === true,
alreadyCame: params.alreadyCame === true,
firstMeeting: trim(params.firstMeeting),
commentFirstMeeting: trim(params.commentFirstMeeting),
comment: trim(params.comment),
})
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

@@ -14,7 +14,7 @@ import {
VolunteerParticipationDetails,
VolunteerTeamAssign,
} from "../../services/volunteers"
import { canonicalEmail } from "../../utils/standardization"
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
import { getJwt } from "../secure"
const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
@@ -24,9 +24,74 @@ const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
)
export const volunteerListGet = expressAccessor.listGet()
export const volunteerAdd = expressAccessor.add()
export const volunteerSet = expressAccessor.set()
export const volunteerDiscordId = expressAccessor.get(async (list, body, id) => {
const requestedId = +body[0] || id
if (requestedId !== id && requestedId !== 0) {
throw Error(`On ne peut acceder qu'à ses propres envies de jours`)
}
const volunteer = list.find((v) => v.id === requestedId)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
return _.pick(volunteer, "id", "discordId")
})
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. Mieux vaut redemander un mot de passe si tu l'as oublié."
)
}
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),
howToContact: trim(params.howToContact),
canHelpBefore: trim(params.canHelpBefore),
pelMember: params.pelMember === true,
password1: passwordHash,
password2: passwordHash,
})
await sendSignUpEmail(newVolunteer.email, password)
return {
toDatabase: newVolunteer,
toCaller: {},
}
})
async function sendSignUpEmail(email: string, password: string): Promise<void> {
const apiKey = process.env.SENDGRID_API_KEY || ""
if (__DEV__ || apiKey === "") {
console.error(`Fake sending signup email to ${email} with password ${password}`)
} else {
sgMail.setApiKey(apiKey)
const msg = {
to: email,
from: "contact@parisestludique.fr",
subject: "Accès au site des bénévoles de Paris est Ludique",
text: `Ton inscription est bien enregistrée, l'aventure PeL peut commencer ! :)\nVoici ton mot de passe pour accéder au site des bénévoles où tu t'es inscrit.e : ${password}\nTu y trouveras notamment comment on communique entre bénévoles.\nBonne journée !\nPierre`,
html: `Ton inscription est bien enregistrée, l'aventure PeL peut commencer ! :)<br />Voici ton mot de passe pour accéder au <a href="https://fo.parisestludique.fr/">site des bénévoles</a> : <strong>${password}</strong><br />Tu y trouveras notamment comment on communique entre bénévoles.<br />Bonne journée !<br />Pierre`,
}
await sgMail.send(msg)
}
}
export const volunteerLogin = expressAccessor.get<VolunteerLogin>(async (list, bodyArray) => {
const [body] = bodyArray
const volunteer = getByEmail(list, body.email)

View File

@@ -19,16 +19,18 @@ import certbotRouter from "../routes/certbot"
import { hasSecret, secure } from "./secure"
import { announcementListGet } from "./gsheets/announcements"
import { gameListGet } from "./gsheets/games"
import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers"
import { postulantAdd } from "./gsheets/postulants"
import { teamListGet } from "./gsheets/teams"
import {
volunteerSet,
volunteerLogin,
volunteerForgot,
volunteerAsksSet,
volunteerParticipationDetailsSet,
volunteerTeamWishesSet,
volunteerDayWishesSet,
volunteerForgot,
volunteerDiscordId,
volunteerLogin,
volunteerPartialAdd,
volunteerParticipationDetailsSet,
volunteerSet,
volunteerTeamWishesSet,
volunteerTeamAssignSet,
volunteerListGet,
} from "./gsheets/volunteers"
@@ -84,8 +86,8 @@ app.get(
app.get("/GameListGet", gameListGet)
app.get("/WishListGet", wishListGet)
app.post("/WishAdd", wishAdd)
app.post("/PreVolunteerAdd", preVolunteerAdd)
app.get("/PreVolunteerCountGet", preVolunteerCountGet)
app.post("/PostulantAdd", postulantAdd)
app.post("/VolunteerPartialAdd", volunteerPartialAdd)
app.post("/VolunteerLogin", volunteerLogin)
app.post("/VolunteerForgot", volunteerForgot)
app.get("/VolunteerListGet", volunteerListGet)
@@ -94,7 +96,7 @@ app.get("/VolunteerListGet", volunteerListGet)
app.get("/AnnouncementListGet", secure as RequestHandler, announcementListGet)
app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
app.get("/TeamListGet", teamListGet)
// UNSAFE app.post("/VolunteerGet", secure as RequestHandler, volunteerGet)
app.get("/VolunteerDiscordId", secure as RequestHandler, volunteerDiscordId)
app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet)
app.post(
"/VolunteerParticipationDetailsSet",

View File

@@ -30,6 +30,9 @@ export default class ServiceAccessors<
...axiosConfig,
params: { id },
})
if (data.error) {
throw Error(data.error)
}
return { data }
} catch (error) {
return { error: error as Error }
@@ -51,6 +54,9 @@ export default class ServiceAccessors<
`${config.API_URL}/${this.elementName}ListGet`,
axiosConfig
)
if (data.error) {
throw Error(data.error)
}
return { data }
} catch (error) {
return { error: error as Error }
@@ -58,7 +64,7 @@ export default class ServiceAccessors<
}
}
secureListGet(): (jwt: string) => Promise<{
securedListGet(): (jwt: string) => Promise<{
data?: Element[]
error?: Error
}> {
@@ -100,6 +106,9 @@ export default class ServiceAccessors<
volunteerWithoutId,
axiosConfig
)
if (data.error) {
throw Error(data.error)
}
return { data }
} catch (error) {
return { error: error as Error }
@@ -122,6 +131,9 @@ export default class ServiceAccessors<
volunteer,
axiosConfig
)
if (data.error) {
throw Error(data.error)
}
return { data }
} catch (error) {
return { error: error as Error }
@@ -143,6 +155,9 @@ export default class ServiceAccessors<
`${config.API_URL}/${this.elementName}CountGet`,
axiosConfig
)
if (data.error) {
throw Error(data.error)
}
return { data }
} catch (error) {
return { error: error as Error }
@@ -177,6 +192,37 @@ export default class ServiceAccessors<
}
}
securedCustomGet<InputElements extends Array<any>>(
apiName: string
): (
jwt: string,
...params: InputElements
) => Promise<{
data?: any
error?: Error
}> {
interface ElementGetResponse {
data?: any
error?: Error
}
return async (jwt: string, ...params: InputElements): Promise<ElementGetResponse> => {
try {
const auth = { headers: { Authorization: `Bearer ${jwt}` } }
const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig)
const { data } = await axios.get(
`${config.API_URL}/${this.elementName}${apiName}`,
{ ...fullAxiosConfig, params }
)
if (data.error) {
throw Error(data.error)
}
return { data }
} catch (error) {
return { error: error as Error }
}
}
}
securedCustomPost<InputElements extends Array<any>>(
apiName: string
): (

View File

@@ -7,4 +7,4 @@ const serviceAccessors = new ServiceAccessors<AnnouncementWithoutId, Announcemen
// export const announcementAdd = serviceAccessors.add()
// export const announcementSet = serviceAccessors.set()
export const announcementListGet = serviceAccessors.secureListGet()
export const announcementListGet = serviceAccessors.securedListGet()

View File

@@ -0,0 +1,45 @@
export class Postulant {
id = 0
firstname = ""
lastname = ""
email = ""
mobile = ""
howToContact = ""
potential = false
alreadyCame = false
firstMeeting = ""
commentFirstMeeting = ""
comment = ""
}
export const translationPostulant: { [k in keyof Postulant]: string } = {
id: "id",
firstname: "prenom",
lastname: "nom",
email: "email",
mobile: "telephone",
howToContact: "commentContacter",
potential: "potentiel",
alreadyCame: "déjàVenu",
firstMeeting: "dateRencontre",
commentFirstMeeting: "commentaireDateRencontre",
comment: "commentaire",
}
export const elementName = "Postulant"
export const emailRegexp =
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
export const passwordMinLength = 4
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,33 +0,0 @@
export class PreVolunteer {
id = 0
firstname = ""
lastname = ""
email = ""
mobile = ""
alreadyVolunteer = false
comment = ""
}
export const translationPreVolunteer: { [k in keyof PreVolunteer]: string } = {
id: "id",
firstname: "prenom",
lastname: "nom",
email: "email",
mobile: "telephone",
alreadyVolunteer: "dejaBenevole",
comment: "commentaire",
}
export const elementName = "PreVolunteer"
export const emailRegexp =
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
export const passwordMinLength = 4
export type PreVolunteerWithoutId = Omit<PreVolunteer, "id">

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
lastname = ""
@@ -9,7 +10,7 @@ export class Volunteer {
mobile = ""
photo = ""
photo = "anonyme.png"
adult = 1
@@ -23,11 +24,11 @@ export class Volunteer {
dayWishesComment = ""
tshirtCount = ""
tshirtCount = 0
tshirtSize = ""
food = ""
food = "Aucune"
team = 0
@@ -35,6 +36,12 @@ export class Volunteer {
teamWishesComment = ""
howToContact = ""
canHelpBefore = ""
pelMember = false
hiddenAsks: number[] = []
created = new Date()
@@ -67,6 +74,9 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
team: "équipe",
teamWishes: "enviesEquipe",
teamWishesComment: "commentaireEnviesEquipe",
howToContact: "commentContacter",
canHelpBefore: "aideEnAmont",
pelMember: "membrePel",
hiddenAsks: "questionsCachees",
created: "creation",
password1: "passe1",
@@ -75,6 +85,16 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
acceptsNotifs: "accepteLesNotifs",
}
export class VolunteerPartial {
lastname = ""
firstname = ""
email = ""
mobile = ""
}
export const elementName = "Volunteer"
export const volunteerExample: Volunteer = {
@@ -90,12 +110,15 @@ export const volunteerExample: Volunteer = {
discordId: "",
dayWishes: [],
dayWishesComment: "",
tshirtCount: "1",
tshirtCount: 1,
tshirtSize: "Femme M",
food: "Végétarien",
team: 2,
teamWishes: [],
teamWishesComment: "",
howToContact: "",
canHelpBefore: "",
pelMember: false,
hiddenAsks: [],
created: new Date(0),
password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
@@ -120,6 +143,11 @@ export interface VolunteerForgot {
message: string
}
export interface VolunteerDiscordId {
id: Volunteer["id"]
discordId: Volunteer["discordId"]
}
export interface VolunteerAsks {
id: Volunteer["id"]
firstname: Volunteer["firstname"]

View File

@@ -13,8 +13,8 @@ import {
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
export const volunteerListGet = serviceAccessors.listGet()
export const volunteerGet = serviceAccessors.get()
export const volunteerAdd = serviceAccessors.add()
export const volunteerDiscordIdGet = serviceAccessors.securedCustomGet<[number]>("DiscordId")
export const volunteerPartialAdd = serviceAccessors.customPost<[Partial<Volunteer>]>("PartialAdd")
export const volunteerSet = serviceAccessors.set()
export const volunteerLogin =

View File

@@ -1,78 +0,0 @@
import axios from "axios"
import mockStore from "../../utils/mockStore"
import volunteer, {
getRequesting,
getSuccess,
getFailure,
fetchVolunteer,
initialState,
} from "../volunteer"
import { Volunteer, volunteerExample } from "../../services/volunteers"
jest.mock("axios")
const mockData: Volunteer = volunteerExample
const { id } = mockData
const mockError = "Oops! Something went wrong."
describe("volunteer reducer", () => {
it("should handle initial state correctly", () => {
// @ts-expect-error
expect(volunteer(undefined, {})).toEqual(initialState)
})
it("should handle requesting correctly", () => {
expect(volunteer(undefined, { type: getRequesting.type, payload: id })).toEqual({
readyStatus: "request",
})
})
it("should handle success correctly", () => {
expect(
volunteer(undefined, {
type: getSuccess.type,
payload: mockData,
})
).toEqual({ readyStatus: "success", entity: mockData })
})
it("should handle failure correctly", () => {
expect(
volunteer(undefined, {
type: getFailure.type,
payload: mockError,
})
).toEqual({ readyStatus: "failure", error: mockError })
})
})
describe("volunteer action", () => {
it("fetches volunteer data successful", async () => {
const { dispatch, getActions } = mockStore()
const expectedActions = [
{ type: getRequesting.type, payload: undefined },
{ type: getSuccess.type, payload: mockData },
]
// @ts-expect-error
axios.get.mockResolvedValue({ data: mockData })
await dispatch(fetchVolunteer(id))
expect(getActions()).toEqual(expectedActions)
})
it("fetches volunteer data failed", async () => {
const { dispatch, getActions } = mockStore()
const expectedActions = [
{ type: getRequesting.type },
{ type: getFailure.type, payload: mockError },
]
// @ts-expect-error
axios.get.mockRejectedValue({ message: mockError })
await dispatch(fetchVolunteer(id))
expect(getActions()).toEqual(expectedActions)
})
})

View File

@@ -35,9 +35,9 @@ export const auth = createSlice({
export const { setCurrentUser, logoutUser } = auth.actions
export const selectAuthData = (state: AppState): AuthState => state.auth
const selectAuthData = (state: AppState): AuthState => state.auth
export const selectRouter = (state: AppState): AppState["router"] => state.router
const selectRouter = (state: AppState): AppState["router"] => state.router
export const selectUserJwtToken = createSelector(selectAuthData, (authData) => authData.jwt)

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: "postulantAdd",
initialState: postulantAdapter.getInitialState({
readyStatus: "idle",
} as StateRequest),
reducers: {
getRequesting: (state) => {
state.readyStatus = "request"
},
getSuccess: (state, { payload }: PayloadAction<Postulant>) => {
state.readyStatus = "success"
postulantAdapter.setOne(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 gameList from "./gameList"
import announcementList from "./announcementList"
import preVolunteerAdd from "./preVolunteerAdd"
import preVolunteerCount from "./preVolunteerCount"
import postulantAdd from "./postulantAdd"
import teamList from "./teamList"
import ui from "./ui"
import volunteer from "./volunteer"
import volunteerAdd from "./volunteerAdd"
import volunteerAdd from "./volunteerPartialAdd"
import volunteerDiscordId from "./volunteerDiscordId"
import volunteerList from "./volunteerList"
import volunteerSet from "./volunteerSet"
import volunteerLogin from "./volunteerLogin"
@@ -28,12 +27,11 @@ export default (history: History) => ({
auth,
gameList,
announcementList,
preVolunteerAdd,
preVolunteerCount,
postulantAdd,
teamList,
ui,
volunteer,
volunteerAdd,
volunteerDiscordId,
volunteerList,
volunteerSet,
volunteerLogin,

View File

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

View File

@@ -1,53 +0,0 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { StateRequest, toastError, elementFetch } from "./utils"
import { Volunteer } from "../services/volunteers"
import { AppThunk, AppState } from "."
import { volunteerGet } from "../services/volunteersAccessors"
type StateVolunteer = { entity?: Volunteer } & StateRequest
export const initialState: StateVolunteer = {
readyStatus: "idle",
}
const volunteer = createSlice({
name: "volunteer",
initialState,
reducers: {
getRequesting: (_) => ({
readyStatus: "request",
}),
getSuccess: (_, { payload }: PayloadAction<Volunteer>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
readyStatus: "failure",
error: payload,
}),
},
})
export default volunteer.reducer
export const { getRequesting, getSuccess, getFailure } = volunteer.actions
export const fetchVolunteer = elementFetch(
volunteerGet,
getRequesting,
getSuccess,
getFailure,
(error: Error) => toastError(`Erreur lors du chargement d'un bénévole: ${error.message}`)
)
const shouldFetchVolunteer = (state: AppState, id: number) =>
state.volunteer.readyStatus !== "success" ||
(state.volunteer.entity && state.volunteer.entity.id !== id)
export const fetchVolunteerIfNeed =
(id: number): AppThunk =>
(dispatch, getState) => {
if (shouldFetchVolunteer(getState(), id)) return dispatch(fetchVolunteer(id))
return null
}

View File

@@ -0,0 +1,65 @@
import { PayloadAction, createSlice, createSelector } from "@reduxjs/toolkit"
import { StateRequest, toastError, elementFetch } from "./utils"
import { VolunteerDiscordId } from "../services/volunteers"
import { AppThunk, AppState } from "."
import { volunteerDiscordIdGet } from "../services/volunteersAccessors"
type StateVolunteerDiscordId = { entity?: VolunteerDiscordId } & StateRequest
export const initialState: StateVolunteerDiscordId = {
readyStatus: "idle",
}
const volunteerDiscordId = createSlice({
name: "volunteerDiscordId",
initialState,
reducers: {
getRequesting: (_) => ({
readyStatus: "request",
}),
getSuccess: (_, { payload }: PayloadAction<VolunteerDiscordId>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
readyStatus: "failure",
error: payload,
}),
},
})
export default volunteerDiscordId.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerDiscordId.actions
export const fetchVolunteerDiscordId = elementFetch(
volunteerDiscordIdGet,
getRequesting,
getSuccess,
getFailure,
(error: Error) =>
toastError(`Erreur lors du chargement du discordId d'un bénévole: ${error.message}`)
)
const shouldFetchVolunteerDiscordId = (state: AppState, id: number) =>
state.volunteerDiscordId.readyStatus !== "success" ||
(state.volunteerDiscordId.entity && state.volunteerDiscordId.entity.id !== id)
export const fetchVolunteerDiscordIdIfNeed =
(id = 0): AppThunk =>
(dispatch, getState) => {
let jwt = ""
if (!id) {
;({ jwt, id } = getState().auth)
}
if (shouldFetchVolunteerDiscordId(getState(), id))
return dispatch(fetchVolunteerDiscordId(jwt, id))
return null
}
export const selectVolunteerDiscordId = createSelector(
(state: AppState) => state,
(state): number | undefined => state.volunteerDiscordId?.entity?.id
)

View File

@@ -1,13 +1,13 @@
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 { volunteerAdd } from "../services/volunteersAccessors"
import { volunteerPartialAdd } from "../services/volunteersAccessors"
const volunteerAdapter = createEntityAdapter<Volunteer>()
const volunteerAddSlice = createSlice({
name: "addVolunteer",
const volunteerPartialAddSlice = createSlice({
name: "volunteerAdd",
initialState: volunteerAdapter.getInitialState({
readyStatus: "idle",
} as StateRequest),
@@ -17,7 +17,7 @@ const volunteerAddSlice = createSlice({
},
getSuccess: (state, { payload }: PayloadAction<Volunteer>) => {
state.readyStatus = "success"
volunteerAdapter.addOne(state, payload)
volunteerAdapter.setOne(state, payload)
},
getFailure: (state, { payload }: PayloadAction<string>) => {
state.readyStatus = "failure"
@@ -26,14 +26,14 @@ const volunteerAddSlice = createSlice({
},
})
export default volunteerAddSlice.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerAddSlice.actions
export default volunteerPartialAddSlice.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerPartialAddSlice.actions
export const fetchVolunteerAdd = elementAddFetch(
volunteerAdd,
export const fetchVolunteerPartialAdd = elementAddFetch(
volunteerPartialAdd,
getRequesting,
getSuccess,
getFailure,
(error: Error) => toastError(`Erreur lors de l'ajout d'un bénévole: ${error.message}`),
() => toastSuccess("Volunteer ajoutée !")
() => null,
() => null
)

View File

@@ -7,7 +7,7 @@ import { wishAdd } from "../services/wishesAccessors"
const wishAdapter = createEntityAdapter<Wish>()
const wishAddSlice = createSlice({
name: "addWish",
name: "wishAdd",
initialState: wishAdapter.getInitialState({
readyStatus: "idle",
} as StateRequest),

View File

@@ -8,7 +8,7 @@ import { wishListGet } from "../services/wishesAccessors"
const wishAdapter = createEntityAdapter<Wish>()
const wishList = createSlice({
name: "getWishList",
name: "wishList",
initialState: wishAdapter.getInitialState({
readyStatus: "idle",
} as StateRequest),