Refactor Notifications into Asks, replace all <Link> by <a>

This commit is contained in:
pikiou 2022-03-29 06:31:03 +02:00
parent 70b53c5af8
commit 4273ea1603
25 changed files with 636 additions and 663 deletions

View File

@ -1,4 +1,3 @@
import { Link } from "react-router-dom"
import { RouteConfig, renderRoutes } from "react-router-config"
import { Helmet } from "react-helmet"
import { ToastContainer } from "react-toastify"
@ -29,7 +28,7 @@ const App = ({ route }: Route): JSX.Element => (
<div className={styles.logo} />
<div>
<h1 className={styles.siteName}>
<Link to="/">{config.APP.title}</Link>
<a href="/">{config.APP.title}</a>
</h1>
<div className={styles.siteDescription}>{config.APP.description}</div>
</div>

View File

@ -0,0 +1,38 @@
import { get } from "lodash"
import { useCallback } from "react"
import { fetchVolunteerAsksSet } from "../../store/volunteerAsksSet"
import { useAskTools, addAsk, answerLaterOnProfile } from "./utils"
import { useUserDayWishes } from "../VolunteerBoard/daysWishes.utils"
import DayWishesForm, {
fetchFor as fetchForDayWishesForm,
} from "../VolunteerBoard/DayWishesForm/DayWishesForm"
export function AskDayWishes(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 [userWishes] = useUserDayWishes()
const participation = get(userWishes, "active", "inconnu")
const newSelection = get(userWishes, "dayWishes", [])
const comment = get(userWishes, "dayWishesComment", "")
const needToShow = participation === "inconnu" || (newSelection.length === 0 && !comment)
addAsk(
asks,
id,
volunteerAsks,
false,
needToShow,
<DayWishesForm afterSubmit={onSubmit}>{answerLaterOnProfile}</DayWishesForm>
)
}
// Fetch server-side data here
export const fetchFor = [...fetchForDayWishesForm]

View File

@ -0,0 +1,41 @@
import { useCallback } from "react"
import classnames from "classnames"
import { fetchVolunteerAsksSet } from "../../store/volunteerAsksSet"
import styles from "./styles.module.scss"
import { useAskTools, addAsk } from "./utils"
import FormButton from "../Form/FormButton/FormButton"
export function AskWelcome(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])
addAsk(
asks,
id,
volunteerAsks,
true,
true,
<form>
<div className={classnames(styles.notifIntro, styles.notifCentered)} key="login-intro">
La{" "}
<a
href="https://mailchi.mp/3c75c3b3a20f/gazette_2020_02-8978118"
onClick={onSubmit}
>
gazette de février
</a>{" "}
est disponible !<br />
<div className={styles.formButtons}>
<FormButton onClick={onSubmit}>Ok, masquer</FormButton>
</div>
</div>
</form>
)
}

View File

@ -0,0 +1,39 @@
import { get } from "lodash"
import { useCallback } from "react"
import { fetchVolunteerAsksSet } from "../../store/volunteerAsksSet"
import { useAskTools, addAsk, answerLaterOnProfile } from "./utils"
import ParticipationDetailsForm, {
fetchFor as fetchForParticipationDetailsForm,
} from "../VolunteerBoard/ParticipationDetailsForm/ParticipationDetailsForm"
import { useUserParticipationDetails } from "../VolunteerBoard/participationDetails.utils"
export function AskParticipationDetails(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 [participationDetails] = useUserParticipationDetails()
const tshirtSize = get(participationDetails, "tshirtSize", "")
const food = get(participationDetails, "food", "")
const needToShow = !tshirtSize && !food
addAsk(
asks,
id,
volunteerAsks,
false,
needToShow,
<ParticipationDetailsForm afterSubmit={onSubmit}>
{answerLaterOnProfile}
</ParticipationDetailsForm>
)
}
// Fetch server-side data here
export const fetchFor = [...fetchForParticipationDetailsForm]

View File

