Add PersonalInfo form and ask; Fix Meals

This commit is contained in:
pikiou 2022-06-22 09:07:40 +02:00
parent ff05fed345
commit 88549bf42d
27 changed files with 750 additions and 98 deletions

View File

@ -1,2 +1 @@
public
announces/*

1
.gitignore vendored
View File

@ -24,7 +24,6 @@ access/*
*.log
.idea
announces/*
/styles.module.css
# VS Code stuff
.vscode/settings.json

View File

@ -9,4 +9,3 @@ public/*
*.log
node_modules/*
access/*
announces/*

View File

@ -84,6 +84,7 @@
"autoprefixer": "^10.2.6",
"axios": "^0.21.1",
"bcrypt": "^5.0.1",
"body-parser": "^1.20.0",
"chalk": "^4.1.1",
"classnames": "^2.3.1",
"compression": "^1.7.4",

View File

@ -0,0 +1,36 @@
import { get } from "lodash"
import { useCallback } from "react"
import { fetchVolunteerAsksSet } from "../../store/volunteerAsksSet"
import { useAskTools, addAsk, answerLaterOnProfile } from "./utils"
import PersonalInfoForm, {
fetchFor as fetchForPersonalInfoForm,
} from "../VolunteerBoard/PersonalInfoForm/PersonalInfoForm"
import { useUserPersonalInfo } from "../VolunteerBoard/personalInfo.utils"
export function AskPersonalInfo(asks: JSX.Element[], id: number): void {
const { dispatch, jwtToken, volunteerAsks } = useAskTools()
const onSubmit = useCallback((): void => {
dispatch(
fetchVolunteerAsksSet(jwtToken, 0, {
hiddenAsks: [...(volunteerAsks?.hiddenAsks || []), id],
})
)
}, [dispatch, id, jwtToken, volunteerAsks?.hiddenAsks])
const [personalInfo] = useUserPersonalInfo()
const photo = get(personalInfo, "photo", false)
const needToShow = !/^[0-9]/.test(photo || "")
addAsk(
asks,
id,
volunteerAsks,
false,
needToShow,
<PersonalInfoForm afterSubmit={onSubmit}>{answerLaterOnProfile}</PersonalInfoForm>
)
}
// Fetch server-side data here
export const fetchFor = [...fetchForPersonalInfoForm]

View File

@ -6,7 +6,8 @@ import { AskWelcome } from "./AskWelcome"
import { AskDiscord, fetchFor as fetchForDiscord } from "./AskDiscord"
import { AskDayWishes, fetchFor as fetchForDayWishes } from "./AskDayWishes"
import { AskHosting, fetchFor as fetchForHosting } from "./AskHosting"
import { AskMeals, fetchFor as fetchForMeals } from "./AskMeals"
// import { AskMeals, fetchFor as fetchForMeals } from "./AskMeals"
import { AskPersonalInfo, fetchFor as fetchForPersonalInfo } from "./AskPersonalInfo"
import { AskTeamWishes, fetchFor as fetchForTeamWishes } from "./AskTeamWishes"
import {
AskParticipationDetails,
@ -24,8 +25,9 @@ const Asks = (): JSX.Element | null => {
AskDayWishes(asks, 10)
AskTeamWishes(asks, 11)
AskParticipationDetails(asks, 12)
AskPersonalInfo(asks, 15)
AskHosting(asks, 20)
AskMeals(asks, 22)
// AskMeals(asks, 22)
AskPushNotif(asks, 99)
@ -63,7 +65,8 @@ export const fetchFor = [
...fetchForDiscord,
...fetchForDayWishes,
...fetchForHosting,
...fetchForMeals,
// ...fetchForMeals,
...fetchForTeamWishes,
...fetchForParticipationDetails,
...fetchForPersonalInfo,
]

View File

@ -30,6 +30,7 @@
max-height: 80vh;
border-radius: 15px;
border: $border-large;
overflow-y: auto;
}
}

View File

@ -48,7 +48,6 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
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)
@ -107,7 +106,6 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
mobile,
howToContact,
canHelpBefore,
pelMember,
})
)
dispatch(
@ -623,54 +621,20 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
</>
)
const pelMemberQuestion = enableRegistering && !potentialVolunteer && (
const pelMember = enableRegistering && (
<>
<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.
l'association <i>Paris est Ludique !</i> qui s'en charge. Pour avoir un
droit de regard dessus, devenir bénévole à cette édition implique
automatiquement d'en devenir membre jusqu'à septembre prochain. Ça n'engage
à rien et c'est gratuit !
</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>
)}
</>
)
@ -803,14 +767,10 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
{cameAsVisitor}
{meeting}
{helpBefore}
{pelMemberQuestion}
{(potentialVolunteer || pelMember) && (
<>
{nameMobileEmail}
{!enableRegistering && commentQuestion}
{howToContact !== "Aucun" && submitButton}
</>
)}
{pelMember}
{nameMobileEmail}
{!enableRegistering && commentQuestion}
{howToContact !== "Aucun" && submitButton}
</>
)}
</form>

View File

@ -34,7 +34,7 @@ type VolunteerEmailProps = {
const VolunteerEmail: FC<VolunteerEmailProps> = withUserRole(ROLES.TEAMLEAD, ({ email }) => (
<td> {email}</td>
))
), null)
type DaysAvailabilityProps = {
volunteer: Volunteer

View File

@ -15,12 +15,17 @@ import { fetchFor as fetchForDayWishesForm } from "./DayWishesForm/DayWishesForm
import { fetchFor as fetchForHostingForm } from "./HostingForm/HostingForm"
import { fetchFor as fetchForMealsForm } from "./MealsForm/MealsForm"
import { fetchFor as fetchForParticipationDetailsForm } from "./ParticipationDetailsForm/ParticipationDetailsForm"
import { fetchFor as fetchForPersonalInfoForm } from "./PersonalInfoForm/PersonalInfoForm"
import { fetchFor as fetchForTeamWishesForm } from "./TeamWishesForm/TeamWishesForm"
import VolunteerTeam from "./VolunteerTeam/VolunteerTeam"
import PersonalInfo from "./PersonalInfo/PersonalInfo"
import PersonalInfoFormModal from "./PersonalInfoForm/PersonalInfoFormModal"
const Board: FC = (): JSX.Element => (
<>
<ContentTitle title="Profil spécifique au festival" />
<PersonalInfo />
<PersonalInfoFormModal />
<DayWishes />
<DayWishesFormModal />
<ParticipationDetails />
@ -42,5 +47,6 @@ export const fetchFor = [
...fetchForHostingForm,
...fetchForMealsForm,
...fetchForParticipationDetailsForm,
...fetchForPersonalInfoForm,
...fetchForTeamWishesForm,
]

View File

@ -1,9 +1,9 @@
import { FC, memo, useCallback } from "react"
import { FC, memo } from "react"
import { find, get } from "lodash"
import styles from "./styles.module.scss"
import { useUserMeals, mealDays, MealOption } from "../meals.utils"
import useAction from "../../../utils/useAction"
import { displayModal, MODAL_IDS } from "../../../store/ui"
// import useAction from "../../../utils/useAction"
// import { displayModal, MODAL_IDS } from "../../../store/ui"
import { useUserDayWishes } from "../daysWishes.utils"
const Meals: FC = (): JSX.Element | null => {
@ -11,8 +11,8 @@ const Meals: FC = (): JSX.Element | null => {
const [userWishes] = useUserDayWishes()
const meals = get(userMeals, "meals", [])
const dayWishesString = get(userWishes, "dayWishes", [])
const execDisplayModal = useAction(displayModal)
const onEdit = useCallback(() => execDisplayModal(MODAL_IDS.MEALS), [execDisplayModal])
// const execDisplayModal = useAction(displayModal)
// const onEdit = useCallback(() => execDisplayModal(MODAL_IDS.MEALS), [execDisplayModal])
const mealChoices = mealDays.map((meal, i: number) =>
find(meal.options, { abbr: meals[i] || "" })
) as MealOption[]
@ -53,10 +53,13 @@ const Meals: FC = (): JSX.Element | null => {
</div>
)}
<div className={styles.editButton} key="edit">
{/* <div className={styles.editButton} key="edit">
<button type="button" onClick={onEdit}>
Modifier
</button>
</div> */}
<div className={styles.editButton} key="edit">
Plus modifiable
</div>
</div>
)

