mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-08 08:34:20 +02:00
Add notifications to home page
This commit is contained in:
parent
adde4f366e
commit
5cbb5811ec
@ -74,6 +74,8 @@
|
||||
"@loadable/server": "^5.15.0",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"@sendgrid/mail": "^7.6.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/lodash": "^4.14.177",
|
||||
"autoprefixer": "^10.2.6",
|
||||
"axios": "^0.21.1",
|
||||
@ -81,6 +83,7 @@
|
||||
"chalk": "^4.1.1",
|
||||
"compression": "^1.7.4",
|
||||
"connected-react-router": "^6.9.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"core-js": "^3.15.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^4.17.1",
|
||||
@ -93,6 +96,7 @@
|
||||
"hpp": "^0.2.3",
|
||||
"html-minifier": "^4.0.0",
|
||||
"https": "^1.0.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"morgan": "^1.10.0",
|
||||
|
@ -3,13 +3,19 @@ import { Provider } from "react-redux"
|
||||
import { ConnectedRouter } from "connected-react-router"
|
||||
import { RouteConfig, renderRoutes } from "react-router-config"
|
||||
import { loadableReady } from "@loadable/component"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
import createStore from "../store"
|
||||
import routes from "../routes"
|
||||
|
||||
const storage: any = localStorage
|
||||
|
||||
// Get the initial state from server-side rendering
|
||||
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[]) =>
|
||||
ReactDOM.hydrate(
|
||||
|
@ -28,7 +28,7 @@ const LoginForm = ({ dispatch, error }: Props): JSX.Element => {
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<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 className={styles.formLine} key="line-email">
|
||||
<label htmlFor="email">Email</label>
|
162
src/components/Notifications/index.tsx
Normal file
162
src/components/Notifications/index.tsx
Normal 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)
|
41
src/components/Notifications/styles.module.scss
Executable file
41
src/components/Notifications/styles.module.scss
Executable 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;
|
||||
}
|
@ -12,7 +12,7 @@ interface Props {
|
||||
preVolunteerCount: number | undefined
|
||||
}
|
||||
|
||||
const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => {
|
||||
const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => {
|
||||
const [firstname, setFirstname] = useState("")
|
||||
const [lastname, setLastname] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
@ -97,7 +97,7 @@ const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => {
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<dl className={styles.registerIntro} key="register-intro">
|
||||
<dl className={styles.preRegisterIntro} key="preRegister-intro">
|
||||
<dt>Qu'est-ce que Paris est Ludique ?</dt>
|
||||
<dd>
|
||||
<p>
|
||||
@ -253,4 +253,4 @@ const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => {
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(RegisterForm)
|
||||
export default memo(PreRegisterForm)
|
@ -1,7 +1,7 @@
|
||||
@import "../../theme/variables";
|
||||
@import "../../theme/mixins";
|
||||
|
||||
.registerIntro {
|
||||
.preRegisterIntro {
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { render } from "@testing-library/react"
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
import { volunteerExample } from "../../../services/volunteers"
|
||||
|
||||
import VolunteerInfo from "../index"
|
||||
|
||||
@ -10,23 +11,7 @@ describe("<VolunteerInfo />", () => {
|
||||
it("renders", () => {
|
||||
const tree = render(
|
||||
<MemoryRouter>
|
||||
<VolunteerInfo
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
<VolunteerInfo item={volunteerExample} />
|
||||
</MemoryRouter>
|
||||
).container.firstChild
|
||||
|
||||
|
@ -4,33 +4,14 @@
|
||||
import { render } from "@testing-library/react"
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
|
||||
import { volunteerExample } from "../../../services/volunteers"
|
||||
import VolunteerList from "../index"
|
||||
|
||||
describe("<VolunteerList />", () => {
|
||||
it("renders", () => {
|
||||
const tree = render(
|
||||
<MemoryRouter>
|
||||
<VolunteerList
|
||||
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",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<VolunteerList items={[volunteerExample]} />
|
||||
</MemoryRouter>
|
||||
).container.firstChild
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { render } from "@testing-library/react"
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
import { volunteerExample } from "../../../services/volunteers"
|
||||
|
||||
import VolunteerSet from "../index"
|
||||
|
||||
@ -11,24 +12,7 @@ describe("<SetVolunteer />", () => {
|
||||
const dispatch = jest.fn()
|
||||
const tree = render(
|
||||
<MemoryRouter>
|
||||
<VolunteerSet
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
<VolunteerSet dispatch={dispatch} volunteer={volunteerExample} />
|
||||
</MemoryRouter>
|
||||
).container.firstChild
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import LoginForm from "./LoginForm"
|
||||
import Notifications from "./Notifications"
|
||||
import VolunteerList from "./VolunteerList"
|
||||
import JavGameList from "./JavGameList"
|
||||
import VolunteerInfo from "./VolunteerInfo"
|
||||
@ -5,9 +7,11 @@ import VolunteerSet from "./VolunteerSet"
|
||||
import ErrorBoundary from "./ErrorBoundary"
|
||||
import Loading from "./Loading"
|
||||
import WishAdd from "./WishAdd"
|
||||
import RegisterForm from "./RegisterForm"
|
||||
import PreRegisterForm from "./PreRegisterForm"
|
||||
|
||||
export {
|
||||
LoginForm,
|
||||
Notifications,
|
||||
VolunteerList,
|
||||
JavGameList,
|
||||
VolunteerInfo,
|
||||
@ -15,5 +19,5 @@ export {
|
||||
ErrorBoundary,
|
||||
Loading,
|
||||
WishAdd,
|
||||
RegisterForm,
|
||||
PreRegisterForm,
|
||||
}
|
||||
|
50
src/pages/Home/Home.tsx
Normal file
50
src/pages/Home/Home.tsx
Normal 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'informer sur le bénévolat </Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch server-side data here
|
||||
export const loadData = (): AppThunk[] => [fetchVolunteerNotifsSetIfNeed()]
|
||||
|
||||
export default memo(HomePage)
|
@ -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)
|
@ -1,15 +1,16 @@
|
||||
import loadable from "@loadable/component"
|
||||
|
||||
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 />,
|
||||
})
|
||||
|
||||
export default (props: Props): JSX.Element => (
|
||||
<ErrorBoundary>
|
||||
<Home {...props} />
|
||||
<HomePage {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
export { loadData }
|
||||
|
@ -4,6 +4,14 @@
|
||||
@include page-wrapper-center;
|
||||
}
|
||||
|
||||
.homeContent {
|
||||
@include page-content-wrapper(600px);
|
||||
.notificationsContent {
|
||||
@include page-content-wrapper;
|
||||
}
|
||||
|
||||
.loginContent {
|
||||
@include page-content-wrapper;
|
||||
}
|
||||
|
||||
.preRegisterContent {
|
||||
@include page-content-wrapper;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import React, { memo } from "react"
|
||||
import { Helmet } from "react-helmet"
|
||||
|
||||
import { AppState } from "../../store"
|
||||
import LoginForm from "../../components/LoginForm/LoginForm"
|
||||
import { LoginForm } from "../../components"
|
||||
import styles from "./styles.module.scss"
|
||||
|
||||
export type Props = RouteComponentProps
|
||||
|
@ -5,7 +5,7 @@ import { Helmet } from "react-helmet"
|
||||
|
||||
import { AppState, AppThunk, ValueRequest } from "../../store"
|
||||
import { fetchPreVolunteerCountIfNeed } from "../../store/preVolunteerCount"
|
||||
import { RegisterForm } from "../../components"
|
||||
import { PreRegisterForm } from "../../components"
|
||||
import styles from "./styles.module.scss"
|
||||
|
||||
export type Props = RouteComponentProps
|
||||
@ -29,14 +29,14 @@ function useList(
|
||||
|
||||
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 => (
|
||||
<div className={styles.registerPage}>
|
||||
<div className={styles.registerContent}>
|
||||
<Helmet title="RegisterPage" />
|
||||
const PreRegisterPage: FC<Props> = (): JSX.Element => (
|
||||
<div className={styles.preRegisterPage}>
|
||||
<div className={styles.preRegisterContent}>
|
||||
<Helmet title="PreRegisterPage" />
|
||||
{useList((state: AppState) => state.preVolunteerCount, fetchPreVolunteerCountIfNeed)()}
|
||||
</div>
|
||||
</div>
|
||||
@ -45,4 +45,4 @@ const RegisterPage: FC<Props> = (): JSX.Element => (
|
||||
// Fetch server-side data here
|
||||
export const loadData = (): AppThunk[] => [fetchPreVolunteerCountIfNeed()]
|
||||
|
||||
export default memo(RegisterPage)
|
||||
export default memo(PreRegisterPage)
|
@ -1,14 +1,16 @@
|
||||
import loadable from "@loadable/component"
|
||||
|
||||
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 />,
|
||||
})
|
||||
|
||||
export default (props: Props): JSX.Element => (
|
||||
<ErrorBoundary>
|
||||
<RegisterPage {...props} />
|
||||
<PreRegister {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
export { loadData }
|
@ -1,9 +1,9 @@
|
||||
@import "../../theme/mixins";
|
||||
|
||||
.registerPage {
|
||||
.preRegisterPage {
|
||||
@include page-wrapper-center;
|
||||
}
|
||||
|
||||
.registerContent {
|
||||
.preRegisterContent {
|
||||
@include page-content-wrapper(600px);
|
||||
}
|
@ -2,11 +2,11 @@ import { RouteConfig } from "react-router-config"
|
||||
|
||||
import App from "../app"
|
||||
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 AsyncVolunteerPage, { loadData as loadVolunteerPageData } from "../pages/VolunteerPage"
|
||||
import Login from "../pages/Login"
|
||||
import Forgot from "../pages/Forgot"
|
||||
import Register from "../pages/Register"
|
||||
import NotFound from "../pages/NotFound"
|
||||
|
||||
export default [
|
||||
@ -16,7 +16,13 @@ export default [
|
||||
{
|
||||
path: "/",
|
||||
exact: true,
|
||||
component: Register,
|
||||
component: AsyncHome,
|
||||
loadData: loadHomeData,
|
||||
},
|
||||
{
|
||||
path: "/preRegister",
|
||||
component: AsyncPreRegisterPage,
|
||||
loadData: loadPreRegisterPage,
|
||||
},
|
||||
{
|
||||
path: "/VolunteerPage/:id",
|
||||
@ -31,11 +37,6 @@ export default [
|
||||
path: "/forgot",
|
||||
component: Forgot,
|
||||
},
|
||||
{
|
||||
path: "/home",
|
||||
component: AsyncHome,
|
||||
loadData: loadHomeData,
|
||||
},
|
||||
{
|
||||
path: "/wish",
|
||||
component: AsyncWish,
|
||||
|
@ -37,7 +37,7 @@ export default class ExpressAccessors<
|
||||
}
|
||||
|
||||
// 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> => {
|
||||
try {
|
||||
const list = (await this.sheet.getList()) || []
|
||||
@ -46,7 +46,12 @@ export default class ExpressAccessors<
|
||||
const id = parseInt(request.query.id as string, 10) || -1
|
||||
toCaller = list.find((e: Element) => e.id === id)
|
||||
} 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)
|
||||
} catch (e: any) {
|
||||
@ -72,7 +77,8 @@ export default class ExpressAccessors<
|
||||
set(
|
||||
custom?: (
|
||||
list: Element[],
|
||||
body: RequestBody
|
||||
body: RequestBody,
|
||||
id: number
|
||||
) => Promise<CustomSetReturn<Element>> | CustomSetReturn<Element>
|
||||
) {
|
||||
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
|
||||
@ -81,8 +87,9 @@ export default class ExpressAccessors<
|
||||
await this.sheet.set(request.body)
|
||||
response.status(200)
|
||||
} else {
|
||||
const memberId = response?.locals?.jwt?.id || -1
|
||||
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) {
|
||||
await this.sheet.set(toDatabase)
|
||||
}
|
||||
|
@ -3,7 +3,13 @@ import bcrypt from "bcrypt"
|
||||
import sgMail from "@sendgrid/mail"
|
||||
|
||||
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 { getJwt } from "../secure"
|
||||
|
||||
@ -14,38 +20,39 @@ const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
|
||||
)
|
||||
|
||||
export const volunteerListGet = expressAccessor.listGet()
|
||||
export const volunteerGet = expressAccessor.get()
|
||||
export const volunteerAdd = expressAccessor.add()
|
||||
export const volunteerSet = expressAccessor.set()
|
||||
|
||||
export const volunteerLogin = expressAccessor.get(async (list: Volunteer[], body: RequestBody) => {
|
||||
const volunteer = getByEmail(list, body.email)
|
||||
if (!volunteer) {
|
||||
throw Error("Il n'y a aucun bénévole avec cet email")
|
||||
}
|
||||
export const volunteerLogin = expressAccessor.get(
|
||||
async (list: Volunteer[], body: RequestBody): Promise<VolunteerLogin> => {
|
||||
const volunteer = getByEmail(list, body.email)
|
||||
if (!volunteer) {
|
||||
throw Error("Il n'y a aucun bénévole avec cet email")
|
||||
}
|
||||
|
||||
const password = body.password || ""
|
||||
const password1Match = await bcrypt.compare(
|
||||
password,
|
||||
volunteer.password1.replace(/^\$2y/, "$2a")
|
||||
)
|
||||
if (!password1Match) {
|
||||
const password2Match = await bcrypt.compare(
|
||||
const password = body.password || ""
|
||||
const password1Match = await bcrypt.compare(
|
||||
password,
|
||||
volunteer.password2.replace(/^\$2y/, "$2a")
|
||||
volunteer.password1.replace(/^\$2y/, "$2a")
|
||||
)
|
||||
if (!password2Match) {
|
||||
throw Error("Mauvais mot de passe pour cet email")
|
||||
if (!password1Match) {
|
||||
const password2Match = await bcrypt.compare(
|
||||
password,
|
||||
volunteer.password2.replace(/^\$2y/, "$2a")
|
||||
)
|
||||
if (!password2Match) {
|
||||
throw Error("Mauvais mot de passe pour cet email")
|
||||
}
|
||||
}
|
||||
|
||||
const jwt = await getJwt(volunteer.id)
|
||||
|
||||
return {
|
||||
id: volunteer.id,
|
||||
jwt,
|
||||
}
|
||||
}
|
||||
|
||||
const jwt = await getJwt(volunteer.email)
|
||||
|
||||
return {
|
||||
firstname: volunteer.firstname,
|
||||
jwt,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const lastForgot: { [id: string]: number } = {}
|
||||
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 {
|
||||
const email = canonicalEmail(rawEmail || "")
|
||||
const volunteer = list.find((v) => canonicalEmail(v.email) === email)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import path from "path"
|
||||
import express, { RequestHandler } from "express"
|
||||
import logger from "morgan"
|
||||
import cookieParser from "cookie-parser"
|
||||
import compression from "compression"
|
||||
import helmet from "helmet"
|
||||
import hpp from "hpp"
|
||||
@ -19,7 +20,12 @@ import { secure } from "./secure"
|
||||
import { javGameListGet } from "./gsheets/javGames"
|
||||
import { wishListGet, wishAdd } from "./gsheets/wishes"
|
||||
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"
|
||||
|
||||
const app = express()
|
||||
@ -45,6 +51,7 @@ app.use(express.static(path.resolve(process.cwd(), "public")))
|
||||
if (__DEV__) devServer(app)
|
||||
|
||||
app.use(express.json())
|
||||
app.use(cookieParser())
|
||||
|
||||
/**
|
||||
* APIs
|
||||
@ -59,8 +66,9 @@ app.post("/VolunteerLogin", volunteerLogin)
|
||||
app.post("/VolunteerForgot", volunteerForgot)
|
||||
|
||||
// Secured APIs
|
||||
app.get("/VolunteerGet", secure as RequestHandler, volunteerGet)
|
||||
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
|
||||
app.get("*", ssr)
|
||||
|
@ -2,7 +2,6 @@ import { NextFunction, Request, Response } from "express"
|
||||
import path from "path"
|
||||
import { promises as fs } from "fs"
|
||||
import { verify, sign } from "jsonwebtoken"
|
||||
import { canonicalEmail } from "../utils/standardization"
|
||||
|
||||
import config from "../config"
|
||||
|
||||
@ -20,16 +19,17 @@ export function secure(request: AuthorizedRequest, response: Response, next: Nex
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (tokenError) {
|
||||
response.status(403).json({
|
||||
error: "Invalid token, please Log in first, criterion auth",
|
||||
response.status(200).json({
|
||||
error: "Accès interdit sans identification",
|
||||
})
|
||||
return
|
||||
}
|
||||
decoded.user.replace(/@gmailcom$/, "@gmail.com")
|
||||
response.locals.jwt = decoded
|
||||
next()
|
||||
})
|
||||
@ -50,9 +50,9 @@ async function getSecret() {
|
||||
return cachedSecret
|
||||
}
|
||||
|
||||
export async function getJwt(email: string): Promise<string> {
|
||||
export async function getJwt(id: number): Promise<string> {
|
||||
const jwt = sign(
|
||||
{ user: canonicalEmail(email), permissions: [] },
|
||||
{ id },
|
||||
await getSecret()
|
||||
// __TEST__
|
||||
// ? undefined
|
||||
|
@ -12,9 +12,11 @@ import { Action } from "@reduxjs/toolkit"
|
||||
import createStore from "../store"
|
||||
import renderHtml from "./renderHtml"
|
||||
import routes from "../routes"
|
||||
import { getCookieJWT } from "../services/auth"
|
||||
|
||||
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
|
||||
const loadBranchData = (): Promise<any> => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import axios from "axios"
|
||||
import _ from "lodash"
|
||||
|
||||
import config from "../config"
|
||||
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
|
||||
error?: Error
|
||||
}> {
|
||||
@ -131,7 +134,7 @@ export default class ServiceAccessors<
|
||||
data?: any
|
||||
error?: Error
|
||||
}
|
||||
return async (params: any): Promise<ElementGetResponse> => {
|
||||
return async (...params: InputElements): Promise<ElementGetResponse> => {
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
`${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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { AxiosRequestConfig } from "axios"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
import { VolunteerLogin } from "./volunteers"
|
||||
|
||||
const storage: any = localStorage
|
||||
|
||||
@ -6,17 +9,30 @@ export const axiosConfig: AxiosRequestConfig = {
|
||||
headers: {},
|
||||
}
|
||||
|
||||
const jwt: string | null = storage?.getItem("id_token")
|
||||
if (jwt) {
|
||||
setJWT(jwt)
|
||||
}
|
||||
|
||||
export function setJWT(token: string): void {
|
||||
export function setJWT(token: string, id: number): void {
|
||||
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 {
|
||||
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 }
|
||||
}
|
||||
|
@ -19,7 +19,9 @@ export class Volunteer {
|
||||
|
||||
privileges = 0
|
||||
|
||||
active = 0
|
||||
active = ""
|
||||
|
||||
hiddenNotifs: number[] = []
|
||||
|
||||
created = new Date()
|
||||
|
||||
@ -39,6 +41,7 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
|
||||
adult: "majeur",
|
||||
privileges: "privilege",
|
||||
active: "actif",
|
||||
hiddenNotifs: "notifsCachees",
|
||||
created: "creation",
|
||||
password1: "passe1",
|
||||
password2: "passe2",
|
||||
@ -46,6 +49,23 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
|
||||
|
||||
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 =
|
||||
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
|
||||
export const passwordMinLength = 4
|
||||
@ -60,12 +80,23 @@ export const volunteerAdd = serviceAccessors.add()
|
||||
export const volunteerSet = serviceAccessors.set()
|
||||
|
||||
export interface VolunteerLogin {
|
||||
firstname: string
|
||||
id: number
|
||||
jwt: string
|
||||
}
|
||||
export const volunteerLogin = serviceAccessors.customPost("Login")
|
||||
export const volunteerLogin =
|
||||
serviceAccessors.customPost<[{ email: string; password: string }]>("Login")
|
||||
|
||||
export interface VolunteerForgot {
|
||||
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")
|
||||
|
@ -8,25 +8,11 @@ import volunteer, {
|
||||
fetchVolunteer,
|
||||
initialState,
|
||||
} from "../volunteer"
|
||||
import { Volunteer } from "../../services/volunteers"
|
||||
import { Volunteer, volunteerExample } from "../../services/volunteers"
|
||||
|
||||
jest.mock("axios")
|
||||
|
||||
const mockData: Volunteer = {
|
||||
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 mockData: Volunteer = volunteerExample
|
||||
const { id } = mockData
|
||||
const mockError = "Oops! Something went wrong."
|
||||
|
||||
|
@ -9,27 +9,11 @@ import volunteerList, {
|
||||
getFailure,
|
||||
fetchVolunteerList,
|
||||
} from "../volunteerList"
|
||||
import { Volunteer } from "../../services/volunteers"
|
||||
import { Volunteer, volunteerExample } from "../../services/volunteers"
|
||||
|
||||
jest.mock("axios")
|
||||
|
||||
const mockData: Volunteer[] = [
|
||||
{
|
||||
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 mockData: Volunteer[] = [volunteerExample]
|
||||
const mockError = "Oops! Something went wrong."
|
||||
|
||||
describe("volunteerList reducer", () => {
|
||||
|
35
src/store/auth.ts
Normal file
35
src/store/auth.ts
Normal 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
|
@ -2,18 +2,22 @@ import { createMemoryHistory, createBrowserHistory } from "history"
|
||||
import { Action, configureStore, EntityState } from "@reduxjs/toolkit"
|
||||
import { ThunkAction } from "redux-thunk"
|
||||
import { routerMiddleware } from "connected-react-router"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
import createRootReducer from "./rootReducer"
|
||||
import { StateRequest } from "./utils"
|
||||
import { setCurrentUser, logoutUser } from "./auth"
|
||||
|
||||
interface Arg {
|
||||
initialState?: typeof window.__INITIAL_STATE__
|
||||
url?: string
|
||||
jwt?: string
|
||||
id?: number
|
||||
}
|
||||
|
||||
// Use inferred return type for making correctly Redux 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__
|
||||
? createMemoryHistory({ initialEntries: [url || "/"] })
|
||||
: createBrowserHistory()
|
||||
@ -28,10 +32,20 @@ const createStore = ({ initialState, url }: Arg = {}) => {
|
||||
devTools: __DEV__,
|
||||
})
|
||||
|
||||
if (jwt && id) {
|
||||
store.dispatch(setCurrentUser({ jwt, id }))
|
||||
} else {
|
||||
store.dispatch(logoutUser())
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { History } from "history"
|
||||
import { connectRouter } from "connected-react-router"
|
||||
|
||||
import auth from "./auth"
|
||||
import wishAdd from "./wishAdd"
|
||||
import wishList from "./wishList"
|
||||
import javGameList from "./javGameList"
|
||||
@ -10,12 +11,14 @@ import volunteerList from "./volunteerList"
|
||||
import volunteerSet from "./volunteerSet"
|
||||
import volunteerLogin from "./volunteerLogin"
|
||||
import volunteerForgot from "./volunteerForgot"
|
||||
import volunteerNotifsSet from "./volunteerNotifsSet"
|
||||
import preVolunteerAdd from "./preVolunteerAdd"
|
||||
import preVolunteerCount from "./preVolunteerCount"
|
||||
|
||||
// Use inferred return type for making correctly Redux types
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export default (history: History) => ({
|
||||
auth,
|
||||
wishAdd,
|
||||
wishList,
|
||||
javGameList,
|
||||
@ -25,6 +28,7 @@ export default (history: History) => ({
|
||||
volunteerSet,
|
||||
volunteerLogin,
|
||||
volunteerForgot,
|
||||
volunteerNotifsSet,
|
||||
preVolunteerAdd,
|
||||
preVolunteerCount,
|
||||
router: connectRouter(history) as any,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ActionCreatorWithoutPayload, ActionCreatorWithPayload } from "@reduxjs/toolkit"
|
||||
import { toast } from "react-toastify"
|
||||
|
||||
import { AppThunk } from "."
|
||||
import { AppThunk, AppDispatch } from "."
|
||||
|
||||
export interface StateRequest {
|
||||
readyStatus: "idle" | "request" | "success" | "failure"
|
||||
@ -32,8 +32,8 @@ export function toastSuccess(message: string): void {
|
||||
})
|
||||
}
|
||||
|
||||
export function elementFetch<Element>(
|
||||
elementService: (...idArgs: any[]) => Promise<{
|
||||
export function elementFetch<Element, ServiceInput extends Array<any>>(
|
||||
elementService: (...idArgs: ServiceInput) => Promise<{
|
||||
data?: Element | undefined
|
||||
error?: Error | undefined
|
||||
}>,
|
||||
@ -41,9 +41,9 @@ export function elementFetch<Element>(
|
||||
getSuccess: ActionCreatorWithPayload<Element, string>,
|
||||
getFailure: ActionCreatorWithPayload<string, string>,
|
||||
errorMessage?: (error: Error) => void,
|
||||
successMessage?: (data: Element) => void
|
||||
): (...idArgs: any[]) => AppThunk {
|
||||
return (...idArgs: any[]): AppThunk =>
|
||||
successMessage?: (data: Element, dispatch: AppDispatch) => void
|
||||
): (...idArgs: ServiceInput) => AppThunk {
|
||||
return (...idArgs: ServiceInput): AppThunk =>
|
||||
async (dispatch) => {
|
||||
dispatch(getRequesting())
|
||||
|
||||
@ -54,7 +54,7 @@ export function elementFetch<Element>(
|
||||
errorMessage?.(error)
|
||||
} else {
|
||||
dispatch(getSuccess(data as Element))
|
||||
successMessage?.(data as Element)
|
||||
successMessage?.(data as Element, dispatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { StateRequest, elementFetch } from "./utils"
|
||||
import { VolunteerLogin, volunteerLogin } from "../services/volunteers"
|
||||
import { setJWT } from "../services/auth"
|
||||
import { AppDispatch } from "."
|
||||
import { setCurrentUser } from "./auth"
|
||||
import { fetchVolunteerNotifsSet } from "./volunteerNotifsSet"
|
||||
|
||||
type StateVolunteer = { entity?: VolunteerLogin } & StateRequest
|
||||
|
||||
@ -31,13 +34,15 @@ const volunteerLoginSlice = createSlice({
|
||||
export default volunteerLoginSlice.reducer
|
||||
export const { getRequesting, getSuccess, getFailure } = volunteerLoginSlice.actions
|
||||
|
||||
export const fetchVolunteerLogin = elementFetch(
|
||||
export const fetchVolunteerLogin = elementFetch<VolunteerLogin, Parameters<typeof volunteerLogin>>(
|
||||
volunteerLogin,
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
undefined,
|
||||
(login: VolunteerLogin) => {
|
||||
setJWT(login.jwt)
|
||||
(login: VolunteerLogin, dispatch: AppDispatch) => {
|
||||
setJWT(login.jwt, login.id)
|
||||
dispatch(setCurrentUser(login))
|
||||
dispatch(fetchVolunteerNotifsSet(login.jwt, login.id, {}))
|
||||
}
|
||||
)
|
||||
|
57
src/store/volunteerNotifsSet.ts
Normal file
57
src/store/volunteerNotifsSet.ts
Normal 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
|
||||
}
|
30
yarn.lock
30
yarn.lock
@ -1566,6 +1566,13 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "3.0.2"
|
||||
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"
|
||||
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":
|
||||
version "7.0.9"
|
||||
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:
|
||||
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:
|
||||
version "1.0.6"
|
||||
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"
|
||||
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:
|
||||
version "3.19.1"
|
||||
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"
|
||||
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:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
Loading…
x
Reference in New Issue
Block a user