diff --git a/src/app/index.tsx b/src/app/index.tsx index a1ad843..bc6a5b9 100755 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -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 => (

- {config.APP.title} + {config.APP.title}

{config.APP.description}
diff --git a/src/components/Asks/AskDayWishes.tsx b/src/components/Asks/AskDayWishes.tsx new file mode 100644 index 0000000..b634a1b --- /dev/null +++ b/src/components/Asks/AskDayWishes.tsx @@ -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, + {answerLaterOnProfile} + ) +} + +// Fetch server-side data here +export const fetchFor = [...fetchForDayWishesForm] diff --git a/src/components/Asks/AskGazette.tsx b/src/components/Asks/AskGazette.tsx new file mode 100644 index 0000000..8be33a8 --- /dev/null +++ b/src/components/Asks/AskGazette.tsx @@ -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, +
+
+ La{" "} + + gazette de février + {" "} + est disponible !
+
+ Ok, masquer +
+
+
+ ) +} diff --git a/src/components/Asks/AskParticipationDetails.tsx b/src/components/Asks/AskParticipationDetails.tsx new file mode 100644 index 0000000..132a07f --- /dev/null +++ b/src/components/Asks/AskParticipationDetails.tsx @@ -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, + + {answerLaterOnProfile} + + ) +} + +// Fetch server-side data here +export const fetchFor = [...fetchForParticipationDetailsForm] diff --git a/src/components/Asks/AskPushNotif.tsx b/src/components/Asks/AskPushNotif.tsx new file mode 100644 index 0000000..9676c19 --- /dev/null +++ b/src/components/Asks/AskPushNotif.tsx @@ -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) => + setAcceptsNotifs(e.target.value) + + const onSubmit = useCallback(async (): Promise => { + 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, +
+ + +
+ Enregistrer +
+
+ ) +} diff --git a/src/components/Asks/AskTeamWishes.tsx b/src/components/Asks/AskTeamWishes.tsx new file mode 100644 index 0000000..528f621 --- /dev/null +++ b/src/components/Asks/AskTeamWishes.tsx @@ -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, + {answerLaterOnProfile} + ) +} + +// Fetch server-side data here +export const fetchFor = [...fetchForTeamWishesForm] diff --git a/src/components/Asks/AskWelcome.tsx b/src/components/Asks/AskWelcome.tsx new file mode 100644 index 0000000..f8e373c --- /dev/null +++ b/src/components/Asks/AskWelcome.tsx @@ -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, +
+ Salut {volunteerAsks?.firstname} ! +
+ Ici tu seras notifié(e) des nouvelles importantes et des questions pour lesquelles + il nous faudrait absolument ta réponse. +
+ Ok, continuer +
+
+
+ ) +} diff --git a/src/components/Asks/index.tsx b/src/components/Asks/index.tsx new file mode 100644 index 0000000..e4877d5 --- /dev/null +++ b/src/components/Asks/index.tsx @@ -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( +
+
+
+
+ +
+
+
+
+ ) + } + + if (volunteerAsks === undefined) { + return null + } + + return
{asks.map((t) => t).reduce((prev, curr) => [prev, curr])}
+} + +export default memo(Asks) + +// Fetch server-side data here +export const fetchFor = [ + ...fetchForDayWishes, + ...fetchForTeamWishes, + ...fetchForParticipationDetails, +] diff --git a/src/components/Notifications/styles.module.scss b/src/components/Asks/styles.module.scss similarity index 100% rename from src/components/Notifications/styles.module.scss rename to src/components/Asks/styles.module.scss diff --git a/src/components/Asks/utils.tsx b/src/components/Asks/utils.tsx new file mode 100644 index 0000000..aca7104 --- /dev/null +++ b/src/components/Asks/utils.tsx @@ -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 + 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( +
+
+
+ {children} +
+
+
+ ) +} + +export const answerLaterOnProfile = ( + <> + Tu pourras y répondre plus tard sur la page Mon profil. + +) diff --git a/src/components/ForgotForm/ForgotForm.tsx b/src/components/ForgotForm/ForgotForm.tsx index 72ad494..eea33d4 100644 --- a/src/components/ForgotForm/ForgotForm.tsx +++ b/src/components/ForgotForm/ForgotForm.tsx @@ -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 => {
{error}
{message}
- S'identifier + S'identifier
) diff --git a/src/components/LoginForm/index.tsx b/src/components/LoginForm/index.tsx index ff729bc..a7c397c 100644 --- a/src/components/LoginForm/index.tsx +++ b/src/components/LoginForm/index.tsx @@ -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 => {
{loginError &&
{loginError}
}
- Demander un nouveau mot de passe + Demander un nouveau mot de passe
) diff --git a/src/components/Navigation/MainMenu.tsx b/src/components/Navigation/MainMenu.tsx index cb8b6e0..9a519b1 100644 --- a/src/components/Navigation/MainMenu.tsx +++ b/src/components/Navigation/MainMenu.tsx @@ -37,7 +37,7 @@ const MainMenu: FC = (): JSX.Element | null => {