View File

@ -46,11 +46,8 @@ const MealsForm: FC<Props> = ({ children, afterSubmit }): JSX.Element => {
<div className={styles.leftCol}>
<div className={styles.needsMealsTitle}>
<b>{mealDays[i].name}</b>
{i === 0 && <>, accompagné d'un délicieux brownie tout chocolat</>}
{i === 1 && (
<>, accompagné du même brownie. Enfin, un autre que celui de la veille</>
)}{" "}
:
{(i === 0 || i === 1) && <>, accompagné d'un délicieux brownie tout chocolat</>}
{i === 2 && <>, accompagné d'une part de tarte indéterminée</>} :
</div>
</div>
<div className={styles.rightCol}>
@ -89,6 +86,13 @@ const MealsForm: FC<Props> = ({ children, afterSubmit }): JSX.Element => {
return (
<div>
<div className={styles.title}>Mes repas</div>
<div className={classnames(styles.inputWrapper)}>
<div>
La composition exacte des repas ne sera pas connues avant le festival (risque de
changement d'ingrédient au dernier moment). Elle sera disponible au moment de
récupérer ton repas pour que tu puisses contrôler l'absence d'allergène.
</div>
</div>
{dayWishesString.includes("S") ? (
<>
{getBreakfeastElement("Samedi")}

View File

@ -0,0 +1,40 @@
import { FC, memo, useCallback } from "react"
import get from "lodash/get"
import styles from "./styles.module.scss"
import { useUserPersonalInfo } from "../personalInfo.utils"
import useAction from "../../../utils/useAction"
import { displayModal, MODAL_IDS } from "../../../store/ui"
const PersonalInfo: FC = (): JSX.Element | null => {
const [userWishes] = useUserPersonalInfo()
const firstname = get(userWishes, "firstname", "")
const lastname = get(userWishes, "lastname", "")
const photo = get(userWishes, "photo", "")
const execDisplayModal = useAction(displayModal)
const onEdit = useCallback(() => execDisplayModal(MODAL_IDS.PERSONALINFO), [execDisplayModal])
return (
<div className={styles.personalInfo}>
<div className={styles.title}>Mes infos personnelles</div>
<div className={styles.personalInfoLabel}>
{firstname || "Aucun prénom"} {lastname || "Aucun nom de famille"}
</div>
{/^[0-9]/.test(photo || "") && (
<div className={styles.personalInfoLabel}>Ta photo est bien renseignée merci !</div>
)}
{!/^[0-9]/.test(photo || "") && (
<div className={styles.personalInfoLabel}>Il faudrait renseigner ta photo !</div>
)}
<div className={styles.editButton}>
<button type="button" onClick={onEdit}>
Modifier
</button>
</div>
</div>
)
}
export default memo(PersonalInfo)

View File

@ -0,0 +1,53 @@
@import "../../../theme/variables";
@import "../../../theme/mixins";
.title {
padding-bottom: 10px;
font-weight: bold;
}
.personalInfoLabel {
margin-right: 5px;
font-style: bold;
}
.personalInfo {
@include inner-content-wrapper();
position: relative;
padding-right: 90px;
}
.personalInfoLabel,
.commentLine {
margin-bottom: 5px;
span {
display: inline-block;
}
}
.lineEmpty {
color: $color-red;
font-style: italic;
}
.commentLineTitle {
padding-right: 5px;
}
.commentLineText {
font-style: italic;
}
.editButton {
@include vertical-center();
position: absolute;
right: 20px;
button {
color: $color-green;
font-weight: bold;
cursor: pointer;
}
}

View File

@ -0,0 +1,155 @@
import { FC, memo, ReactNode, useCallback, useEffect, useRef, useState } from "react"
import classnames from "classnames"
import { get, set } from "lodash"
import styles from "./styles.module.scss"
import { useUserPersonalInfo } from "../personalInfo.utils"
import FormButton from "../../Form/FormButton/FormButton"
import { fetchVolunteerPersonalInfoSetIfNeed } from "../../../store/volunteerPersonalInfoSet"
import IgnoreButton from "../../Form/IgnoreButton/IgnoreButton"
type Props = {
children?: ReactNode | undefined
afterSubmit?: () => void | undefined
}
const PersonalInfoForm: FC<Props> = ({ children, afterSubmit }): JSX.Element => {
const firstnameRef = useRef<HTMLInputElement | null>(null)
const lastnameRef = useRef<HTMLInputElement | null>(null)
const [photo, setPhoto] = useState("")
const [selectedImage, setSelectedImage] = useState<File | null>(null)
const [userWishes, saveWishes] = useUserPersonalInfo()
useEffect(() => {
if (!userWishes) return
set(firstnameRef, "current.value", get(userWishes, "firstname", ""))
set(lastnameRef, "current.value", get(userWishes, "lastname", ""))
setPhoto(get(userWishes, "photo", ""))
}, [userWishes])
const onChoiceSubmit = useCallback(() => {
const firstname = get(firstnameRef, "current.value", "")
const lastname = get(lastnameRef, "current.value", "")
const reader = new FileReader()
reader.onload = async (event) => {
const photoData = event?.target?.result as string
if (!photoData) {
throw Error("Ce n'est pas une photo valide")
}
saveWishes(firstname, lastname, photoData)
if (afterSubmit) afterSubmit()
}
if (selectedImage) {
reader.readAsDataURL(selectedImage)
} else {
saveWishes(firstname, lastname, undefined)
if (afterSubmit) afterSubmit()
}
}, [selectedImage, saveWishes, afterSubmit])
return (
<div>
<div className={styles.title}>Mes infos personnelles</div>
<div className={styles.inputWrapper}>
<div className={styles.leftCol}>
<div className={styles.firstnameTitle}>Prénom :</div>
</div>
<div className={styles.rightCol}>
<input className={styles.firstnameLabel} type="text" ref={firstnameRef} />
</div>
</div>
<div className={styles.inputWrapper}>
<div className={styles.leftCol}>
<div className={styles.lastnameTitle}>Nom :</div>
</div>
<div className={styles.rightCol}>
<input className={styles.lastnameLabel} type="text" ref={lastnameRef} />
</div>
</div>
<div className={classnames(styles.inputWrapper, styles.noBottomMargin)}>
<div className={styles.leftCol}>
<div className={styles.photoTitle}>
Ta photo de profil pour le trombinoscope de l'association :
</div>
</div>
<div className={styles.rightCol}>
<label className={styles.photoLabel}>
{/^[0-9]/.test(photo || "") && !selectedImage && (
<div>
<img alt="actuelle" width="100px" src={`/photos/${photo}`} />
</div>
)}
{selectedImage && (
<div>
<img
alt="remplacement"
width="100px"
src={URL.createObjectURL(selectedImage)}
/>
</div>
)}
<br />
<input
type="file"
name="myImage"
onChange={(event) => {
console.log(event?.target?.files?.[0])
setSelectedImage(event?.target?.files?.[0] || null)
}}
/>
</label>
</div>
</div>
<div className={styles.inputWrapper}>
<div>
En tant que bénévole pour le festival, tu es automatiquement membre adhérent de
l'association Paris est Ludique ! pour avoir droit de regard sur son
fonctionnement. Aucune cotisation n'est demandée et aucun engagement autre
qu'être bénévole pendant le festival n'est nécessaire. Les statuts sont{" "}
<a
href="https://drive.google.com/file/d/1KJIJxZmDdJ_PBKJSh_W3yrHLzH0-awsE/view?usp=sharing"
target="_blank"
rel="noreferrer"
>
accessibles ici
</a>
.
</div>
</div>
<div className={styles.buttonWrapper}>
<FormButton onClick={onChoiceSubmit}>Enregistrer</FormButton>
{children === undefined && (
<>
{" "}
<FormButton onClick={afterSubmit} type="grey">
Annuler
</FormButton>{" "}
</>
)}
{children !== undefined && (
<>
{" "}
<IgnoreButton onClick={afterSubmit} text="Ignorer">
{children}
</IgnoreButton>{" "}
</>
)}
</div>
</div>
)
}
PersonalInfoForm.defaultProps = {
children: undefined,
afterSubmit: undefined,
}
export default memo(PersonalInfoForm)
// Fetch server-side data here
export const fetchFor = [fetchVolunteerPersonalInfoSetIfNeed]

View File

@ -0,0 +1,18 @@
import { FC, memo, useCallback } from "react"
import { hideModal, MODAL_IDS } from "../../../store/ui"
import Modal from "../../Modal/Modal"
import useAction from "../../../utils/useAction"
import PersonalInfoForm from "./PersonalInfoForm"
const PersonalInfoFormModal: FC = (): JSX.Element => {
const execHideModal = useAction(hideModal)
const afterFormSubmit = useCallback(() => execHideModal(), [execHideModal])
return (
<Modal modalId={MODAL_IDS.PERSONALINFO}>
<PersonalInfoForm afterSubmit={afterFormSubmit} />
</Modal>
)
}
export default memo(PersonalInfoFormModal)

View File

@ -0,0 +1,123 @@
@import "../../../theme/variables";
@import "../../../theme/mixins";
.title {
padding: 15px 0;
font-weight: bold;
text-align: center;
}
.inputWrapper {
margin: 25px 0;
@include desktop {
display: flex;
}
}
.noTopMargin {
margin-top: 0;
}
.noBottomMargin {
margin-bottom: 0;
}
.leftCol {
flex: 0 0 220px;
}
.rightCol {
width: 100%;
text-align: center;
}
.firstnameTitle,
.lastnameTitle {
display: inline-block;
width: 80px;
margin-bottom: 10px;
}
.firstnameLabel,
.lastnameLabel {
text-align: left;
display: inline-block;
margin-bottom: 10px;
width: 120px;
}
.photoTitle {
display: inline-block;
width: 180px;
margin-bottom: 10px;
}
.photoLabel {
text-align: left;
display: inline-block;
margin-bottom: 10px;
width: 120px;
}
.personalInfoTitle {
display: inline-block;
width: 320px;
margin-bottom: 10px;
}
.personalInfoList {
@include clear-ul-style;
display: inline-block;
width: 204px;
text-align: center;
}
.personalInfoItem {
display: inline-block;
margin: 3px;
}
.personalInfoButton {
margin: 0;
padding: 7px 2px 6px;
border: 0;
border-radius: 0;
width: 90px;
text-align: center;
color: $color-grey-dark;
background-color: $color-grey-light;
cursor: pointer;
&.active {
color: $color-yellow;
background-color: $color-black;
}
}
.personalInfoCommentWrapper {
margin: 6px 0 14px;
label {
display: block;
padding: 6px 0 2px 4px;
}
textarea {
width: 100%;
height: 50px;
padding: 5px;
border: 1px solid $color-grey-light;
background-color: $color-grey-lighter;
outline: 0;
}
}
.buttonWrapper {
margin-bottom: 10px;
text-align: center;
}
.yesMembership {
color: $color-orange;
}

View File

@ -0,0 +1,37 @@
import { shallowEqual, useSelector } from "react-redux"
import { useCallback } from "react"
import { selectUserJwtToken } from "../../store/auth"
import { AppState } from "../../store"
import { fetchVolunteerPersonalInfoSet } from "../../store/volunteerPersonalInfoSet"
import useAction from "../../utils/useAction"
import { VolunteerPersonalInfo } from "../../services/volunteers"
type SetFunction = (
firstname: VolunteerPersonalInfo["firstname"],
lastname: VolunteerPersonalInfo["lastname"],
photo: VolunteerPersonalInfo["photo"] | undefined
) => void
export const useUserPersonalInfo = (): [VolunteerPersonalInfo | undefined, SetFunction] => {
const save = useAction(fetchVolunteerPersonalInfoSet)
const jwtToken = useSelector(selectUserJwtToken)
const userWishes = useSelector(
(state: AppState) => state.volunteerPersonalInfoSet?.entity,
shallowEqual
)
const saveWishes = useCallback(
(firstname, lastname, photo) => {
if (!userWishes) return
save(jwtToken, 0, {
id: userWishes.id,
firstname,
lastname,
photo,
})
},
[userWishes, save, jwtToken]
)
return [userWishes, saveWishes]
}

View File

@ -1,3 +1,5 @@
import path from "path"
import * as fs from "fs"
import { assign, cloneDeep, max, omit, pick } from "lodash"
import bcrypt from "bcrypt"
import sgMail from "@sendgrid/mail"
@ -16,6 +18,7 @@ import {
VolunteerParticipationDetails,
VolunteerTeamAssign,
VolunteerKnowledge,
VolunteerPersonalInfo,
} from "../../services/volunteers"
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
import { getJwt } from "../secure"
@ -113,7 +116,6 @@ export const volunteerPartialAdd = expressAccessor.add(async (list, body) => {
mobile: canonicalMobile(params.mobile),
howToContact: trim(params.howToContact),
canHelpBefore: trim(params.canHelpBefore),
pelMember: params.pelMember === true,
password1: passwordHash,
password2: passwordHash,
})
@ -181,7 +183,7 @@ export const volunteerForgot = expressAccessor.set(async (list, bodyArray) => {
if (!volunteer) {
throw Error("Il n'y a aucun bénévole avec cet email")
}
const newVolunteer = cloneDeep(volunteer)
const newVolunteer: Volunteer = cloneDeep(volunteer)
const now = +new Date()
const timeSinceLastSent = now - lastForgot[volunteer.id]
@ -238,11 +240,11 @@ export const volunteerAsksSet = expressAccessor.set(async (list, body, id) => {
throw Error(`On ne peut acceder qu'à ses propres questions`)
}
const notifChanges = body[1]
const volunteer = list.find((v) => v.id === requestedId)
const volunteer: Volunteer | undefined = list.find((v) => v.id === requestedId)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
const newVolunteer = cloneDeep(volunteer)
const newVolunteer: Volunteer = cloneDeep(volunteer)
if (notifChanges.hiddenAsks !== undefined) newVolunteer.hiddenAsks = notifChanges.hiddenAsks
if (notifChanges.acceptsNotifs !== undefined)
@ -303,7 +305,7 @@ export const volunteerDayWishesSet = expressAccessor.set(async (list, body, id)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
const newVolunteer = cloneDeep(volunteer)
const newVolunteer: Volunteer = cloneDeep(volunteer)
if (wishes.active !== undefined) {
newVolunteer.active = wishes.active
@ -336,7 +338,7 @@ export const volunteerHostingSet = expressAccessor.set(async (list, body, id) =>
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
const newVolunteer = cloneDeep(volunteer)
const newVolunteer: Volunteer = cloneDeep(volunteer)
if (wishes.needsHosting !== undefined) {
newVolunteer.needsHosting = wishes.needsHosting
@ -363,6 +365,62 @@ export const volunteerHostingSet = expressAccessor.set(async (list, body, id) =>
}
})
export const volunteerPersonalInfoSet = expressAccessor.set(async (list, body, id) => {
const requestedId = +body[0] || id
if (requestedId !== id && requestedId !== 0) {
throw Error(`On ne peut acceder qu'à ses propres infos d'hébergement`)
}
const wishes = body[1] as VolunteerPersonalInfo
const volunteer: Volunteer | undefined = list.find((v) => v.id === requestedId)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
const newVolunteer: Volunteer = cloneDeep(volunteer)
if (wishes.firstname !== undefined) {
newVolunteer.firstname = wishes.firstname
}
if (wishes.lastname !== undefined) {
newVolunteer.lastname = wishes.lastname
}
if (wishes.photo !== undefined) {
const filename = setNewPhoto(
requestedId,
wishes.photo,
/^[0-9]/.test(volunteer.photo) ? volunteer.photo : undefined
)
newVolunteer.photo = filename
}
return {
toDatabase: newVolunteer,
toCaller: {
id: newVolunteer.id,
firstname: newVolunteer.firstname,
lastname: newVolunteer.lastname,
photo: newVolunteer.photo,
} as VolunteerPersonalInfo,
}
})
function setNewPhoto(id: number, photoData: string, prevFilename: string | undefined): string {
const matches = photoData.match(/^data:.+\/([a-z0-9]+);base64,(.*)$/)
if (!matches) {
throw Error("Not image data ><")
}
const ext = matches[1]
const base64Data = matches[2]
const buffer = Buffer.from(base64Data, "base64")
const filename = `${id}.${ext}`
const filePath = path.resolve(process.cwd(), `public/photos/${filename}`)
if (prevFilename) {
const prevFilePath = path.resolve(process.cwd(), `public/photos/${prevFilename}`)
fs.unlinkSync(prevFilePath)
}
fs.writeFileSync(filePath, buffer)
return filename
}
export const volunteerMealsSet = expressAccessor.set(async (list, body, id) => {
const requestedId = +body[0] || id
if (requestedId !== id && requestedId !== 0) {
@ -373,7 +431,7 @@ export const volunteerMealsSet = expressAccessor.set(async (list, body, id) => {
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
const newVolunteer = cloneDeep(volunteer)
const newVolunteer: Volunteer = cloneDeep(volunteer)
if (wishes.meals !== undefined) {
newVolunteer.meals = wishes.meals
@ -395,11 +453,11 @@ export const volunteerParticipationDetailsSet = expressAccessor.set(async (list,
)
}
const wishes = body[1] as VolunteerParticipationDetails
const volunteer = list.find((v) => v.id === requestedId)
const volunteer: Volunteer | undefined = list.find((v) => v.id === requestedId)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
const newVolunteer = cloneDeep(volunteer)
const newVolunteer: Volunteer = cloneDeep(volunteer)
if (wishes.tshirtSize !== undefined) {
newVolunteer.tshirtSize = wishes.tshirtSize
@ -432,11 +490,11 @@ export const volunteerTeamAssignSet = expressAccessor.set(async (list, body, _id
}
const teamAssign = body[1] as VolunteerTeamAssign
const volunteer = list.find((v) => v.id === teamAssign.volunteer)
const volunteer: Volunteer | undefined = list.find((v) => v.id === teamAssign.volunteer)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${teamAssign.volunteer}`)
}
const newVolunteer = cloneDeep(volunteer)
const newVolunteer: Volunteer = cloneDeep(volunteer)
newVolunteer.team = teamAssign.team
return {
@ -455,12 +513,12 @@ function getByEmail<T extends { email: string }>(list: T[], rawEmail: string): T
export const volunteerKnowledgeSet = expressAccessor.set(async (list, body, id) => {
const requestedId = +body[0] || id
const volunteer = list.find((v) => v.id === requestedId)
const volunteer: Volunteer | undefined = list.find((v) => v.id === requestedId)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
const knowledge = body[1] as VolunteerKnowledge
const newVolunteer = cloneDeep(volunteer)
const newVolunteer: Volunteer = cloneDeep(volunteer)
if (knowledge?.ok !== undefined) newVolunteer.ok = knowledge.ok
if (knowledge?.bof !== undefined) newVolunteer.bof = knowledge.bof
if (knowledge?.niet !== undefined) newVolunteer.niet = knowledge.niet

View File

@ -31,6 +31,7 @@ import {
volunteerDiscordId,
volunteerLogin,
volunteerParticipationDetailsSet,
volunteerPersonalInfoSet,
volunteerSet,
volunteerTeamWishesSet,
volunteerTeamAssignSet,
@ -52,13 +53,16 @@ notificationMain()
const app = express()
// Allow receiving big images
app.use(express.json({ limit: "200mb" }))
app.use(express.urlencoded({ limit: "200mb" }))
// Use helmet to secure Express with various HTTP headers
app.use(helmet({ contentSecurityPolicy: false }))
// Prevent HTTP parameter pollution
app.use(hpp())
// Compress all requests
app.use(compression())
// Https with certbot and Let's Encrypt
if (!__DEV__) {
app.use("/.well-known/acme-challenge", certbotRouter)
@ -114,6 +118,7 @@ app.post(
app.post("/VolunteerDayWishesSet", secure as RequestHandler, volunteerDayWishesSet)
app.post("/VolunteerHostingSet", secure as RequestHandler, volunteerHostingSet)
app.post("/VolunteerMealsSet", secure as RequestHandler, volunteerMealsSet)
app.post("/VolunteerPersonalInfoSet", secure as RequestHandler, volunteerPersonalInfoSet)
app.post("/VolunteerTeamWishesSet", secure as RequestHandler, volunteerTeamWishesSet)
app.post("/VolunteerTeamAssignSet", secure as RequestHandler, volunteerTeamAssignSet)

View File

@ -2,10 +2,10 @@
export class Volunteer implements VolunteerPartial {
id = 0
lastname = ""
firstname = ""
lastname = ""
email = ""
mobile = ""
@ -40,8 +40,6 @@ export class Volunteer implements VolunteerPartial {
canHelpBefore = ""
pelMember = false
hiddenAsks: number[] = []
created = new Date()
@ -73,8 +71,8 @@ export class Volunteer implements VolunteerPartial {
export const translationVolunteer: { [k in keyof Volunteer]: string } = {
id: "id",
lastname: "nom",
firstname: "prenom",
lastname: "nom",
email: "mail",
mobile: "telephone",
photo: "photo",
@ -92,7 +90,6 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
teamWishesComment: "commentaireEnviesEquipe",
howToContact: "commentContacter",
canHelpBefore: "aideEnAmont",
pelMember: "membrePel",
hiddenAsks: "questionsCachees",
created: "creation",
password1: "passe1",
@ -110,10 +107,10 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
}
export class VolunteerPartial {
lastname = ""
firstname = ""
lastname = ""
email = ""
mobile = ""
@ -142,7 +139,6 @@ export const volunteerExample: Volunteer = {
teamWishesComment: "",
howToContact: "",
canHelpBefore: "",
pelMember: false,
hiddenAsks: [],
created: new Date(0),
password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
@ -222,6 +218,13 @@ export interface VolunteerParticipationDetails {
food: Volunteer["food"]
}
export interface VolunteerPersonalInfo {
id: Volunteer["id"]
firstname: Volunteer["firstname"]
lastname: Volunteer["lastname"]
photo: string
}
export interface VolunteerTeamAssign {
id: Volunteer["id"]
volunteer: number

View File

@ -12,6 +12,7 @@ import {
VolunteerDiscordId,
VolunteerKnowledge,
VolunteerMeals,
VolunteerPersonalInfo,
} from "./volunteers"
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
@ -51,6 +52,9 @@ export const volunteerParticipationDetailsSet =
"ParticipationDetailsSet"
)
export const volunteerPersonalInfoSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerPersonalInfo>]>("PersonalInfoSet")
export const volunteerTeamAssignSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerTeamAssign>]>("TeamAssignSet")

View File

@ -22,6 +22,7 @@ import volunteerList from "./volunteerList"
import volunteerLogin from "./volunteerLogin"
import volunteerKnowledgeSet from "./volunteerKnowledgeSet"
import volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
import volunteerPersonalInfoSet from "./volunteerPersonalInfoSet"
import volunteerSet from "./volunteerSet"
import volunteerTeamAssignSet from "./volunteerTeamAssignSet"
import volunteerTeamWishesSet from "./volunteerTeamWishesSet"
@ -52,6 +53,7 @@ export default (history: History) => ({
volunteerLogin,
volunteerKnowledgeSet,
volunteerParticipationDetailsSet,
volunteerPersonalInfoSet,
volunteerSet,
volunteerTeamAssignSet,
volunteerTeamWishesSet,

View File

@ -31,5 +31,6 @@ export const MODAL_IDS = {
HOSTING: "HOSTING",
MEALS: "MEALS",
PARTICIPATIONDETAILS: "PARTICIPATIONDETAILS",
PERSONALINFO: "PERSONALINFO",
TEAMWISHES: "TEAMWISHES",
}

View File

@ -0,0 +1,60 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { StateRequest, toastError, elementFetch } from "./utils"
import { VolunteerPersonalInfo } from "../services/volunteers"
import { AppThunk, AppState } from "."
import { volunteerPersonalInfoSet } from "../services/volunteersAccessors"
type StateVolunteerPersonalInfoSet = { entity?: VolunteerPersonalInfo } & StateRequest
export const initialState: StateVolunteerPersonalInfoSet = {
readyStatus: "idle",
}
const volunteerPersonalInfoSetSlice = createSlice({
name: "volunteerPersonalInfoSet",
initialState,
reducers: {
getRequesting: (_) => ({
readyStatus: "request",
}),
getSuccess: (_, { payload }: PayloadAction<VolunteerPersonalInfo>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
readyStatus: "failure",
error: payload,
}),
},
})
export default volunteerPersonalInfoSetSlice.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerPersonalInfoSetSlice.actions
export const fetchVolunteerPersonalInfoSet = elementFetch(
volunteerPersonalInfoSet,
getRequesting,
getSuccess,
getFailure,
(error: Error) =>
toastError(`Erreur lors du chargement des choix de jours de présence: ${error.message}`)
)
const shouldFetchVolunteerPersonalInfoSet = (state: AppState, id: number) =>
state.volunteerPersonalInfoSet?.readyStatus !== "success" ||
(state.volunteerPersonalInfoSet?.entity && state.volunteerPersonalInfoSet?.entity?.id !== id)
export const fetchVolunteerPersonalInfoSetIfNeed =
(id = 0, wishes: Partial<VolunteerPersonalInfo> = {}): AppThunk =>
(dispatch, getState) => {
let jwt = ""
if (!id) {
;({ jwt, id } = getState().auth)
}
if (shouldFetchVolunteerPersonalInfoSet(getState(), id))
return dispatch(fetchVolunteerPersonalInfoSet(jwt, id, wishes))
return null
}

View File

@ -2,13 +2,17 @@ import React from "react"
import { useSelector } from "react-redux"
import { selectUserRoles } from "../store/auth"
function withUserRole<T>(requiredRole: string, Component: React.ComponentType<T>) {
return (props: T): JSX.Element => {
function withUserRole<T>(
requiredRole: string,
Component: React.ComponentType<T>,
doShowMissingRole: true | null = true
) {
return (props: T): JSX.Element | null => {
const roles = useSelector(selectUserRoles)
return roles.includes(requiredRole) ? (
<Component {...props} />
) : (
<div>Missing role {requiredRole}</div>
doShowMissingRole && <div>Missing role {requiredRole}</div>
)
}
}

View File

@ -2907,6 +2907,24 @@ body-parser@1.19.0:
raw-body "2.4.0"
type-is "~1.6.17"
body-parser@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5"
integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==
dependencies:
bytes "3.1.2"
content-type "~1.0.4"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.10.3"
raw-body "2.5.1"
type-is "~1.6.18"
unpipe "1.0.0"
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@ -3022,6 +3040,11 @@ bytes@3.1.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
cacheable-request@^2.1.1:
version "2.1.4"
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d"
@ -3933,15 +3956,20 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
depd@2.0.0, depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
depd@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
destroy@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
destroy@~1.0.4:
version "1.0.4"
@ -5588,6 +5616,17 @@ http-errors@1.7.2:
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
dependencies:
depd "2.0.0"
inherits "2.0.4"
setprototypeof "1.2.0"
statuses "2.0.1"
toidentifier "1.0.1"
http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
@ -7772,6 +7811,13 @@ object.values@^1.1.5:
define-properties "^1.1.3"
es-abstract "^1.19.1"
on-finished@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
dependencies:
ee-first "1.1.1"
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@ -8640,6 +8686,13 @@ pupa@^2.1.1:
dependencies:
escape-goat "^2.0.0"
qs@6.10.3:
version "6.10.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
dependencies:
side-channel "^1.0.4"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@ -8698,6 +8751,16 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-body@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
rc@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@ -9416,6 +9479,11 @@ setprototypeof@1.1.1:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
@ -9632,6 +9700,11 @@ stackframe@^1.1.1:
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303"
integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
@ -10149,6 +10222,11 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
toidentifier@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
totalist@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"