@ -0,0 +1,229 @@
import _ from "lodash"
import React, { useCallback, useEffect, useRef, useState } from "react"
import isNode from "detect-node"
import { fetchVolunteerAsksSet } from "../../store/volunteerAsksSet"
import styles from "./styles.module.scss"
import { useAskTools, addAsk } from "./utils"
import FormButton from "../Form/FormButton/FormButton"
import { toastError, toastSuccess } from "../../store/utils"
export function AskPushNotif(asks: JSX.Element[], id: number): void {
const { dispatch, jwtToken, volunteerAsks } = useAskTools()
const [acceptsNotifs, setAcceptsNotifs] = useState("")
const mounted = useRef(false)
useEffect(() => {
if (mounted.current) {
return
}
mounted.current = true
if (!isNode) {
if (volunteerAsks?.acceptsNotifs === "oui") {
navigator.serviceWorker.ready.then((registration) =>
registration.pushManager.getSubscription().then((existedSubscription) => {
const doesAcceptNotifs =
_.isEqual(
JSON.parse(JSON.stringify(existedSubscription)),
JSON.parse(volunteerAsks?.pushNotifSubscription)
) && volunteerAsks?.acceptsNotifs === "oui"
setAcceptsNotifs(doesAcceptNotifs ? "oui" : "non")
})
)
} else {
setAcceptsNotifs("non")
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onChangeValue = (e: React.ChangeEvent<HTMLInputElement>) =>
setAcceptsNotifs(e.target.value)
const onSubmit = useCallback(async (): Promise<void> => {
if (isNode) {
return
}
if (!("serviceWorker" in navigator)) {
return
}
if (acceptsNotifs === "non") {
dispatch(
fetchVolunteerAsksSet(jwtToken, 0, {
hiddenAsks: [...(volunteerAsks?.hiddenAsks || []), id],
acceptsNotifs: "non",
})
)
return
}
const registration = await navigator.serviceWorker.ready
if (!registration.pushManager) {
toastError(
"Il y a un problème avec le push manager. Il faudrait utiliser un navigateur plus récent !"
)
dispatch(
fetchVolunteerAsksSet(jwtToken, 0, {
hiddenAsks: [...(volunteerAsks?.hiddenAsks || []), id],
})
)
return
}
const convertedVapidKey = urlBase64ToUint8Array(process.env.FORCE_ORANGE_PUBLIC_VAPID_KEY)
function urlBase64ToUint8Array(base64String?: string) {
if (!base64String) {
return ""
}
const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
// eslint-disable-next-line
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/")
const rawData = atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; i += 1) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}
if (!convertedVapidKey) {
toastError("No convertedVapidKey available")
}
try {
const existedSubscription = await registration.pushManager.getSubscription()
if (existedSubscription === null) {
// No subscription detected, make a request
try {
const newSubscription = await registration.pushManager.subscribe({
applicationServerKey: convertedVapidKey,
userVisibleOnly: true,
})
// New subscription added
if (
volunteerAsks?.acceptsNotifs === "oui" &&
!subscriptionEqualsSave(
newSubscription,
volunteerAsks?.pushNotifSubscription
)
) {
toastSuccess(
"Un autre navigateur était notifié, mais c'est maintenant celui-ci qui le sera."
)
}
dispatch(
fetchVolunteerAsksSet(jwtToken, 0, {
pushNotifSubscription: JSON.stringify(newSubscription),
acceptsNotifs: "oui",
hiddenAsks: [...(volunteerAsks?.hiddenAsks || []), id],
})
)
} catch (_e) {
if (Notification.permission !== "granted") {
toastError(
"Mince tu as bloqué les notifications pour le site des bénévoles ! En haut juste à gauche de la barre d'adresse, il y a une icone de cadenas ou de message barré sur lequel cliquer pour annuler ce blocage.",
false
)
} else {
toastError(
"Il y a eu une erreur avec l'enregistrement avec le Service Worker. Il faudrait utiliser un navigateur plus récent !"
)
}
}
} else {
// Existed subscription detected
if (
volunteerAsks?.acceptsNotifs === "oui" &&
!subscriptionEqualsSave(
existedSubscription,
volunteerAsks?.pushNotifSubscription
)
) {
toastSuccess(
"Un autre navigateur était notifié, mais c'est maintenant celui-ci qui le sera."
)
}
dispatch(
fetchVolunteerAsksSet(jwtToken, 0, {
pushNotifSubscription: JSON.stringify(existedSubscription),
acceptsNotifs: "oui",
hiddenAsks: [...(volunteerAsks?.hiddenAsks || []), id],
})
)
}
} catch (_e) {
toastError(
"Il y a eu une erreur avec l'enregistrement avec le Service Worker. Il faudrait utiliser un navigateur plus récent !"
)
}
}, [
acceptsNotifs,
dispatch,
id,
jwtToken,
volunteerAsks?.acceptsNotifs,
volunteerAsks?.hiddenAsks,
volunteerAsks?.pushNotifSubscription,
])
function subscriptionEqualsSave(toCheck: PushSubscription, save: string | undefined): boolean {
if (!save) {
return !toCheck
}
return _.isEqual(JSON.parse(JSON.stringify(toCheck)), JSON.parse(save))
}
const needToShow =
volunteerAsks?.acceptsNotifs !== "oui" && volunteerAsks?.acceptsNotifs !== "non"
addAsk(
asks,
id,
volunteerAsks,
true,
needToShow,
<div className={styles.formLine} key="line-participation">
<label>
Acceptes-tu de recevoir une alerte dans ton navigateur quand on en aura
d&apos;autres à t'afficher ici ?<br />
<span className={styles.sousMessage}>
(Ça nous simplifierait la vie, on a des soucis à contacter les bénévoles par
email.)
</span>
<label>
<input
type="radio"
value="oui"
name="notifs"
checked={acceptsNotifs === "oui"}
onChange={onChangeValue}
/>{" "}
Oui
</label>
<label>
<input
type="radio"
value="non"
name="notifs"
checked={acceptsNotifs === "non"}
onChange={onChangeValue}
/>{" "}
Non
</label>
</label>
<div className={styles.formButtons}>
<FormButton onClick={onSubmit}>Enregistrer</FormButton>
</div>
</div>
)
}

View File

@ -0,0 +1,37 @@
import { get } from "lodash"
import { useCallback } from "react"
import { fetchVolunteerAsksSet } from "../../store/volunteerAsksSet"
import { useAskTools, addAsk, answerLaterOnProfile } from "./utils"
import { useUserTeamWishes } from "../VolunteerBoard/teamWishes.utils"
import TeamWishesForm, {
fetchFor as fetchForTeamWishesForm,
} from "../VolunteerBoard/TeamWishesForm/TeamWishesForm"
export function AskTeamWishes(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 [teamWishesData] = useUserTeamWishes()
const teamWishesString = get(teamWishesData, "teamWishes", [])
const comment = get(teamWishesData, "teamWishesComment", "")
const needToShow = teamWishesString.length === 0 && !comment
addAsk(
asks,
id,
volunteerAsks,
false,
needToShow,
<TeamWishesForm afterSubmit={onSubmit}>{answerLaterOnProfile}</TeamWishesForm>
)
}
// Fetch server-side data here
export const fetchFor = [...fetchForTeamWishesForm]

View File

@ -0,0 +1,35 @@
import { useCallback } from "react"
import { fetchVolunteerAsksSet } from "../../store/volunteerAsksSet"
import styles from "./styles.module.scss"
import { useAskTools, addAsk } from "./utils"
import FormButton from "../Form/FormButton/FormButton"
export function AskWelcome(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])
addAsk(
asks,
id,
volunteerAsks,
true,
true,
<form>
Salut {volunteerAsks?.firstname} !
<div className={styles.notifIntro} key="login-intro">
Ici tu seras notifié(e) des nouvelles importantes et des questions pour lesquelles
il nous faudrait absolument ta réponse.
<div className={styles.formButtons}>
<FormButton onClick={onSubmit}>Ok, continuer</FormButton>
</div>
</div>
</form>
)
}

View File

@ -0,0 +1,60 @@
import _ from "lodash"
import React, { memo } from "react"
import styles from "./styles.module.scss"
import { useAskTools } from "./utils"
import { AskWelcome } from "./AskWelcome"
import { AskPushNotif } from "./AskPushNotif"
import { AskDayWishes, fetchFor as fetchForDayWishes } from "./AskDayWishes"
import { AskTeamWishes, fetchFor as fetchForTeamWishes } from "./AskTeamWishes"
import {
AskParticipationDetails,
fetchFor as fetchForParticipationDetails,
} from "./AskParticipationDetails"
const Asks = (): JSX.Element | null => {
const { volunteerAsks } = useAskTools()
const asks: JSX.Element[] = []
AskWelcome(asks, 1)
AskDayWishes(asks, 10)
AskTeamWishes(asks, 11)
AskParticipationDetails(asks, 12)
AskPushNotif(asks, 99)
if (_.isEmpty(asks)) {
asks.push(
<div key="pushNotifs">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<div className={styles.formLine} key="line-participation">
<label>
Tu as fait le tour des dernières infos ou questions importantes,
merci ! :)
<br />
Nous te préviendrons quand il y en aura de nouvelles.
<br />
</label>
</div>
</div>
</div>
</div>
)
}
if (volunteerAsks === undefined) {
return null
}
return <div>{asks.map<React.ReactNode>((t) => t).reduce((prev, curr) => [prev, curr])}</div>
}
export default memo(Asks)
// Fetch server-side data here
export const fetchFor = [
...fetchForDayWishes,
...fetchForTeamWishes,
...fetchForParticipationDetails,
]

