mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-09-10 21:46:27 +02:00
Merge from registration branch which was in prod
This commit is contained in:
BIN
src/app/img/barnums.jpg
Normal file
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
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
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
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
BIN
src/app/img/plan2019.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 302 KiB |
67
src/components/Asks/AskDiscord.tsx
Normal file
67
src/components/Asks/AskDiscord.tsx
Normal 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]
|
@@ -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'autres à t'afficher ici ?<br />
|
||||
|
@@ -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,
|
||||
|
@@ -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) {
|
||||
|
@@ -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,
|
||||
}
|
||||
|
||||
|
@@ -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'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'éditeurs,
|
||||
d'associations, d'animateurs bénévoles, du coin des petits
|
||||
joueurs, de l'espace tournois, ou de l'espace prototypes.
|
||||
</p>
|
||||
</dd>
|
||||
<dt>Et les bénévoles de PeL ?</dt>
|
||||
<dd>
|
||||
<p>
|
||||
L'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'ailleurs, un soir par mois nous nous réunissons pour un apéro ludique
|
||||
où discuter de l'organisation ! On joue autant que les visiteurs, mais
|
||||
sur toute l'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'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 où faire des pauses régulières. Et
|
||||
puis nous hébergeons ceux d'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'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'expérience pourrait vous tenter, remplissez le formulaire suivant pour
|
||||
en discuter lors d'un des gros apéros mensuels !<br />
|
||||
Cette inscription ne vous oblige en rien il s'agit juste d'une prise
|
||||
de contact.
|
||||
<br />
|
||||
Les prochains sont les 21 décembre et 27 janvier, mais nous vous appelerons
|
||||
d'ici là pour les détails :)
|
||||
<br />
|
||||
{/* */}
|
||||
<span className={styles.lightTitle} 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'ai déjà été 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)
|
@@ -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);
|
||||
}
|
||||
}
|
793
src/components/RegisterForm/index.tsx
Normal file
793
src/components/RegisterForm/index.tsx
Normal 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'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'éditeurs, d'associations,
|
||||
d'animateurs bénévoles, du coin des petits joueurs, de l'espace
|
||||
tournois, ou de l'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'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'ailleurs, un soir par mois nous nous réunissons pour un apéro ludique où
|
||||
discuter de l'organisation ! On joue autant que les visiteurs, mais sur
|
||||
toute l'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'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 où faire des pauses régulières. Et puis nous
|
||||
hébergeons ceux d'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'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'un saut pareil dans l'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)
|
223
src/components/RegisterForm/styles.module.scss
Executable file
223
src/components/RegisterForm/styles.module.scss
Executable 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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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)
|
27
src/pages/Register/Register.tsx
Normal file
27
src/pages/Register/Register.tsx
Normal 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)
|
@@ -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>
|
||||
)
|
||||
|
@@ -1,9 +1,9 @@
|
||||
@import "../../theme/mixins";
|
||||
|
||||
.preRegisterPage {
|
||||
.registerPage {
|
||||
@include page-wrapper-center;
|
||||
}
|
||||
|
||||
.preRegisterContent {
|
||||
.registerContent {
|
||||
@include page-content-wrapper(600px);
|
||||
}
|
@@ -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)
|
@@ -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 }
|
@@ -1,3 +0,0 @@
|
||||
.VolunteerPage {
|
||||
padding: 0 15px;
|
||||
}
|
@@ -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",
|
||||
|
@@ -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)
|
||||
})
|
||||
|
@@ -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 })
|
||||
|
@@ -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
|
||||
}
|
||||
|
50
src/server/gsheets/postulants.ts
Normal file
50
src/server/gsheets/postulants.ts
Normal 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
|
||||
}
|
@@ -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)
|
@@ -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)
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
): (
|
||||
|
@@ -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()
|
||||
|
45
src/services/postulants.ts
Normal file
45
src/services/postulants.ts
Normal 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">
|
9
src/services/postulantsAccessors.ts
Normal file
9
src/services/postulantsAccessors.ts
Normal 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()
|
@@ -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">
|
@@ -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()
|
@@ -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"]
|
||||
|
@@ -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 =
|
||||
|
@@ -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)
|
||||
})
|
||||
})
|
@@ -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
39
src/store/postulantAdd.ts
Normal 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
|
||||
)
|
@@ -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
|
||||
)
|
@@ -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
|
||||
}
|
@@ -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,
|
||||
|
@@ -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
|
||||
}>,
|
||||
|
@@ -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
|
||||
}
|
65
src/store/volunteerDiscordId.ts
Normal file
65
src/store/volunteerDiscordId.ts
Normal 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
|
||||
)
|
@@ -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
|
||||
)
|
@@ -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),
|
||||
|
@@ -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),
|
||||
|
Reference in New Issue
Block a user