Add notifications to home page

This commit is contained in:
pikiou 2022-01-07 14:23:33 +01:00
parent adde4f366e
commit 5cbb5811ec
37 changed files with 661 additions and 215 deletions

View File

@ -74,6 +74,8 @@
"@loadable/server": "^5.15.0", "@loadable/server": "^5.15.0",
"@reduxjs/toolkit": "^1.6.0", "@reduxjs/toolkit": "^1.6.0",
"@sendgrid/mail": "^7.6.0", "@sendgrid/mail": "^7.6.0",
"@types/cookie-parser": "^1.4.2",
"@types/js-cookie": "^3.0.1",
"@types/lodash": "^4.14.177", "@types/lodash": "^4.14.177",
"autoprefixer": "^10.2.6", "autoprefixer": "^10.2.6",
"axios": "^0.21.1", "axios": "^0.21.1",
@ -81,6 +83,7 @@
"chalk": "^4.1.1", "chalk": "^4.1.1",
"compression": "^1.7.4", "compression": "^1.7.4",
"connected-react-router": "^6.9.1", "connected-react-router": "^6.9.1",
"cookie-parser": "^1.4.6",
"core-js": "^3.15.2", "core-js": "^3.15.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"express": "^4.17.1", "express": "^4.17.1",
@ -93,6 +96,7 @@
"hpp": "^0.2.3", "hpp": "^0.2.3",
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"https": "^1.0.0", "https": "^1.0.0",
"js-cookie": "^3.0.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"morgan": "^1.10.0", "morgan": "^1.10.0",

View File