View File

@ -0,0 +1,63 @@
import _ from "lodash"
import { Dispatch } from "react"
import { shallowEqual, useDispatch, useSelector } from "react-redux"
import { VolunteerAsks } from "../../services/volunteers"
import { AppState } from "../../store"
import { selectUserJwtToken } from "../../store/auth"
import styles from "./styles.module.scss"
let prevVolunteerAsks: VolunteerAsks | undefined
type AskTools = {
dispatch: Dispatch<any>
jwtToken: string
volunteerAsks: VolunteerAsks | undefined
}
export function useAskTools(): AskTools {
const dispatch = useDispatch()
const jwtToken = useSelector(selectUserJwtToken)
const volunteerAsks = useSelector((state: AppState) => {
const vAsks = state.volunteerAsksSet?.entity
if (vAsks) {
prevVolunteerAsks = vAsks
return vAsks
}
return prevVolunteerAsks
}, shallowEqual)
return { dispatch, jwtToken, volunteerAsks }
}
export function addAsk(
asks: JSX.Element[],
id: number,
volunteerAsks: VolunteerAsks | undefined,
isNarrow: boolean,
needToShow: boolean,
children: JSX.Element
): void {
const hidden = volunteerAsks?.hiddenAsks || []
if (_.includes(hidden, id) || !_.isEmpty(asks) || !needToShow) {
return
}
asks.push(
<div key={id}>
<div className={styles.notificationsPage}>
<div
className={
isNarrow ? styles.notificationsContentNarrow : styles.notificationsContent
}
>
{children}
</div>
</div>
</div>
)
}
export const answerLaterOnProfile = (
<>
Tu pourras y répondre plus tard sur la page <a href="/profil">Mon profil</a>.
</>
)

View File

@ -1,5 +1,4 @@
import React, { memo, useCallback } from "react"
import { Link } from "react-router-dom"
import { AppDispatch } from "../../store"
import { fetchVolunteerForgot } from "../../store/volunteerForgot"
import styles from "./styles.module.scss"
@ -40,7 +39,7 @@ const ForgotForm = ({ dispatch, error, message }: Props): JSX.Element => {
<div className={styles.error}>{error}</div>
<div className={styles.message}>{message}</div>
<div className={styles.link}>
<Link to="/"> S&apos;identifier </Link>
<a href="/"> S&apos;identifier </a>
</div>
</form>
)

View File

@ -1,5 +1,4 @@
import React, { memo, useCallback } from "react"
import { Link } from "react-router-dom"
import { shallowEqual, useDispatch, useSelector } from "react-redux"
import { AppState } from "../../store"
import { fetchVolunteerLogin } from "../../store/volunteerLogin"
@ -43,7 +42,7 @@ const LoginForm = (): JSX.Element => {
</div>
{loginError && <div className={styles.error}>{loginError}</div>}
<div className={styles.link}>
<Link to="/oubli"> Demander un nouveau mot de passe </Link>
<a href="/oubli"> Demander un nouveau mot de passe </a>
</div>
</form>
)

View File

