diff --git a/.eslintignore b/.eslintignore index 157c89f..d70ebaa 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1 @@ -public -announces/* \ No newline at end of file +public \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0ec8880..b1644c7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ access/* *.log .idea announces/* -/styles.module.css # VS Code stuff .vscode/settings.json diff --git a/.prettierignore b/.prettierignore index d5651d6..e666141 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,4 +9,3 @@ public/* *.log node_modules/* access/* -announces/* diff --git a/package.json b/package.json index c2494c4..3b41ed0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Asks/AskPersonalInfo.tsx b/src/components/Asks/AskPersonalInfo.tsx new file mode 100644 index 0000000..9498e48 --- /dev/null +++ b/src/components/Asks/AskPersonalInfo.tsx @@ -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, + {answerLaterOnProfile} + ) +} + +// Fetch server-side data here +export const fetchFor = [...fetchForPersonalInfoForm] diff --git a/src/components/Asks/index.tsx b/src/components/Asks/index.tsx index 7aad7b0..79acb2d 100644 --- a/src/components/Asks/index.tsx +++ b/src/components/Asks/index.tsx @@ -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, ] diff --git a/src/components/Modal/styles.module.scss b/src/components/Modal/styles.module.scss index 0dd7a0c..9adae36 100755 --- a/src/components/Modal/styles.module.scss +++ b/src/components/Modal/styles.module.scss @@ -30,6 +30,7 @@ max-height: 80vh; border-radius: 15px; border: $border-large; + overflow-y: auto; } } diff --git a/src/components/RegisterForm/index.tsx b/src/components/RegisterForm/index.tsx index 5bcb36a..1e5095d 100644 --- a/src/components/RegisterForm/index.tsx +++ b/src/components/RegisterForm/index.tsx @@ -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 && ( <>
Association Paris est Ludique

Légalement il faut que le festival soit organisé par une structure, et c'est - l'association Paris est Ludique ! 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 Paris est Ludique ! 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 !

-
-
-
- Acceptes-tu de devenir membre de l'association Paris est Ludique ! ? -
-
-
-
- {["Oui", "Non"].map((option) => ( - - ))} -
-
-
- {!pelMember && ( -
-
-

- Tant que tu n'as pas accepté cette condition je suis désolé on ne peut - pas continuer. -

-
-
- )} ) @@ -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} )} diff --git a/src/components/TeamMembers/TeamMembers.tsx b/src/components/TeamMembers/TeamMembers.tsx index 4a150c1..e133cfb 100644 --- a/src/components/TeamMembers/TeamMembers.tsx +++ b/src/components/TeamMembers/TeamMembers.tsx @@ -34,7 +34,7 @@ type VolunteerEmailProps = { const VolunteerEmail: FC = withUserRole(ROLES.TEAMLEAD, ({ email }) => ( {email} -)) +), null) type DaysAvailabilityProps = { volunteer: Volunteer diff --git a/src/components/VolunteerBoard/Board.tsx b/src/components/VolunteerBoard/Board.tsx index 678c246..a33ca43 100644 --- a/src/components/VolunteerBoard/Board.tsx +++ b/src/components/VolunteerBoard/Board.tsx @@ -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 => ( <> + + @@ -42,5 +47,6 @@ export const fetchFor = [ ...fetchForHostingForm, ...fetchForMealsForm, ...fetchForParticipationDetailsForm, + ...fetchForPersonalInfoForm, ...fetchForTeamWishesForm, ] diff --git a/src/components/VolunteerBoard/Meals/Meals.tsx b/src/components/VolunteerBoard/Meals/Meals.tsx index fd6e155..210fcf5 100644 --- a/src/components/VolunteerBoard/Meals/Meals.tsx +++ b/src/components/VolunteerBoard/Meals/Meals.tsx @@ -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 => { )} -
+ {/*
+
*/} +
+ Plus modifiable
) diff --git a/src/components/VolunteerBoard/MealsForm/MealsForm.tsx b/src/components/VolunteerBoard/MealsForm/MealsForm.tsx index 122d941..dff4477 100644 --- a/src/components/VolunteerBoard/MealsForm/MealsForm.tsx +++ b/src/components/VolunteerBoard/MealsForm/MealsForm.tsx @@ -46,11 +46,8 @@ const MealsForm: FC = ({ children, afterSubmit }): JSX.Element => {
{mealDays[i].name} - {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} :
@@ -89,6 +86,13 @@ const MealsForm: FC = ({ children, afterSubmit }): JSX.Element => { return (
Mes repas
+
+
+ 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. +
+
{dayWishesString.includes("S") ? ( <> {getBreakfeastElement("Samedi")} diff --git a/src/components/VolunteerBoard/PersonalInfo/PersonalInfo.tsx b/src/components/VolunteerBoard/PersonalInfo/PersonalInfo.tsx new file mode 100644 index 0000000..15a92fc --- /dev/null +++ b/src/components/VolunteerBoard/PersonalInfo/PersonalInfo.tsx @@ -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 ( +
+
Mes infos personnelles
+
+ {firstname || "Aucun prénom"} {lastname || "Aucun nom de famille"} +
+ + {/^[0-9]/.test(photo || "") && ( +
Ta photo est bien renseignée merci !
+ )} + + {!/^[0-9]/.test(photo || "") && ( +
Il faudrait renseigner ta photo !
+ )} + +
+ +
+
+ ) +} + +export default memo(PersonalInfo) diff --git a/src/components/VolunteerBoard/PersonalInfo/styles.module.scss b/src/components/VolunteerBoard/PersonalInfo/styles.module.scss new file mode 100755 index 0000000..7bf852d --- /dev/null +++ b/src/components/VolunteerBoard/PersonalInfo/styles.module.scss @@ -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; + } +} diff --git a/src/components/VolunteerBoard/PersonalInfoForm/PersonalInfoForm.tsx b/src/components/VolunteerBoard/PersonalInfoForm/PersonalInfoForm.tsx new file mode 100644 index 0000000..4af8957 --- /dev/null +++ b/src/components/VolunteerBoard/PersonalInfoForm/PersonalInfoForm.tsx @@ -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 = ({ children, afterSubmit }): JSX.Element => { + const firstnameRef = useRef(null) + const lastnameRef = useRef(null) + const [photo, setPhoto] = useState("") + const [selectedImage, setSelectedImage] = useState(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 ( +
+
Mes infos personnelles
+ +
+
+
Prénom :
+
+
+ +
+
+ +
+
+
Nom :
+
+
+ +
+
+ +
+
+
+ Ta photo de profil pour le trombinoscope de l'association : +
+
+
+ +
+
+ +
+
+ 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{" "} + + accessibles ici + + . +
+
+ +
+ Enregistrer + {children === undefined && ( + <> + {" "} + + Annuler + {" "} + + )} + {children !== undefined && ( + <> + {" "} + + {children} + {" "} + + )} +
+
+ ) +} + +PersonalInfoForm.defaultProps = { + children: undefined, + afterSubmit: undefined, +} + +export default memo(PersonalInfoForm) + +// Fetch server-side data here +export const fetchFor = [fetchVolunteerPersonalInfoSetIfNeed] diff --git a/src/components/VolunteerBoard/PersonalInfoForm/PersonalInfoFormModal.tsx b/src/components/VolunteerBoard/PersonalInfoForm/PersonalInfoFormModal.tsx new file mode 100644 index 0000000..b9fd074 --- /dev/null +++ b/src/components/VolunteerBoard/PersonalInfoForm/PersonalInfoFormModal.tsx @@ -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 ( + + + + ) +} + +export default memo(PersonalInfoFormModal) diff --git a/src/components/VolunteerBoard/PersonalInfoForm/styles.module.scss b/src/components/VolunteerBoard/PersonalInfoForm/styles.module.scss new file mode 100755 index 0000000..8cbdc37 --- /dev/null +++ b/src/components/VolunteerBoard/PersonalInfoForm/styles.module.scss @@ -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; +} diff --git a/src/components/VolunteerBoard/personalInfo.utils.ts b/src/components/VolunteerBoard/personalInfo.utils.ts new file mode 100644 index 0000000..629dcb4 --- /dev/null +++ b/src/components/VolunteerBoard/personalInfo.utils.ts @@ -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] +} diff --git a/src/server/gsheets/volunteers.ts b/src/server/gsheets/volunteers.ts index 1ca81da..3eceb07 100644 --- a/src/server/gsheets/volunteers.ts +++ b/src/server/gsheets/volunteers.ts @@ -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(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 diff --git a/src/server/index.ts b/src/server/index.ts index 32c1274..ba42158 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -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) diff --git a/src/services/volunteers.ts b/src/services/volunteers.ts index b6f7511..0154ea4 100644 --- a/src/services/volunteers.ts +++ b/src/services/volunteers.ts @@ -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 diff --git a/src/services/volunteersAccessors.ts b/src/services/volunteersAccessors.ts index 40a963e..f1904be 100644 --- a/src/services/volunteersAccessors.ts +++ b/src/services/volunteersAccessors.ts @@ -12,6 +12,7 @@ import { VolunteerDiscordId, VolunteerKnowledge, VolunteerMeals, + VolunteerPersonalInfo, } from "./volunteers" const serviceAccessors = new ServiceAccessors(elementName) @@ -51,6 +52,9 @@ export const volunteerParticipationDetailsSet = "ParticipationDetailsSet" ) +export const volunteerPersonalInfoSet = + serviceAccessors.securedCustomPost<[number, Partial]>("PersonalInfoSet") + export const volunteerTeamAssignSet = serviceAccessors.securedCustomPost<[number, Partial]>("TeamAssignSet") diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 7642271..79f0fe4 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -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, diff --git a/src/store/ui.ts b/src/store/ui.ts index 2de7700..a951e8c 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -31,5 +31,6 @@ export const MODAL_IDS = { HOSTING: "HOSTING", MEALS: "MEALS", PARTICIPATIONDETAILS: "PARTICIPATIONDETAILS", + PERSONALINFO: "PERSONALINFO", TEAMWISHES: "TEAMWISHES", } diff --git a/src/store/volunteerPersonalInfoSet.ts b/src/store/volunteerPersonalInfoSet.ts new file mode 100644 index 0000000..a0ff054 --- /dev/null +++ b/src/store/volunteerPersonalInfoSet.ts @@ -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) => ({ + readyStatus: "success", + entity: payload, + }), + getFailure: (_, { payload }: PayloadAction) => ({ + 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 = {}): AppThunk => + (dispatch, getState) => { + let jwt = "" + + if (!id) { + ;({ jwt, id } = getState().auth) + } + if (shouldFetchVolunteerPersonalInfoSet(getState(), id)) + return dispatch(fetchVolunteerPersonalInfoSet(jwt, id, wishes)) + + return null + } diff --git a/src/utils/withUserRole.tsx b/src/utils/withUserRole.tsx index 7e43068..813d860 100644 --- a/src/utils/withUserRole.tsx +++ b/src/utils/withUserRole.tsx @@ -2,13 +2,17 @@ import React from "react" import { useSelector } from "react-redux" import { selectUserRoles } from "../store/auth" -function withUserRole(requiredRole: string, Component: React.ComponentType) { - return (props: T): JSX.Element => { +function withUserRole( + requiredRole: string, + Component: React.ComponentType, + doShowMissingRole: true | null = true +) { + return (props: T): JSX.Element | null => { const roles = useSelector(selectUserRoles) return roles.includes(requiredRole) ? ( ) : ( -
Missing role {requiredRole}
+ doShowMissingRole &&
Missing role {requiredRole}
) } } diff --git a/yarn.lock b/yarn.lock index 9321b01..953adde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"