@ -3,13 +3,19 @@ import { Provider } from "react-redux"
import { ConnectedRouter } from "connected-react-router" import { ConnectedRouter } from "connected-react-router"
import { RouteConfig, renderRoutes } from "react-router-config" import { RouteConfig, renderRoutes } from "react-router-config"
import { loadableReady } from "@loadable/component" import { loadableReady } from "@loadable/component"
import Cookies from "js-cookie"
import createStore from "../store" import createStore from "../store"
import routes from "../routes" import routes from "../routes"
const storage: any = localStorage
// Get the initial state from server-side rendering // Get the initial state from server-side rendering
const initialState = window.__INITIAL_STATE__ const initialState = window.__INITIAL_STATE__
const { store, history } = createStore({ initialState })
const id = +(Cookies.get("id") || storage?.getItem("id"))
const jwt = Cookies.get("jwt") || storage?.getItem("jwt")
const { store, history } = createStore({ initialState, id, jwt })
const render = (Routes: RouteConfig[]) => const render = (Routes: RouteConfig[]) =>
ReactDOM.hydrate( ReactDOM.hydrate(

View File

@ -28,7 +28,7 @@ const LoginForm = ({ dispatch, error }: Props): JSX.Element => {
return ( return (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<div className={styles.loginIntro} key="login-intro"> <div className={styles.loginIntro} key="login-intro">
Connectez-vous pour accéder à votre espace. Si vous êtes bénévole, connectez-vous pour accéder à votre espace.
</div> </div>
<div className={styles.formLine} key="line-email"> <div className={styles.formLine} key="line-email">
<label htmlFor="email">Email</label> <label htmlFor="email">Email</label>

View File

@ -0,0 +1,162 @@
import _ from "lodash"
import React, { memo, useCallback, useState } from "react"
import { AppDispatch } from "../../store"
import { fetchVolunteerNotifsSet } from "../../store/volunteerNotifsSet"
import { VolunteerNotifs } from "../../services/volunteers"
import styles from "./styles.module.scss"
import { logoutUser } from "../../store/auth"
import { unsetJWT } from "../../services/auth"
interface Props {
dispatch: AppDispatch
jwt: string
// eslint-disable-next-line react/require-default-props
volunteerNotifs?: VolunteerNotifs
}
const Notifications = ({ dispatch, jwt, volunteerNotifs }: Props): JSX.Element => {
const hidden = volunteerNotifs?.hiddenNotifs || []
const notifs: JSX.Element[] = []
const onSubmit1 = useCallback(
(event: React.SyntheticEvent): void => {
event.preventDefault()
dispatch(
fetchVolunteerNotifsSet(jwt, 0, {
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 1],
})
)
},
[dispatch, jwt, volunteerNotifs]
)
if (!_.includes(hidden, 1)) {
notifs.push(
<div key="1">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<form onSubmit={onSubmit1}>
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}>
<button type="submit">Ok, continuer</button>
</div>
</div>
</form>
</div>
</div>
</div>
)
}
const [participation, setParticipation] = useState(volunteerNotifs?.active || "inconnu")
const onChangeValue2 = (e: React.ChangeEvent<HTMLInputElement>) =>
setParticipation(e.target.value)
const onSubmit2 = useCallback(
(event: React.SyntheticEvent): void => {
event.preventDefault()
dispatch(
fetchVolunteerNotifsSet(jwt, 0, {
hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 2],
active: participation,
})
)
},
[dispatch, jwt, volunteerNotifs, participation]
)
if (!_.includes(hidden, 2)) {
notifs.push(
<div key="2">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<div className={styles.formLine} key="line-participation">
<form onSubmit={onSubmit2}>
Si les conditions sanitaires te le permettent, souhaites-tu être
bénévole à PeL 2022 ?<br />
<input
type="radio"
value="inconnu"
name="gender"
checked={participation === "inconnu"}
onChange={onChangeValue2}
/>{" "}
-<br />
<input
type="radio"
value="oui"
name="gender"
checked={participation === "oui"}
onChange={onChangeValue2}
/>{" "}
Oui
<br />
<input
type="radio"
value="non"
name="gender"
checked={participation === "non"}
onChange={onChangeValue2}
/>{" "}
Non
<br />
<input
type="radio"
value="peut-etre"
name="gender"
checked={participation === "peut-etre"}
onChange={onChangeValue2}
/>{" "}
Je ne sais pas encore
<br />
{participation === "peut-etre" ? (
<div>
On te redemandera dans quelques temps. Si tu as des
questions
</div>
) : null}
<div className={styles.formButtons}>
<button type="submit">Confirmer</button>
</div>
</form>
</div>
</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 onClick = useCallback(
(event: React.SyntheticEvent): void => {
event.preventDefault()
unsetJWT()
dispatch(logoutUser())
},
[dispatch]
)
notifs.push(
<div key="logout" className={styles.formButtons}>
<button type="button" onClick={onClick}>
Se déconnecter
</button>
</div>
)
return <div>{notifs.map<React.ReactNode>((t) => t).reduce((prev, curr) => [prev, curr])}</div>
}
export default memo(Notifications)

View File

@ -0,0 +1,41 @@
@import "../../theme/variables";
@import "../../theme/mixins";
.notificationsPage {
@include page-wrapper-center;
}
.notificationsContent {
@include page-content-wrapper;
}
.notifIntro {
margin-bottom: 10px;
}
.formLine {
padding: 5px 0;
label {
display: block;
margin-left: 5px;
}
select,
input {
width: 10%;
border: 1px solid #333;
border-radius: 4px;
}
}
.formButtons {
margin-top: 10px;
padding: 5px 0;
text-align: center;
}
.error {
margin-top: 10px;
color: rgb(255, 0, 0);
text-align: center;
}

View File

@ -12,7 +12,7 @@ interface Props {
preVolunteerCount: number | undefined preVolunteerCount: number | undefined
} }
const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => { const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => {
const [firstname, setFirstname] = useState("") const [firstname, setFirstname] = useState("")
const [lastname, setLastname] = useState("") const [lastname, setLastname] = useState("")
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
@ -97,7 +97,7 @@ const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => {
return ( return (
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<dl className={styles.registerIntro} key="register-intro"> <dl className={styles.preRegisterIntro} key="preRegister-intro">
<dt>Qu&apos;est-ce que Paris est Ludique ?</dt> <dt>Qu&apos;est-ce que Paris est Ludique ?</dt>
<dd> <dd>
<p> <p>
@ -253,4 +253,4 @@ const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => {
) )
} }
export default memo(RegisterForm) export default memo(PreRegisterForm)

View File

@ -1,7 +1,7 @@
@import "../../theme/variables"; @import "../../theme/variables";
@import "../../theme/mixins"; @import "../../theme/mixins";
.registerIntro { .preRegisterIntro {
dt { dt {
font-weight: bold; font-weight: bold;
margin-top: 10px; margin-top: 10px;

View File

@ -3,6 +3,7 @@
*/ */
import { render } from "@testing-library/react" import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom" import { MemoryRouter } from "react-router-dom"
import { volunteerExample } from "../../../services/volunteers"
import VolunteerInfo from "../index" import VolunteerInfo from "../index"
@ -10,23 +11,7 @@ describe("<VolunteerInfo />", () => {
it("renders", () => { it("renders", () => {
const tree = render( const tree = render(
<MemoryRouter> <MemoryRouter>
<VolunteerInfo <VolunteerInfo item={volunteerExample} />
item={{
id: 1,
firstname: "Aupeix",
lastname: "Amélie",
email: "pakouille.lakouille@yahoo.fr",
mobile: "0675650392",
photo: "images/volunteers/$taille/amélie_aupeix.jpg",
food: "Végétarien",
adult: 1,
privileges: 0,
active: 0,
created: new Date(0),
password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
}}
/>
</MemoryRouter> </MemoryRouter>
).container.firstChild ).container.firstChild

View File

@ -4,33 +4,14 @@
import { render } from "@testing-library/react" import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom" import { MemoryRouter } from "react-router-dom"
import { volunteerExample } from "../../../services/volunteers"
import VolunteerList from "../index" import VolunteerList from "../index"
describe("<VolunteerList />", () => { describe("<VolunteerList />", () => {
it("renders", () => { it("renders", () => {
const tree = render( const tree = render(
<MemoryRouter> <MemoryRouter>
<VolunteerList <VolunteerList items={[volunteerExample]} />
items={[
{
id: 1,
firstname: "Aupeix",
lastname: "Amélie",
email: "pakouille.lakouille@yahoo.fr",
mobile: "0675650392",
photo: "images/volunteers/$taille/amélie_aupeix.jpg",
food: "Végétarien",
adult: 1,
privileges: 0,
active: 0,
created: new Date(0),
password1:
"$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
password2:
"$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
},
]}
/>
</MemoryRouter> </MemoryRouter>
).container.firstChild ).container.firstChild

View File

@ -3,6 +3,7 @@
*/ */
import { render } from "@testing-library/react" import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom" import { MemoryRouter } from "react-router-dom"
import { volunteerExample } from "../../../services/volunteers"
import VolunteerSet from "../index" import VolunteerSet from "../index"
@ -11,24 +12,7 @@ describe("<SetVolunteer />", () => {
const dispatch = jest.fn() const dispatch = jest.fn()
const tree = render( const tree = render(
<MemoryRouter> <MemoryRouter>
<VolunteerSet <VolunteerSet dispatch={dispatch} volunteer={volunteerExample} />
dispatch={dispatch}
volunteer={{
id: 1,
firstname: "Aupeix",
lastname: "Amélie",
email: "pakouille.lakouille@yahoo.fr",
mobile: "0675650392",
photo: "images/volunteers/$taille/amélie_aupeix.jpg",
food: "Végétarien",
adult: 1,
privileges: 0,
active: 0,
created: new Date(0),
password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
}}
/>
</MemoryRouter> </MemoryRouter>
).container.firstChild ).container.firstChild

View File

@ -1,3 +1,5 @@
import LoginForm from "./LoginForm"
import Notifications from "./Notifications"
import VolunteerList from "./VolunteerList" import VolunteerList from "./VolunteerList"
import JavGameList from "./JavGameList" import JavGameList from "./JavGameList"
import VolunteerInfo from "./VolunteerInfo" import VolunteerInfo from "./VolunteerInfo"
@ -5,9 +7,11 @@ import VolunteerSet from "./VolunteerSet"
import ErrorBoundary from "./ErrorBoundary" import ErrorBoundary from "./ErrorBoundary"
import Loading from "./Loading" import Loading from "./Loading"
import WishAdd from "./WishAdd" import WishAdd from "./WishAdd"
import RegisterForm from "./RegisterForm" import PreRegisterForm from "./PreRegisterForm"
export { export {
LoginForm,
Notifications,
VolunteerList, VolunteerList,
JavGameList, JavGameList,
VolunteerInfo, VolunteerInfo,
@ -15,5 +19,5 @@ export {
ErrorBoundary, ErrorBoundary,
Loading, Loading,
WishAdd, WishAdd,
RegisterForm, PreRegisterForm,
} }

50
src/pages/Home/Home.tsx Normal file
View File

@ -0,0 +1,50 @@
import { FC, memo } from "react"
import { RouteComponentProps, Link } from "react-router-dom"
import { useDispatch, useSelector, shallowEqual } from "react-redux"
import { Helmet } from "react-helmet"
import { AppState, AppThunk } from "../../store"
import { LoginForm, Notifications } from "../../components"
import styles from "./styles.module.scss"
import { fetchVolunteerNotifsSetIfNeed } from "../../store/volunteerNotifsSet"
export type Props = RouteComponentProps
const HomePage: FC<Props> = (): JSX.Element => {
const dispatch = useDispatch()
const readyStatus = useSelector((state: AppState) => state.volunteerNotifsSet.readyStatus)
const volunteerNotifs = useSelector(
(state: AppState) => state.volunteerNotifsSet.entity,
shallowEqual
)
const loginError = useSelector((state: AppState) => state.volunteerLogin.error, shallowEqual)
const jwt = useSelector((state: AppState) => state.auth.jwt, shallowEqual)
if (!readyStatus || readyStatus === "idle" || readyStatus === "request")
return <p>Loading...</p>
if (jwt) {
return <Notifications dispatch={dispatch} jwt={jwt} volunteerNotifs={volunteerNotifs} />
}
return (
<div>
<div className={styles.homePage}>
<div className={styles.loginContent}>
<Helmet title="LoginPage" />
<LoginForm dispatch={dispatch} error={loginError || ""} />
</div>
</div>
<div className={styles.homePage}>
<div className={styles.preRegisterContent}>
<Link to="/preRegister"> S&apos;informer sur le bénévolat </Link>
</div>
</div>
</div>
)
}
// Fetch server-side data here
export const loadData = (): AppThunk[] => [fetchVolunteerNotifsSetIfNeed()]
export default memo(HomePage)

View File

@ -1,27 +0,0 @@
import { FC, memo } from "react"
import { RouteComponentProps } from "react-router-dom"
import { Helmet } from "react-helmet"
import { AppThunk } from "../../store"
import styles from "./styles.module.scss"
export type Props = RouteComponentProps
const fetchUserData = () => () => Promise.resolve()
const HomePage: FC<Props> = (): JSX.Element => (
<div className={styles.homePage}>
<div className={styles.homeContent}>
<Helmet title="Home" />
<div>Tableau de bord</div>
</div>
</div>
)
// Fetch server-side data here
export const loadData = (): AppThunk[] => [
fetchUserData(),
// More pre-fetched actions...
]
export default memo(HomePage)

View File

@ -1,15 +1,16 @@
import loadable from "@loadable/component" import loadable from "@loadable/component"
import { Loading, ErrorBoundary } from "../../components" import { Loading, ErrorBoundary } from "../../components"
import { Props, loadData } from "./HomePage" import { Props, loadData } from "./Home"
const Home = loadable(() => import("./HomePage"), { const HomePage = loadable(() => import("./Home"), {
fallback: <Loading />, fallback: <Loading />,
}) })
export default (props: Props): JSX.Element => ( export default (props: Props): JSX.Element => (
<ErrorBoundary> <ErrorBoundary>
<Home {...props} /> <HomePage {...props} />
</ErrorBoundary> </ErrorBoundary>
) )
export { loadData } export { loadData }

View File

@ -4,6 +4,14 @@
@include page-wrapper-center; @include page-wrapper-center;
} }
.homeContent { .notificationsContent {
@include page-content-wrapper(600px); @include page-content-wrapper;
}
.loginContent {
@include page-content-wrapper;
}
.preRegisterContent {
@include page-content-wrapper;
} }

View File

@ -4,7 +4,7 @@ import React, { memo } from "react"
import { Helmet } from "react-helmet" import { Helmet } from "react-helmet"
import { AppState } from "../../store" import { AppState } from "../../store"
import LoginForm from "../../components/LoginForm/LoginForm" import { LoginForm } from "../../components"
import styles from "./styles.module.scss" import styles from "./styles.module.scss"
export type Props = RouteComponentProps export type Props = RouteComponentProps

View File

@ -5,7 +5,7 @@ import { Helmet } from "react-helmet"
import { AppState, AppThunk, ValueRequest } from "../../store" import { AppState, AppThunk, ValueRequest } from "../../store"
import { fetchPreVolunteerCountIfNeed } from "../../store/preVolunteerCount" import { fetchPreVolunteerCountIfNeed } from "../../store/preVolunteerCount"
import { RegisterForm } from "../../components" import { PreRegisterForm } from "../../components"
import styles from "./styles.module.scss" import styles from "./styles.module.scss"
export type Props = RouteComponentProps export type Props = RouteComponentProps
@ -29,14 +29,14 @@ function useList(
if (readyStatus === "failure") return <p>Oops, Failed to load!</p> if (readyStatus === "failure") return <p>Oops, Failed to load!</p>
return <RegisterForm dispatch={dispatch} preVolunteerCount={value} /> return <PreRegisterForm dispatch={dispatch} preVolunteerCount={value} />
} }
} }
const RegisterPage: FC<Props> = (): JSX.Element => ( const PreRegisterPage: FC<Props> = (): JSX.Element => (
<div className={styles.registerPage}> <div className={styles.preRegisterPage}>
<div className={styles.registerContent}> <div className={styles.preRegisterContent}>
<Helmet title="RegisterPage" /> <Helmet title="PreRegisterPage" />
{useList((state: AppState) => state.preVolunteerCount, fetchPreVolunteerCountIfNeed)()} {useList((state: AppState) => state.preVolunteerCount, fetchPreVolunteerCountIfNeed)()}
</div> </div>
</div> </div>
@ -45,4 +45,4 @@ const RegisterPage: FC<Props> = (): JSX.Element => (
// Fetch server-side data here // Fetch server-side data here
export const loadData = (): AppThunk[] => [fetchPreVolunteerCountIfNeed()] export const loadData = (): AppThunk[] => [fetchPreVolunteerCountIfNeed()]
export default memo(RegisterPage) export default memo(PreRegisterPage)

View File

@ -1,14 +1,16 @@
import loadable from "@loadable/component" import loadable from "@loadable/component"
import { Loading, ErrorBoundary } from "../../components" import { Loading, ErrorBoundary } from "../../components"
import { Props } from "./RegisterPage" import { Props, loadData } from "./PreRegister"
const RegisterPage = loadable(() => import("./RegisterPage"), { const PreRegister = loadable(() => import("./PreRegister"), {
fallback: <Loading />, fallback: <Loading />,
}) })
export default (props: Props): JSX.Element => ( export default (props: Props): JSX.Element => (
<ErrorBoundary> <ErrorBoundary>
<RegisterPage {...props} /> <PreRegister {...props} />
</ErrorBoundary> </ErrorBoundary>
) )
export { loadData }

View File

@ -1,9 +1,9 @@
@import "../../theme/mixins"; @import "../../theme/mixins";
.registerPage { .preRegisterPage {
@include page-wrapper-center; @include page-wrapper-center;
} }
.registerContent { .preRegisterContent {
@include page-content-wrapper(600px); @include page-content-wrapper(600px);
} }

View File

@ -2,11 +2,11 @@ import { RouteConfig } from "react-router-config"
import App from "../app" import App from "../app"
import AsyncHome, { loadData as loadHomeData } from "../pages/Home" import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
import AsyncPreRegisterPage, { loadData as loadPreRegisterPage } from "../pages/PreRegister"
import AsyncWish, { loadData as loadWishData } from "../pages/Wish" import AsyncWish, { loadData as loadWishData } from "../pages/Wish"
import AsyncVolunteerPage, { loadData as loadVolunteerPageData } from "../pages/VolunteerPage" import AsyncVolunteerPage, { loadData as loadVolunteerPageData } from "../pages/VolunteerPage"
import Login from "../pages/Login" import Login from "../pages/Login"
import Forgot from "../pages/Forgot" import Forgot from "../pages/Forgot"
import Register from "../pages/Register"
import NotFound from "../pages/NotFound" import NotFound from "../pages/NotFound"
export default [ export default [
@ -16,7 +16,13 @@ export default [
{ {
path: "/", path: "/",
exact: true, exact: true,
component: Register, component: AsyncHome,
loadData: loadHomeData,
},
{
path: "/preRegister",
component: AsyncPreRegisterPage,
loadData: loadPreRegisterPage,
}, },
{ {
path: "/VolunteerPage/:id", path: "/VolunteerPage/:id",
@ -31,11 +37,6 @@ export default [
path: "/forgot", path: "/forgot",
component: Forgot, component: Forgot,
}, },
{
path: "/home",
component: AsyncHome,
loadData: loadHomeData,
},
{ {
path: "/wish", path: "/wish",
component: AsyncWish, component: AsyncWish,

View File

@ -37,7 +37,7 @@ export default class ExpressAccessors<
} }
// custom can be async // custom can be async
get(custom?: (list: Element[], body: Request["body"]) => Promise<any> | any) { get(custom?: (list: Element[], body: Request["body"], id: number) => Promise<any> | any) {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
try { try {
const list = (await this.sheet.getList()) || [] const list = (await this.sheet.getList()) || []
@ -46,7 +46,12 @@ export default class ExpressAccessors<
const id = parseInt(request.query.id as string, 10) || -1 const id = parseInt(request.query.id as string, 10) || -1
toCaller = list.find((e: Element) => e.id === id) toCaller = list.find((e: Element) => e.id === id)
} else { } else {
toCaller = await custom(list, request.body) const memberId = response?.locals?.jwt?.id || -1
toCaller = await custom(list, request.body, memberId)
if (toCaller?.jwt && toCaller?.id) {
response.cookie("jwt", toCaller.jwt, { maxAge: 365 * 24 * 60 * 60 })
response.cookie("id", toCaller.id, { maxAge: 365 * 24 * 60 * 60 })
}
} }
response.status(200).json(toCaller) response.status(200).json(toCaller)
} catch (e: any) { } catch (e: any) {
@ -72,7 +77,8 @@ export default class ExpressAccessors<
set( set(
custom?: ( custom?: (
list: Element[], list: Element[],
body: RequestBody body: RequestBody,
id: number
) => Promise<CustomSetReturn<Element>> | CustomSetReturn<Element> ) => Promise<CustomSetReturn<Element>> | CustomSetReturn<Element>
) { ) {
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => { return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
@ -81,8 +87,9 @@ export default class ExpressAccessors<
await this.sheet.set(request.body) await this.sheet.set(request.body)
response.status(200) response.status(200)
} else { } else {
const memberId = response?.locals?.jwt?.id || -1
const list = (await this.sheet.getList()) || [] const list = (await this.sheet.getList()) || []
const { toDatabase, toCaller } = await custom(list, request.body) const { toDatabase, toCaller } = await custom(list, request.body, memberId)
if (toDatabase !== undefined) { if (toDatabase !== undefined) {
await this.sheet.set(toDatabase) await this.sheet.set(toDatabase)
} }

View File

@ -3,7 +3,13 @@ import bcrypt from "bcrypt"
import sgMail from "@sendgrid/mail" import sgMail from "@sendgrid/mail"
import ExpressAccessors, { RequestBody } from "./expressAccessors" import ExpressAccessors, { RequestBody } from "./expressAccessors"
import { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers" import {
Volunteer,
VolunteerWithoutId,
VolunteerLogin,
VolunteerNotifs,
translationVolunteer,
} from "../../services/volunteers"
import { canonicalEmail } from "../../utils/standardization" import { canonicalEmail } from "../../utils/standardization"
import { getJwt } from "../secure" import { getJwt } from "../secure"
@ -14,11 +20,11 @@ const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
) )
export const volunteerListGet = expressAccessor.listGet() export const volunteerListGet = expressAccessor.listGet()
export const volunteerGet = expressAccessor.get()
export const volunteerAdd = expressAccessor.add() export const volunteerAdd = expressAccessor.add()
export const volunteerSet = expressAccessor.set() export const volunteerSet = expressAccessor.set()
export const volunteerLogin = expressAccessor.get(async (list: Volunteer[], body: RequestBody) => { export const volunteerLogin = expressAccessor.get(
async (list: Volunteer[], body: RequestBody): Promise<VolunteerLogin> => {
const volunteer = getByEmail(list, body.email) const volunteer = getByEmail(list, body.email)
if (!volunteer) { if (!volunteer) {
throw Error("Il n'y a aucun bénévole avec cet email") throw Error("Il n'y a aucun bénévole avec cet email")
@ -39,13 +45,14 @@ export const volunteerLogin = expressAccessor.get(async (list: Volunteer[], body
} }
} }
const jwt = await getJwt(volunteer.email) const jwt = await getJwt(volunteer.id)
return { return {
firstname: volunteer.firstname, id: volunteer.id,
jwt, jwt,
} }
}) }
)
const lastForgot: { [id: string]: number } = {} const lastForgot: { [id: string]: number } = {}
export const volunteerForgot = expressAccessor.set(async (list: Volunteer[], body: RequestBody) => { export const volunteerForgot = expressAccessor.set(async (list: Volunteer[], body: RequestBody) => {
@ -78,6 +85,29 @@ export const volunteerForgot = expressAccessor.set(async (list: Volunteer[], bod
} }
}) })
export const volunteerNotifsSet = expressAccessor.set(
async (list: Volunteer[], body: RequestBody, id: number) => {
const volunteer = list.find((v) => v.id === id)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${id}`)
}
const newVolunteer = _.cloneDeep(volunteer)
_.assign(newVolunteer, _.pick(body[1], _.keys(newVolunteer)))
return {
toDatabase: newVolunteer,
toCaller: {
id: newVolunteer.id,
firstname: newVolunteer.firstname,
adult: newVolunteer.adult,
active: newVolunteer.active,
hiddenNotifs: newVolunteer.hiddenNotifs,
} as VolunteerNotifs,
}
}
)
function getByEmail(list: Volunteer[], rawEmail: string): Volunteer | undefined { function getByEmail(list: Volunteer[], rawEmail: string): Volunteer | undefined {
const email = canonicalEmail(rawEmail || "") const email = canonicalEmail(rawEmail || "")
const volunteer = list.find((v) => canonicalEmail(v.email) === email) const volunteer = list.find((v) => canonicalEmail(v.email) === email)

View File

@ -1,6 +1,7 @@
import path from "path" import path from "path"
import express, { RequestHandler } from "express" import express, { RequestHandler } from "express"
import logger from "morgan" import logger from "morgan"
import cookieParser from "cookie-parser"
import compression from "compression" import compression from "compression"
import helmet from "helmet" import helmet from "helmet"
import hpp from "hpp" import hpp from "hpp"
@ -19,7 +20,12 @@ import { secure } from "./secure"
import { javGameListGet } from "./gsheets/javGames" import { javGameListGet } from "./gsheets/javGames"
import { wishListGet, wishAdd } from "./gsheets/wishes" import { wishListGet, wishAdd } from "./gsheets/wishes"
import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers" import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers"
import { volunteerGet, volunteerSet, volunteerLogin, volunteerForgot } from "./gsheets/volunteers" import {
volunteerNotifsSet,
volunteerSet,
volunteerLogin,
volunteerForgot,
} from "./gsheets/volunteers"
import config from "../config" import config from "../config"
const app = express() const app = express()
@ -45,6 +51,7 @@ app.use(express.static(path.resolve(process.cwd(), "public")))
if (__DEV__) devServer(app) if (__DEV__) devServer(app)
app.use(express.json()) app.use(express.json())
app.use(cookieParser())
/** /**
* APIs * APIs
@ -59,8 +66,9 @@ app.post("/VolunteerLogin", volunteerLogin)
app.post("/VolunteerForgot", volunteerForgot) app.post("/VolunteerForgot", volunteerForgot)
// Secured APIs // Secured APIs
app.get("/VolunteerGet", secure as RequestHandler, volunteerGet)
app.post("/VolunteerSet", secure as RequestHandler, volunteerSet) app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
// UNSAFE app.post("/VolunteerGet", secure as RequestHandler, volunteerGet)
app.post("/VolunteerNotifsSet", secure as RequestHandler, volunteerNotifsSet)
// Use React server-side rendering middleware // Use React server-side rendering middleware
app.get("*", ssr) app.get("*", ssr)

View File

@ -2,7 +2,6 @@ import { NextFunction, Request, Response } from "express"
import path from "path" import path from "path"
import { promises as fs } from "fs" import { promises as fs } from "fs"
import { verify, sign } from "jsonwebtoken" import { verify, sign } from "jsonwebtoken"
import { canonicalEmail } from "../utils/standardization"
import config from "../config" import config from "../config"
@ -20,16 +19,17 @@ export function secure(request: AuthorizedRequest, response: Response, next: Nex
} }
const rawToken = request.headers.authorization const rawToken = request.headers.authorization
const token = rawToken && rawToken.split(/\s/)[1] const token1 = rawToken && rawToken.split(/\s/)[1]
const token2 = request.cookies?.jwt
const token = token1 || token2
verify(token, cachedSecret, (tokenError: any, decoded: any) => { verify(token, cachedSecret, (tokenError: any, decoded: any) => {
if (tokenError) { if (tokenError) {
response.status(403).json({ response.status(200).json({
error: "Invalid token, please Log in first, criterion auth", error: "Accès interdit sans identification",
}) })
return return
} }
decoded.user.replace(/@gmailcom$/, "@gmail.com")
response.locals.jwt = decoded response.locals.jwt = decoded
next() next()
}) })
@ -50,9 +50,9 @@ async function getSecret() {
return cachedSecret return cachedSecret
} }
export async function getJwt(email: string): Promise<string> { export async function getJwt(id: number): Promise<string> {
const jwt = sign( const jwt = sign(
{ user: canonicalEmail(email), permissions: [] }, { id },
await getSecret() await getSecret()
// __TEST__ // __TEST__
// ? undefined // ? undefined

View File

@ -12,9 +12,11 @@ import { Action } from "@reduxjs/toolkit"
import createStore from "../store" import createStore from "../store"
import renderHtml from "./renderHtml" import renderHtml from "./renderHtml"
import routes from "../routes" import routes from "../routes"
import { getCookieJWT } from "../services/auth"
export default async (req: Request, res: Response, next: NextFunction): Promise<void> => { export default async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const { store } = createStore({ url: req.url }) const { jwt, id } = getCookieJWT(req.headers.cookie)
const { store } = createStore({ url: req.url, jwt, id })
// The method for loading data from server-side // The method for loading data from server-side
const loadBranchData = (): Promise<any> => { const loadBranchData = (): Promise<any> => {

View File

@ -1,4 +1,5 @@
import axios from "axios" import axios from "axios"
import _ from "lodash"
import config from "../config" import config from "../config"
import { axiosConfig } from "./auth" import { axiosConfig } from "./auth"
@ -123,7 +124,9 @@ export default class ServiceAccessors<
} }
} }
customPost(apiName: string): (params: any) => Promise<{ customPost<InputElements extends Array<any>>(
apiName: string
): (...params: InputElements) => Promise<{
data?: any data?: any
error?: Error error?: Error
}> { }> {
@ -131,7 +134,7 @@ export default class ServiceAccessors<
data?: any data?: any
error?: Error error?: Error
} }
return async (params: any): Promise<ElementGetResponse> => { return async (...params: InputElements): Promise<ElementGetResponse> => {
try { try {
const { data } = await axios.post( const { data } = await axios.post(
`${config.API_URL}/${this.elementName}${apiName}`, `${config.API_URL}/${this.elementName}${apiName}`,
@ -147,4 +150,36 @@ export default class ServiceAccessors<
} }
} }
} }
securedCustomPost<InputElements extends Array<any>>(
apiName: string
): (
jwt: string,
...params: InputElements
) => Promise<{
data?: any
error?: Error
}> {
interface ElementGetResponse {
data?: any
error?: Error
}
return async (jwt: string, ...params: InputElements): Promise<ElementGetResponse> => {
try {
const auth = { headers: { Authorization: `Bearer ${jwt}` } }
const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig)
const { data } = await axios.post(
`${config.API_URL}/${this.elementName}${apiName}`,
params,
fullAxiosConfig
)
if (data.error) {
throw Error(data.error)
}
return { data }
} catch (error) {
return { error: error as Error }
}
}
}
} }

View File

@ -1,4 +1,7 @@
import { AxiosRequestConfig } from "axios" import { AxiosRequestConfig } from "axios"
import Cookies from "js-cookie"
import { VolunteerLogin } from "./volunteers"
const storage: any = localStorage const storage: any = localStorage
@ -6,17 +9,30 @@ export const axiosConfig: AxiosRequestConfig = {
headers: {}, headers: {},
} }
const jwt: string | null = storage?.getItem("id_token") export function setJWT(token: string, id: number): void {
if (jwt) {
setJWT(jwt)
}
export function setJWT(token: string): void {
axiosConfig.headers.Authorization = `Bearer ${token}` axiosConfig.headers.Authorization = `Bearer ${token}`
storage?.setItem("id_token", token) storage?.setItem("jwt", token)
storage?.setItem("id", id)
Cookies.set("jwt", token)
Cookies.set("id", `${id}`)
} }
export function unsetJWT(): void { export function unsetJWT(): void {
delete axiosConfig.headers.Authorization delete axiosConfig.headers.Authorization
storage?.removeItem("id_token") storage?.removeItem("jwt")
storage?.removeItem("id")
Cookies.remove("jwt")
Cookies.remove("id")
}
export function getCookieJWT(cookie = ""): VolunteerLogin {
const cookies = cookie
.split(";")
.reduce((res: { [cookieName: string]: string }, el: string) => {
const [k, v] = el.split("=")
res[k.trim()] = v
return res
}, {})
return { jwt: cookies.jwt, id: +cookies.id }
} }

View File

@ -19,7 +19,9 @@ export class Volunteer {
privileges = 0 privileges = 0
active = 0 active = ""
hiddenNotifs: number[] = []
created = new Date() created = new Date()
@ -39,6 +41,7 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
adult: "majeur", adult: "majeur",
privileges: "privilege", privileges: "privilege",
active: "actif", active: "actif",
hiddenNotifs: "notifsCachees",
created: "creation", created: "creation",
password1: "passe1", password1: "passe1",
password2: "passe2", password2: "passe2",
@ -46,6 +49,23 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
const elementName = "Volunteer" const elementName = "Volunteer"
export const volunteerExample: Volunteer = {
id: 1,
firstname: "Aupeix",
lastname: "Amélie",
email: "pakouille.lakouille@yahoo.fr",
mobile: "0675650392",
photo: "images/volunteers/$taille/amélie_aupeix.jpg",
food: "Végétarien",
adult: 1,
privileges: 0,
active: "inconnu",
hiddenNotifs: [],
created: new Date(0),
password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
}
export const emailRegexp = export const emailRegexp =
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
export const passwordMinLength = 4 export const passwordMinLength = 4
@ -60,12 +80,23 @@ export const volunteerAdd = serviceAccessors.add()
export const volunteerSet = serviceAccessors.set() export const volunteerSet = serviceAccessors.set()
export interface VolunteerLogin { export interface VolunteerLogin {
firstname: string id: number
jwt: string jwt: string
} }
export const volunteerLogin = serviceAccessors.customPost("Login") export const volunteerLogin =
serviceAccessors.customPost<[{ email: string; password: string }]>("Login")
export interface VolunteerForgot { export interface VolunteerForgot {
message: string message: string
} }
export const volunteerForgot = serviceAccessors.customPost("Forgot") export const volunteerForgot = serviceAccessors.customPost<[{ email: string }]>("Forgot")
export interface VolunteerNotifs {
id: number
firstname: string
adult: number
active: string
hiddenNotifs: number[]
}
export const volunteerNotifsSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerNotifs>]>("NotifsSet")

View File

@ -8,25 +8,11 @@ import volunteer, {
fetchVolunteer, fetchVolunteer,
initialState, initialState,
} from "../volunteer" } from "../volunteer"
import { Volunteer } from "../../services/volunteers" import { Volunteer, volunteerExample } from "../../services/volunteers"
jest.mock("axios") jest.mock("axios")
const mockData: Volunteer = { const mockData: Volunteer = volunteerExample
id: 1,
lastname: "Aupeix",
firstname: "Amélie",
email: "pakouille.lakouille@yahoo.fr",
mobile: "0675650392",
photo: "images/volunteers/$taille/amélie_aupeix.jpg",
food: "Végétarien",
adult: 1,
privileges: 0,
active: 0,
created: new Date(0),
password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
}
const { id } = mockData const { id } = mockData
const mockError = "Oops! Something went wrong." const mockError = "Oops! Something went wrong."

View File

@ -9,27 +9,11 @@ import volunteerList, {
getFailure, getFailure,
fetchVolunteerList, fetchVolunteerList,
} from "../volunteerList" } from "../volunteerList"
import { Volunteer } from "../../services/volunteers" import { Volunteer, volunteerExample } from "../../services/volunteers"
jest.mock("axios") jest.mock("axios")
const mockData: Volunteer[] = [ const mockData: Volunteer[] = [volunteerExample]
{
id: 1,
lastname: "Aupeix",
firstname: "Amélie",
email: "pakouille.lakouille@yahoo.fr",
mobile: "0675650392",
photo: "images/volunteers/$taille/amélie_aupeix.jpg",
food: "Végétarien",
adult: 1,
privileges: 0,
active: 0,
created: new Date(0),
password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
},
]
const mockError = "Oops! Something went wrong." const mockError = "Oops! Something went wrong."
describe("volunteerList reducer", () => { describe("volunteerList reducer", () => {

35
src/store/auth.ts Normal file
View File

@ -0,0 +1,35 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { AppState } from "."
// Define a type for the slice state
interface AuthState {
id: number
jwt: string
}
// Define the initial state using that type
const initialState: AuthState = {
id: 0,
jwt: "",
}
export const auth = createSlice({
name: "auth",
initialState,
reducers: {
setCurrentUser: (state, action: PayloadAction<AuthState>) => {
state.id = action.payload.id
state.jwt = action.payload.jwt
},
logoutUser: (state) => {
state.id = 0
state.jwt = ""
},
},
})
export const { setCurrentUser, logoutUser } = auth.actions
export const selectCount = (state: AppState): AuthState => state.auth
export default auth.reducer

View File

@ -2,18 +2,22 @@ import { createMemoryHistory, createBrowserHistory } from "history"
import { Action, configureStore, EntityState } from "@reduxjs/toolkit" import { Action, configureStore, EntityState } from "@reduxjs/toolkit"
import { ThunkAction } from "redux-thunk" import { ThunkAction } from "redux-thunk"
import { routerMiddleware } from "connected-react-router" import { routerMiddleware } from "connected-react-router"
import Cookies from "js-cookie"
import createRootReducer from "./rootReducer" import createRootReducer from "./rootReducer"
import { StateRequest } from "./utils" import { StateRequest } from "./utils"
import { setCurrentUser, logoutUser } from "./auth"
interface Arg { interface Arg {
initialState?: typeof window.__INITIAL_STATE__ initialState?: typeof window.__INITIAL_STATE__
url?: string url?: string
jwt?: string
id?: number
} }
// Use inferred return type for making correctly Redux types // Use inferred return type for making correctly Redux types
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
const createStore = ({ initialState, url }: Arg = {}) => { const createStore = ({ initialState, url, jwt, id }: Arg = {}) => {
const history = __SERVER__ const history = __SERVER__
? createMemoryHistory({ initialEntries: [url || "/"] }) ? createMemoryHistory({ initialEntries: [url || "/"] })
: createBrowserHistory() : createBrowserHistory()
@ -28,10 +32,20 @@ const createStore = ({ initialState, url }: Arg = {}) => {
devTools: __DEV__, devTools: __DEV__,
}) })
if (jwt && id) {
store.dispatch(setCurrentUser({ jwt, id }))
} else {
store.dispatch(logoutUser())
}
return { store, history } return { store, history }
} }
const { store } = createStore() const storage: any = localStorage
const id = +(Cookies.get("id") || storage?.getItem("id"))
const jwt = Cookies.get("jwt") || storage?.getItem("jwt")
const { store } = createStore({ id, jwt })
export type AppState = ReturnType<typeof store.getState> export type AppState = ReturnType<typeof store.getState>

View File

@ -1,6 +1,7 @@
import { History } from "history" import { History } from "history"
import { connectRouter } from "connected-react-router" import { connectRouter } from "connected-react-router"
import auth from "./auth"
import wishAdd from "./wishAdd" import wishAdd from "./wishAdd"
import wishList from "./wishList" import wishList from "./wishList"
import javGameList from "./javGameList" import javGameList from "./javGameList"
@ -10,12 +11,14 @@ import volunteerList from "./volunteerList"
import volunteerSet from "./volunteerSet" import volunteerSet from "./volunteerSet"
import volunteerLogin from "./volunteerLogin" import volunteerLogin from "./volunteerLogin"
import volunteerForgot from "./volunteerForgot" import volunteerForgot from "./volunteerForgot"
import volunteerNotifsSet from "./volunteerNotifsSet"
import preVolunteerAdd from "./preVolunteerAdd" import preVolunteerAdd from "./preVolunteerAdd"
import preVolunteerCount from "./preVolunteerCount" import preVolunteerCount from "./preVolunteerCount"
// Use inferred return type for making correctly Redux types // Use inferred return type for making correctly Redux types
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export default (history: History) => ({ export default (history: History) => ({
auth,
wishAdd, wishAdd,
wishList, wishList,
javGameList, javGameList,
@ -25,6 +28,7 @@ export default (history: History) => ({
volunteerSet, volunteerSet,
volunteerLogin, volunteerLogin,
volunteerForgot, volunteerForgot,
volunteerNotifsSet,
preVolunteerAdd, preVolunteerAdd,
preVolunteerCount, preVolunteerCount,
router: connectRouter(history) as any, router: connectRouter(history) as any,

View File

@ -1,7 +1,7 @@
import { ActionCreatorWithoutPayload, ActionCreatorWithPayload } from "@reduxjs/toolkit" import { ActionCreatorWithoutPayload, ActionCreatorWithPayload } from "@reduxjs/toolkit"
import { toast } from "react-toastify" import { toast } from "react-toastify"
import { AppThunk } from "." import { AppThunk, AppDispatch } from "."
export interface StateRequest { export interface StateRequest {
readyStatus: "idle" | "request" | "success" | "failure" readyStatus: "idle" | "request" | "success" | "failure"
@ -32,8 +32,8 @@ export function toastSuccess(message: string): void {
}) })
} }
export function elementFetch<Element>( export function elementFetch<Element, ServiceInput extends Array<any>>(
elementService: (...idArgs: any[]) => Promise<{ elementService: (...idArgs: ServiceInput) => Promise<{
data?: Element | undefined data?: Element | undefined
error?: Error | undefined error?: Error | undefined
}>, }>,
@ -41,9 +41,9 @@ export function elementFetch<Element>(
getSuccess: ActionCreatorWithPayload<Element, string>, getSuccess: ActionCreatorWithPayload<Element, string>,
getFailure: ActionCreatorWithPayload<string, string>, getFailure: ActionCreatorWithPayload<string, string>,
errorMessage?: (error: Error) => void, errorMessage?: (error: Error) => void,
successMessage?: (data: Element) => void successMessage?: (data: Element, dispatch: AppDispatch) => void
): (...idArgs: any[]) => AppThunk { ): (...idArgs: ServiceInput) => AppThunk {
return (...idArgs: any[]): AppThunk => return (...idArgs: ServiceInput): AppThunk =>
async (dispatch) => { async (dispatch) => {
dispatch(getRequesting()) dispatch(getRequesting())
@ -54,7 +54,7 @@ export function elementFetch<Element>(
errorMessage?.(error) errorMessage?.(error)
} else { } else {
dispatch(getSuccess(data as Element)) dispatch(getSuccess(data as Element))
successMessage?.(data as Element) successMessage?.(data as Element, dispatch)
} }
} }
} }

View File

@ -3,6 +3,9 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { StateRequest, elementFetch } from "./utils" import { StateRequest, elementFetch } from "./utils"
import { VolunteerLogin, volunteerLogin } from "../services/volunteers" import { VolunteerLogin, volunteerLogin } from "../services/volunteers"
import { setJWT } from "../services/auth" import { setJWT } from "../services/auth"
import { AppDispatch } from "."
import { setCurrentUser } from "./auth"
import { fetchVolunteerNotifsSet } from "./volunteerNotifsSet"
type StateVolunteer = { entity?: VolunteerLogin } & StateRequest type StateVolunteer = { entity?: VolunteerLogin } & StateRequest
@ -31,13 +34,15 @@ const volunteerLoginSlice = createSlice({
export default volunteerLoginSlice.reducer export default volunteerLoginSlice.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerLoginSlice.actions export const { getRequesting, getSuccess, getFailure } = volunteerLoginSlice.actions
export const fetchVolunteerLogin = elementFetch( export const fetchVolunteerLogin = elementFetch<VolunteerLogin, Parameters<typeof volunteerLogin>>(
volunteerLogin, volunteerLogin,
getRequesting, getRequesting,
getSuccess, getSuccess,
getFailure, getFailure,
undefined, undefined,
(login: VolunteerLogin) => { (login: VolunteerLogin, dispatch: AppDispatch) => {
setJWT(login.jwt) setJWT(login.jwt, login.id)
dispatch(setCurrentUser(login))
dispatch(fetchVolunteerNotifsSet(login.jwt, login.id, {}))
} }
) )

View File

@ -0,0 +1,57 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { StateRequest, toastError, elementFetch } from "./utils"
import { VolunteerNotifs, volunteerNotifsSet } from "../services/volunteers"
import { AppThunk, AppState } from "."
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) {
;({ id, jwt } = getState().auth)
}
if (shouldFetchVolunteerNotifsSet(getState(), id))
return dispatch(fetchVolunteerNotifsSet(jwt, id, notif))
return null
}

View File

@ -1566,6 +1566,13 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/cookie-parser@^1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5"
integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg==
dependencies:
"@types/express" "*"
"@types/css-minimizer-webpack-plugin@^3.0.2": "@types/css-minimizer-webpack-plugin@^3.0.2":
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.2.tgz#ed58bbbde8a7b7591118aa93d8f8d0cdc0cc6173" resolved "https://registry.yarnpkg.com/@types/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.2.tgz#ed58bbbde8a7b7591118aa93d8f8d0cdc0cc6173"
@ -1725,6 +1732,11 @@
jest-diff "^26.0.0" jest-diff "^26.0.0"
pretty-format "^26.0.0" pretty-format "^26.0.0"
"@types/js-cookie@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.1.tgz#04aa743e2e0a85a22ee9aa61f6591a8bc19b5d68"
integrity sha512-7wg/8gfHltklehP+oyJnZrz9XBuX5ZPP4zB6UsI84utdlkRYLnOm2HfpLXazTwZA+fpGn0ir8tGNgVnMEleBGQ==
"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.9" version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
@ -3500,6 +3512,14 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
dependencies: dependencies:
safe-buffer "~5.1.1" safe-buffer "~5.1.1"
cookie-parser@^1.4.6:
version "1.4.6"
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594"
integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==
dependencies:
cookie "0.4.1"
cookie-signature "1.0.6"
cookie-signature@1.0.6: cookie-signature@1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@ -3510,6 +3530,11 @@ cookie@0.4.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
cookie@0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
core-js-compat@^3.18.0, core-js-compat@^3.19.1: core-js-compat@^3.18.0, core-js-compat@^3.19.1:
version "3.19.1" version "3.19.1"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.19.1.tgz#fe598f1a9bf37310d77c3813968e9f7c7bb99476" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.19.1.tgz#fe598f1a9bf37310d77c3813968e9f7c7bb99476"
@ -6790,6 +6815,11 @@ js-base64@^2.1.8:
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
js-cookie@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"