@ -37,7 +37,7 @@ const MainMenu: FC = (): JSX.Element | null => {
<ul className={classnames(styles.mainMenu, opened && styles.opened)}>
{createMenuItem("Questions", "/")}
{createMenuItem("Annonces", "/annonces")}
createMenuItem("Mon profil", "/profil")
{createMenuItem("Mon profil", "/profil")}
<button type="button" className={styles.close} onClick={onClose}>
×
</button>

View File

@ -1,546 +0,0 @@
import _, { get } from "lodash"
import React, { memo, useCallback, useEffect, useRef, useState } from "react"
import isNode from "detect-node"
import { shallowEqual, useDispatch, useSelector } from "react-redux"
import { fetchVolunteerNotifsSet } from "../../store/volunteerNotifsSet"
import styles from "./styles.module.scss"
import { selectUserJwtToken } from "../../store/auth"
import { VolunteerNotifs } from "../../services/volunteers"
import TeamWishesForm, {
fetchFor as fetchForTeamWishesForm,
} from "../VolunteerBoard/TeamWishesForm/TeamWishesForm"
import { AppState } from "../../store"
import { useUserTeamWishes } from "../VolunteerBoard/teamWishes.utils"
import { useUserDayWishes } from "../VolunteerBoard/daysWishes.utils"
import ParticipationDetailsForm, {
fetchFor as fetchForarticipationDetailsForm,
} from "../VolunteerBoard/ParticipationDetailsForm/ParticipationDetailsForm"
import DayWishesForm, {
fetchFor as fetchForDayWishesForm,
} from "../VolunteerBoard/DayWishesForm/DayWishesForm"
import { useUserParticipationDetails } from "../VolunteerBoard/participationDetails.utils"
import FormButton from "../Form/FormButton/FormButton"
import { toastError, toastSuccess } from "../../store/utils"
let prevNotifs: VolunteerNotifs | undefined
const Notifications = (): JSX.Element | null => {
const dispatch = useDispatch()
const jwtToken = useSelector(selectUserJwtToken)
const volunteerNotifs = useSelector((state: AppState) => {
const notifs = state.volunteerNotifsSet?.entity
if (notifs) {
prevNotifs = notifs
return notifs
}
return prevNotifs
}, shallowEqual)
const hidden = volunteerNotifs?.hiddenNotifs || []
const notifs: JSX.Element[] = []
const onSubmit1 = useCallback((): void => {
dispatch(
fetchVolunteerNotifsSet(jwtToken, 0, {
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 1],
})
)
}, [dispatch, jwtToken, volunteerNotifs])
if (!_.includes(hidden, 1)) {
notifs.push(
<div key="1">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContentNarrow}>
<form>
Salut {volunteerNotifs?.firstname} !
<div className={styles.notifIntro} key="login-intro">
Ici tu seras notifié(e) des nouvelles importantes et des questions
pour lesquelles il nous faudrait absolument ta réponse.
<div className={styles.formButtons}>
<FormButton onClick={onSubmit1}>Ok, continuer</FormButton>
</div>
</div>
</form>
</div>
</div>
</div>
)
}
// const [participation, setParticipation] = useState(volunteerNotifs?.active || "inconnu")
// const [participationMessage, setParticipationMessage] = useState("")
// const onChangeValue2 = (e: React.ChangeEvent<HTMLInputElement>) =>
// setParticipation(e.target.value)
// const onSubmit2 = useCallback(
// (): void => {
// if (participation === "inconnu") {
// setParticipationMessage("Il nous faudrait une réponse ^^")
// return
// }
// dispatch(
// fetchVolunteerNotifsSet(jwtToken, 0, {
// hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 2],
// active: participation,
// })
// )
// },
// [dispatch, jwtToken, volunteerNotifs, participation]
// )
// if (!_.includes(hidden, 2)) {
// notifs.push(
// <div key="2">
// <div className={styles.notificationsPage}>
// <div className={styles.notificationsContentNarrow}>
// <div className={styles.formLine} key="line-participation">
// <form>
// Si les conditions sanitaires te le permettent, souhaites-tu être
// bénévole à PeL 2022 ?<br />
// <label>
// <input
// type="radio"
// value="oui"
// name="participation"
// checked={participation === "oui"}
// onChange={onChangeValue2}
// />{" "}
// Oui
// </label>
// <label>
// <input
// type="radio"
// value="non"
// name="participation"
// checked={participation === "non"}
// onChange={onChangeValue2}
// />{" "}
// Non
// </label>
// <label>
// <input
// type="radio"
// value="peut-etre"
// name="participation"
// checked={participation === "peut-etre"}
// onChange={onChangeValue2}
// />{" "}
// Je ne sais pas encore
// </label>
// {participation === "peut-etre" ? (
// <div>
// On te le reproposera dans quelques temps.
// <br />
// Si tu as besoin d&apos;infos, viens nous en parler sur le
// serveur Discord ! Pour le rejoindre,{" "}
// <a
// href="https://discord.com/invite/eXhjKxSBB4"
// target="_blank"
// rel="noreferrer"
// >
// clique ici{" "}
// </a>
// .
// </div>
// ) : null}
// <div className={styles.formButtons}>
// <FormButton onClick={onSubmit2}>Confirmer</FormButton>
// </div>
// <div className={styles.message}>{participationMessage}</div>
// </form>
// </div>
// </div>
// </div>
// </div>
// )
// }
// const onSubmit3 = useCallback((): void => {
// dispatch(
// fetchVolunteerNotifsSet(jwtToken, 0, {
// hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 3],
// })
// )
// }, [dispatch, jwtToken, volunteerNotifs])
// if (!_.includes(hidden, 3)) {
// notifs.push(
// <div key="3">
// <div className={styles.notificationsPage}>
// <div className={styles.notificationsContentNarrow}>
// <form>
// <div
// className={classnames(styles.notifIntro, styles.notifCentered)}
// key="login-intro"
// >
// La{" "}
// <a
// href="https://mailchi.mp/3c75c3b3a20f/gazette_2020_02-8978118"
// onClick={onSubmit3}
// >
// gazette de février
// </a>{" "}
// est disponible !<br />
// <div className={styles.formButtons}>
// <FormButton onClick={onSubmit3}>Ok, masquer</FormButton>
// </div>
// </div>
// </form>
// </div>
// </div>
// </div>
// )
// }
const answerLaterOnProfile = (
<>
Tu pourras y répondre plus tard sur la page <a href="/profil">Mon profil</a>.
</>
)
const onSubmit10 = useCallback((): void => {
dispatch(
fetchVolunteerNotifsSet(jwtToken, 0, {
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 10],
})
)
}, [dispatch, jwtToken, volunteerNotifs])
const [userWishes] = useUserDayWishes()
const participation10 = get(userWishes, "active", "inconnu")
const newSelection = get(userWishes, "dayWishes", [])
const comment10 = get(userWishes, "dayWishesComment", "")
const needToShow10 = participation10 === "inconnu" || (newSelection.length === 0 && !comment10)
if (!_.includes(hidden, 10) && _.isEmpty(notifs) && needToShow10) {
notifs.push(
<div key="10">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<DayWishesForm afterSubmit={onSubmit10}>
{answerLaterOnProfile}
</DayWishesForm>
</div>
</div>
</div>
)
}
const onSubmit11 = useCallback((): void => {
dispatch(
fetchVolunteerNotifsSet(jwtToken, 0, {
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 11],
})
)
}, [dispatch, jwtToken, volunteerNotifs])
const [teamWishesData] = useUserTeamWishes()
const teamWishesString = get(teamWishesData, "teamWishes", [])
const comment = get(teamWishesData, "teamWishesComment", "")
const needToShow11 = teamWishesString.length === 0 && !comment
if (!_.includes(hidden, 11) && _.isEmpty(notifs) && needToShow11) {
notifs.push(
<div key="11">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<TeamWishesForm afterSubmit={onSubmit11}>
{answerLaterOnProfile}
</TeamWishesForm>
</div>
</div>
</div>
)
}
const onSubmit12 = useCallback((): void => {
dispatch(
fetchVolunteerNotifsSet(jwtToken, 0, {
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 12],
})
)
}, [dispatch, jwtToken, volunteerNotifs])
const [participationDetails] = useUserParticipationDetails()
const tshirtSize = get(participationDetails, "tshirtSize", "")
const food = get(participationDetails, "food", "")
const needToShow12 = !tshirtSize && !food
if (!_.includes(hidden, 12) && _.isEmpty(notifs) && needToShow12) {
notifs.push(
<div key="12">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<ParticipationDetailsForm afterSubmit={onSubmit12}>
{answerLaterOnProfile}
</ParticipationDetailsForm>
</div>
</div>
</div>
)
}
/* DISCORD
Discord nous donne à tous la parole via nos téléphone ou navigateurs, pour organiser le meilleur des festivals !
Il permet de discuter sujet par sujet entre tous les bénévoles, entre les membres d'une même équipe, ou avec ton référent.
Il permet de choisir les sujets spécifiques sur lesquels être notifié de nouveaux messages.
Rejoindre les 86 bénévoles déjà présents sur le serveur se fait en cliquant ici.
Tu n'y es absolument pas obligé(e) ! C'est juste plus pratique.
*/
const [acceptsNotifs, setAcceptsNotifs] = useState("")
const mounted = useRef(false)
useEffect(() => {
if (mounted.current) {
return
}
mounted.current = true
if (!isNode) {
if (volunteerNotifs?.acceptsNotifs === "oui") {
navigator.serviceWorker.ready.then((registration) =>
registration.pushManager.getSubscription().then((existedSubscription) => {
const doesAcceptNotifs =
_.isEqual(
JSON.parse(JSON.stringify(existedSubscription)),
JSON.parse(volunteerNotifs?.pushNotifSubscription)
) && volunteerNotifs?.acceptsNotifs === "oui"
setAcceptsNotifs(doesAcceptNotifs ? "oui" : "non")
})
)
} else {
setAcceptsNotifs("non")
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onChangeValue99 = (e: React.ChangeEvent<HTMLInputElement>) =>
setAcceptsNotifs(e.target.value)
const onSubmit99 = useCallback(async (): Promise<void> => {
if (isNode) {
return
}
if (!("serviceWorker" in navigator)) {
return
}
if (acceptsNotifs === "non") {
setAcceptsNotifs("non")
dispatch(
fetchVolunteerNotifsSet(jwtToken, 0, {
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 99],
acceptsNotifs: "non",
})
)
return
}
const registration = await navigator.serviceWorker.ready
if (!registration.pushManager) {
toastError(
"Il y a un problème avec le push manager. Il faudrait utiliser un navigateur plus récent !"
)
dispatch(
fetchVolunteerNotifsSet(jwtToken, 0, {
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 99],
})
)
return
}
const convertedVapidKey = urlBase64ToUint8Array(process.env.FORCE_ORANGE_PUBLIC_VAPID_KEY)
function urlBase64ToUint8Array(base64String?: string) {
if (!base64String) {
return ""
}
const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
// eslint-disable-next-line
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/")
const rawData = atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; i += 1) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}
if (!convertedVapidKey) {
toastError("No convertedVapidKey available")
}
try {
const existedSubscription = await registration.pushManager.getSubscription()
if (existedSubscription === null) {
// No subscription detected, make a request
try {
const newSubscription = await registration.pushManager.subscribe({
applicationServerKey: convertedVapidKey,
userVisibleOnly: true,
})
// New subscription added
if (
volunteerNotifs?.acceptsNotifs === "oui" &&
!subscriptionEqualsSave(
newSubscription,
volunteerNotifs?.pushNotifSubscription
)
) {
toastSuccess(
"Un autre navigateur était notifié, mais c'est maintenant celui-ci qui le sera."
)
}
dispatch(
fetchVolunteerNotifsSet(jwtToken, 0, {
pushNotifSubscription: JSON.stringify(newSubscription),
acceptsNotifs: "oui",
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 99],
})
)
} catch (_e) {
if (Notification.permission !== "granted") {
toastError(
"Mince tu as bloqué les notifications pour le site des bénévoles ! En haut juste à gauche de la barre d'adresse, il y a une icone de cadenas ou de message barré sur lequel cliquer pour annuler ce blocage.",
false
)
} else {
toastError(
"Il y a eu une erreur avec l'enregistrement avec le Service Worker. Il faudrait utiliser un navigateur plus récent !"
)
}
}
} else {
// Existed subscription detected
if (
volunteerNotifs?.acceptsNotifs === "oui" &&
!subscriptionEqualsSave(
existedSubscription,
volunteerNotifs?.pushNotifSubscription
)
) {
toastSuccess(
"Un autre navigateur était notifié, mais c'est maintenant celui-ci qui le sera."
)
}
dispatch(
fetchVolunteerNotifsSet(jwtToken, 0, {
pushNotifSubscription: JSON.stringify(existedSubscription),
acceptsNotifs: "oui",
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 99],
})
)
}
} catch (_e) {
toastError(
"Il y a eu une erreur avec l'enregistrement avec le Service Worker. Il faudrait utiliser un navigateur plus récent !"
)
}
}, [
acceptsNotifs,
dispatch,
jwtToken,
volunteerNotifs?.acceptsNotifs,
volunteerNotifs?.hiddenNotifs,
volunteerNotifs?.pushNotifSubscription,
])
function subscriptionEqualsSave(toCheck: PushSubscription, save: string | undefined): boolean {
if (!save) {
return !toCheck
}
return _.isEqual(JSON.parse(JSON.stringify(toCheck)), JSON.parse(save))
}
const needToShow99 = volunteerNotifs?.acceptsNotifs !== "oui"
if (!_.includes(hidden, 99) && _.isEmpty(notifs) && needToShow99) {
notifs.push(
<div key="pushNotifs">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<div className={styles.formLine} key="line-participation">
<label>
Acceptes-tu de recevoir une alerte dans ton navigateur quand on en
aura d&apos;autres à t'afficher ici ?<br />
<span className={styles.sousMessage}>
(Ça nous simplifierait la vie, on a des soucis à contacter les
bénévoles par email.)
</span>
<label>
<input
type="radio"
value="oui"
name="gender"
checked={acceptsNotifs === "oui"}
onChange={onChangeValue99}
/>{" "}
Oui
</label>
<label>
<input
type="radio"
value="non"
name="gender"
checked={acceptsNotifs === "non"}
onChange={onChangeValue99}
/>{" "}
Non
</label>
</label>
<div className={styles.formButtons}>
<FormButton onClick={onSubmit99}>Enregistrer</FormButton>
</div>
</div>
</div>
</div>
</div>
)
}
if (_.isEmpty(notifs)) {
notifs.push(
<div key="pushNotifs">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<div className={styles.formLine} key="line-participation">
<label>
Tu as fait le tour des dernières infos ou questions importantes,
merci ! :)
<br />
Nous te préviendrons quand il y en aura de nouvelles.
<br />
</label>
</div>
</div>
</div>
</div>
)
}
if (volunteerNotifs === undefined) {
return null
}
return <div>{notifs.map<React.ReactNode>((t) => t).reduce((prev, curr) => [prev, curr])}</div>
}
export default memo(Notifications)
// Fetch server-side data here
export const fetchFor = [
...fetchForTeamWishesForm,
...fetchForarticipationDetailsForm,
...fetchForDayWishesForm,
]

View File

@ -1,5 +1,4 @@
import { memo } from "react"
import { Link } from "react-router-dom"
import { Volunteer } from "../../services/volunteers"
import styles from "./styles.module.scss"
@ -14,9 +13,9 @@ const VolunteerList = ({ items }: Props) => (
<ul>
{items.map(({ id, lastname, firstname }) => (
<li key={id}>
<Link to={`/Volunteer/${id}`}>
<a href={`/Volunteer/${id}`}>
<b>{firstname}</b> {lastname}
</Link>
</a>
</li>
))}
</ul>

View File

@ -7,7 +7,7 @@ import ErrorBoundary from "./ErrorBoundary"
import GameList from "./GameList"
import Loading from "./Loading"
import LoginForm from "./LoginForm"
import Notifications, { fetchFor as fetchForNotifications } from "./Notifications"
import Asks, { fetchFor as fetchForAsks } from "./Asks"
import ParticipationDetailsForm, {
fetchFor as fetchForParticipationDetailsForm,
} from "./VolunteerBoard/ParticipationDetailsForm/ParticipationDetailsForm"
@ -30,8 +30,8 @@ export {
GameList,
Loading,
LoginForm,
Notifications,
fetchForNotifications,
Asks,
fetchForAsks,
ParticipationDetailsForm,
fetchForParticipationDetailsForm,
PreRegisterForm,

View File

@ -1,6 +1,6 @@
import { FC, memo } from "react"
import * as _ from "lodash"
import { RouteComponentProps, Link } from "react-router-dom"
import { RouteComponentProps } from "react-router-dom"
import { useSelector, shallowEqual } from "react-redux"
import { Helmet } from "react-helmet"
import { EntityState } from "@reduxjs/toolkit"
@ -60,7 +60,7 @@ const AnnouncementsPage: FC<Props> = (): JSX.Element => {
</div>
<div className={styles.announcements}>
<div className={styles.navigationLink}>
<Link to="/sinscrire"> S&apos;informer sur le bénévolat </Link>
<a href="/sinscrire"> S&apos;informer sur le bénévolat </a>
</div>
</div>
</div>

View File

@ -1,12 +1,12 @@
import { FC, memo } from "react"
import { RouteComponentProps, Link } from "react-router-dom"
import { RouteComponentProps } from "react-router-dom"
import { useSelector } from "react-redux"
import { Helmet } from "react-helmet"
import { AppThunk } from "../../store"
import { fetchForNotifications, LoginForm, Notifications } from "../../components"
import { fetchForAsks, LoginForm, Asks } from "../../components"
import styles from "./styles.module.scss"
import { fetchVolunteerNotifsSetIfNeed } from "../../store/volunteerNotifsSet"
import { fetchVolunteerAsksSetIfNeed } from "../../store/volunteerAsksSet"
import { selectUserJwtToken } from "../../store/auth"
export type Props = RouteComponentProps
@ -17,7 +17,7 @@ const HomePage: FC<Props> = (): JSX.Element => {
if (jwtToken === undefined) return <p>Loading...</p>
if (jwtToken) {
return <Notifications />
return <Asks />
}
return (
<div>
@ -29,7 +29,7 @@ const HomePage: FC<Props> = (): JSX.Element => {
</div>
<div className={styles.homePage}>
<div className={styles.navigationLink}>
<Link to="/sinscrire"> S&apos;informer sur le bénévolat </Link>
<a href="/sinscrire"> S&apos;informer sur le bénévolat </a>
</div>
</div>
</div>
@ -38,8 +38,8 @@ const HomePage: FC<Props> = (): JSX.Element => {
// Fetch server-side data here
export const loadData = (): AppThunk[] => [
fetchVolunteerNotifsSetIfNeed(),
...fetchForNotifications.map((f) => f()),
fetchVolunteerAsksSetIfNeed(),
...fetchForAsks.map((f) => f()),
]
export default memo(HomePage)

View File

@ -7,7 +7,7 @@ import {
Volunteer,
VolunteerWithoutId,
VolunteerLogin,
VolunteerNotifs,
VolunteerAsks,
VolunteerTeamWishes,
translationVolunteer,
VolunteerDayWishes,
@ -115,10 +115,10 @@ async function sendForgetEmail(email: string, password: string): Promise<void> {
}
}
export const volunteerNotifsSet = expressAccessor.set(async (list, body, id) => {
export const volunteerAsksSet = 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 notifs`)
throw Error(`On ne peut acceder qu'à ses propres questions`)
}
const notifChanges = body[1]
const volunteer = list.find((v) => v.id === requestedId)
@ -136,10 +136,10 @@ export const volunteerNotifsSet = expressAccessor.set(async (list, body, id) =>
firstname: newVolunteer.firstname,
adult: newVolunteer.adult,
active: newVolunteer.active,
hiddenNotifs: newVolunteer.hiddenNotifs,
hiddenAsks: newVolunteer.hiddenAsks,
pushNotifSubscription: newVolunteer.pushNotifSubscription,
acceptsNotifs: newVolunteer.acceptsNotifs,
} as VolunteerNotifs,
} as VolunteerAsks,
}
})

View File

@ -25,7 +25,7 @@ import {
volunteerSet,
volunteerLogin,
volunteerForgot,
volunteerNotifsSet,
volunteerAsksSet,
volunteerParticipationDetailsSet,
volunteerTeamWishesSet,
volunteerDayWishesSet,
@ -92,7 +92,7 @@ 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.post("/VolunteerNotifsSet", secure as RequestHandler, volunteerNotifsSet)
app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet)
app.post(
"/VolunteerParticipationDetailsSet",
secure as RequestHandler,

View File

@ -33,7 +33,7 @@ export class Volunteer {
teamWishesComment = ""
hiddenNotifs: number[] = []
hiddenAsks: number[] = []
created = new Date()
@ -64,7 +64,7 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
food: "alimentation",
teamWishes: "enviesEquipe",
teamWishesComment: "commentaireEnviesEquipe",
hiddenNotifs: "notifsCachees",
hiddenAsks: "questionsCachees",
created: "creation",
password1: "passe1",
password2: "passe2",
@ -92,7 +92,7 @@ export const volunteerExample: Volunteer = {
food: "Végétarien",
teamWishes: [],
teamWishesComment: "",
hiddenNotifs: [],
hiddenAsks: [],
created: new Date(0),
password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
@ -116,12 +116,12 @@ export interface VolunteerForgot {
message: string
}
export interface VolunteerNotifs {
export interface VolunteerAsks {
id: Volunteer["id"]
firstname: Volunteer["firstname"]
adult: Volunteer["adult"]
active: Volunteer["active"]
hiddenNotifs: Volunteer["hiddenNotifs"]
hiddenAsks: Volunteer["hiddenAsks"]
pushNotifSubscription: Volunteer["pushNotifSubscription"]
acceptsNotifs: Volunteer["acceptsNotifs"]
}

View File

@ -3,7 +3,7 @@ import {
elementName,
Volunteer,
VolunteerDayWishes,
VolunteerNotifs,
VolunteerAsks,
VolunteerParticipationDetails,
VolunteerTeamWishes,
VolunteerWithoutId,
@ -21,8 +21,8 @@ export const volunteerLogin =
export const volunteerForgot = serviceAccessors.customPost<[{ email: string }]>("Forgot")
export const volunteerNotifsSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerNotifs>]>("NotifsSet")
export const volunteerAsksSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerAsks>]>("AsksSet")
export const volunteerTeamWishesSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerTeamWishes>]>("TeamWishesSet")

View File

@ -14,7 +14,7 @@ import volunteerList from "./volunteerList"
import volunteerSet from "./volunteerSet"
import volunteerLogin from "./volunteerLogin"
import volunteerForgot from "./volunteerForgot"
import volunteerNotifsSet from "./volunteerNotifsSet"
import volunteerAsksSet from "./volunteerAsksSet"
import volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
import volunteerDayWishesSet from "./volunteerDayWishesSet"
import volunteerTeamWishesSet from "./volunteerTeamWishesSet"
@ -37,7 +37,7 @@ export default (history: History) => ({
volunteerSet,
volunteerLogin,
volunteerForgot,
volunteerNotifsSet,
volunteerAsksSet,
volunteerParticipationDetailsSet,
volunteerDayWishesSet,
volunteerTeamWishesSet,

View File

@ -0,0 +1,59 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { StateRequest, toastError, elementFetch } from "./utils"
import { VolunteerAsks } from "../services/volunteers"
import { AppThunk, AppState } from "."
import { volunteerAsksSet } from "../services/volunteersAccessors"
type StateVolunteerAsksSet = { entity?: VolunteerAsks } & StateRequest
export const initialState: StateVolunteerAsksSet = {
readyStatus: "idle",
}
const volunteerAsksSetSlice = createSlice({
name: "volunteerAsksSet",
initialState,
reducers: {
getRequesting: (_) => ({
readyStatus: "request",
}),
getSuccess: (_, { payload }: PayloadAction<VolunteerAsks>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
readyStatus: "failure",
error: payload,
}),
},
})
export default volunteerAsksSetSlice.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerAsksSetSlice.actions
export const fetchVolunteerAsksSet = elementFetch(
volunteerAsksSet,
getRequesting,
getSuccess,
getFailure,
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
)
const shouldFetchVolunteerAsksSet = (state: AppState, id: number) =>
state.volunteerAsksSet?.readyStatus !== "success" ||
(state.volunteerAsksSet?.entity && state.volunteerAsksSet?.entity?.id !== id)
export const fetchVolunteerAsksSetIfNeed =
(id = 0, notif: Partial<VolunteerAsks> = {}): AppThunk =>
(dispatch, getState) => {
let jwt = ""
if (!id) {
;({ jwt, id } = getState().auth)
}
if (shouldFetchVolunteerAsksSet(getState(), id))
return dispatch(fetchVolunteerAsksSet(jwt, id, notif))
return null
}

View File

@ -1,78 +0,0 @@
import { PayloadAction, createSlice, createSelector } from "@reduxjs/toolkit"
import get from "lodash/get"
import { StateRequest, toastError, elementFetch } from "./utils"
import { VolunteerNotifs } from "../services/volunteers"
import { AppThunk, AppState } from "."
import { volunteerNotifsSet } from "../services/volunteersAccessors"
type StateVolunteerNotifsSet = { entity?: VolunteerNotifs } & StateRequest
export const initialState: StateVolunteerNotifsSet = {
readyStatus: "idle",
}
const volunteerNotifsSetSlice = createSlice({
name: "volunteerNotifsSet",
initialState,
reducers: {
getRequesting: (_) => ({
readyStatus: "request",
}),
getSuccess: (_, { payload }: PayloadAction<VolunteerNotifs>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
readyStatus: "failure",
error: payload,
}),
},
})
export default volunteerNotifsSetSlice.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerNotifsSetSlice.actions
export const fetchVolunteerNotifsSet = elementFetch(
volunteerNotifsSet,
getRequesting,
getSuccess,
getFailure,
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
)
const shouldFetchVolunteerNotifsSet = (state: AppState, id: number) =>
state.volunteerNotifsSet?.readyStatus !== "success" ||
(state.volunteerNotifsSet?.entity && state.volunteerNotifsSet?.entity?.id !== id)
export const fetchVolunteerNotifsSetIfNeed =
(id = 0, notif: Partial<VolunteerNotifs> = {}): AppThunk =>
(dispatch, getState) => {
let jwt = ""
if (!id) {
;({ jwt, id } = getState().auth)
}
if (shouldFetchVolunteerNotifsSet(getState(), id))
return dispatch(fetchVolunteerNotifsSet(jwt, id, notif))
return null
}
export const openedNotifsIds = [1, 2, 3]
export const selectVolunteerNotifsSetState = (state: AppState): StateVolunteerNotifsSet =>
state.volunteerNotifsSet
export const selectHiddenNotifs = createSelector(selectVolunteerNotifsSetState, (notifState) =>
get(notifState, "entity.hiddenNotifs", [])
)
export const selectWaitingsNotifs = createSelector(selectHiddenNotifs, (hidden) =>
openedNotifsIds.filter((id) => !hidden.find((hiddenId: number) => hiddenId === id))
)
export const hasWaitingNotifs = createSelector(
selectWaitingsNotifs,
(waiting) => waiting.length > 0
)