mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-09 17:14:21 +02:00
Add db edit feature for admin
This commit is contained in:
parent
df33d3a951
commit
da643df6a6
34
src/components/Admin/DbEdit.tsx
Normal file
34
src/components/Admin/DbEdit.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { FC, memo } from "react"
|
||||||
|
import { useSelector } from "react-redux"
|
||||||
|
import withUserConnected from "../../utils/withUserConnected"
|
||||||
|
import { fetchVolunteerListIfNeed, selectVolunteerList } from "../../store/volunteerList"
|
||||||
|
import withUserRole from "../../utils/withUserRole"
|
||||||
|
import ROLES from "../../utils/roles.constants"
|
||||||
|
import MemberEdit from "./MemberEdit"
|
||||||
|
import useAction from "../../utils/useAction"
|
||||||
|
import { fetchVolunteerSetIfNeed } from "../../store/volunteerSet"
|
||||||
|
import { Volunteer } from "../../services/volunteers"
|
||||||
|
import styles from "./styles.module.scss"
|
||||||
|
|
||||||
|
const DbEdit: FC = (): JSX.Element => {
|
||||||
|
const volunteers = useSelector(selectVolunteerList)
|
||||||
|
const saveVolunteer = useAction(fetchVolunteerSetIfNeed)
|
||||||
|
if (!volunteers) {
|
||||||
|
return <>No member found</>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ul className={styles.list}>
|
||||||
|
{volunteers.map((volunteer: Volunteer) => (
|
||||||
|
<MemberEdit
|
||||||
|
key={volunteer.id}
|
||||||
|
saveVolunteer={saveVolunteer}
|
||||||
|
volunteer={volunteer}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withUserRole(ROLES.ADMIN, memo(withUserConnected(DbEdit)))
|
||||||
|
|
||||||
|
export const fetchFor = [fetchVolunteerListIfNeed]
|
31
src/components/Admin/GameDetailsUpdate.tsx
Normal file
31
src/components/Admin/GameDetailsUpdate.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { FC, memo, useCallback } from "react"
|
||||||
|
import { useSelector } from "react-redux"
|
||||||
|
import withUserConnected from "../../utils/withUserConnected"
|
||||||
|
import withUserRole from "../../utils/withUserRole"
|
||||||
|
import ROLES from "../../utils/roles.constants"
|
||||||
|
import { fetchGameDetailsUpdate } from "../../store/gameDetailsUpdate"
|
||||||
|
import { selectUserJwtToken } from "../../store/auth"
|
||||||
|
import useAction from "../../utils/useAction"
|
||||||
|
import styles from "./styles.module.scss"
|
||||||
|
import FormButton from "../Form/FormButton/FormButton"
|
||||||
|
|
||||||
|
const GameDetailsUpdate: FC = (): JSX.Element => {
|
||||||
|
const jwtToken = useSelector(selectUserJwtToken)
|
||||||
|
const save = useAction(fetchGameDetailsUpdate)
|
||||||
|
|
||||||
|
const onSubmit = useCallback(async () => {
|
||||||
|
await save(jwtToken)
|
||||||
|
}, [save, jwtToken])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.formButtons}>
|
||||||
|
<FormButton onClick={onSubmit}>Ok, noté</FormButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withUserRole(ROLES.ADMIN, memo(withUserConnected(GameDetailsUpdate)))
|
||||||
|
|
||||||
|
export const fetchFor = [fetchGameDetailsUpdate]
|
117
src/components/Admin/MemberEdit.tsx
Normal file
117
src/components/Admin/MemberEdit.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { isFinite } from "lodash"
|
||||||
|
import { FC, memo, useState } from "react"
|
||||||
|
import withUserConnected from "../../utils/withUserConnected"
|
||||||
|
import withUserRole from "../../utils/withUserRole"
|
||||||
|
import ROLES from "../../utils/roles.constants"
|
||||||
|
import { Volunteer } from "../../services/volunteers"
|
||||||
|
import styles from "./styles.module.scss"
|
||||||
|
import { toastError } from "../../store/utils"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
volunteer: Volunteer
|
||||||
|
saveVolunteer: (newVolunteer: Partial<Volunteer>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemberEdit: FC<Props> = ({ volunteer, saveVolunteer }): JSX.Element => {
|
||||||
|
const [localVolunteer, setLocalVolunteer] = useState(volunteer)
|
||||||
|
|
||||||
|
const stringDispatch =
|
||||||
|
(propName: string) =>
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
saveVolunteer({ id: localVolunteer.id, [propName]: e.target.value })
|
||||||
|
setLocalVolunteer({ ...localVolunteer, [propName]: e.target.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringInput(id: string, value: string): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div key={id} className={styles.inputContainer}>
|
||||||
|
<span className={styles.inputDesc}>{id}</span>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
onChange={stringDispatch(id)}
|
||||||
|
className={styles.stringInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberDispatch =
|
||||||
|
(propName: string) =>
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const value: number = +e.target.value
|
||||||
|
if (!isFinite(value)) {
|
||||||
|
toastError("Should be a number")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveVolunteer({ id: localVolunteer.id, [propName]: +value })
|
||||||
|
setLocalVolunteer({ ...localVolunteer, [propName]: +value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberInput(id: string, value: number): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div key={id} className={styles.inputContainer}>
|
||||||
|
<span className={styles.inputDesc}>{id}</span>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
onChange={numberDispatch(id)}
|
||||||
|
className={styles.numberInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const booleanDispatch =
|
||||||
|
(propName: string) =>
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const value: boolean = e.target.value !== "0" && e.target.value !== ""
|
||||||
|
saveVolunteer({ id: localVolunteer.id, [propName]: value })
|
||||||
|
setLocalVolunteer({ ...localVolunteer, [propName]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function booleanInput(id: string, value: boolean): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div key={id} className={styles.inputContainer}>
|
||||||
|
<span className={styles.inputDesc}>{id}</span>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id={id}
|
||||||
|
value={value ? "X" : ""}
|
||||||
|
onChange={booleanDispatch(id)}
|
||||||
|
className={styles.booleanInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const volunteerDefault = new Volunteer()
|
||||||
|
const typeHandler: { [id: string]: (id: string, value: any) => JSX.Element } = {
|
||||||
|
string: stringInput,
|
||||||
|
number: numberInput,
|
||||||
|
boolean: booleanInput,
|
||||||
|
}
|
||||||
|
const keys = Object.keys(volunteerDefault) as (keyof Volunteer)[]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={styles.item} key={volunteer.id}>
|
||||||
|
{keys.map((key) => {
|
||||||
|
const valueType = typeof volunteerDefault[key]
|
||||||
|
const value = localVolunteer[key]
|
||||||
|
return (
|
||||||
|
typeHandler[valueType as string]?.(key, value as any) ||
|
||||||
|
stringInput(key, value as any)
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withUserRole(ROLES.ADMIN, memo(withUserConnected(MemberEdit)))
|
||||||
|
|
||||||
|
export const fetchFor = []
|
39
src/components/Admin/styles.module.scss
Executable file
39
src/components/Admin/styles.module.scss
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
@import "../../theme/variables";
|
||||||
|
@import "../../theme/mixins";
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formButtons {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 5px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stringInput {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
.numberInput {
|
||||||
|
width: 3em;
|
||||||
|
}
|
||||||
|
.booleanInput {
|
||||||
|
width: 1.5em;
|
||||||
|
}
|
||||||
|
.inputDesc {
|
||||||
|
font-size: small;
|
||||||
|
}
|
@ -29,8 +29,6 @@ describe("<List />", () => {
|
|||||||
"5": {
|
"5": {
|
||||||
id: 5,
|
id: 5,
|
||||||
title: "6 qui prend!",
|
title: "6 qui prend!",
|
||||||
author: "Wolfgang Kramer",
|
|
||||||
editor: "(uncredited) , Design Edge , B",
|
|
||||||
playersMin: 2,
|
playersMin: 2,
|
||||||
playersMax: 10,
|
playersMax: 10,
|
||||||
duration: 45,
|
duration: 45,
|
||||||
|
@ -10,7 +10,7 @@ const KnowledgeIntro: React.FC = (): JSX.Element => (
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
OK signifie que tu peux expliquer le jeu, avec au maximum un coup d'oeil aux règles sur
|
OK signifie que tu peux expliquer le jeu, avec au maximum un coup d'oeil aux règles sur
|
||||||
un nombre de cartes à distribuer, ou un nombre de PV déclancheur de fin de partie.
|
un nombre de cartes à distribuer, ou un nombre de PV déclencheur de fin de partie.
|
||||||
<br />
|
<br />
|
||||||
Bof signifie que tu seras plus utile que la lecture des règles.
|
Bof signifie que tu seras plus utile que la lecture des règles.
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { memo, useEffect, useState } from "react"
|
import { memo, useEffect, useState } from "react"
|
||||||
import { useSelector, shallowEqual } from "react-redux"
|
import { useSelector, shallowEqual } from "react-redux"
|
||||||
import { toast } from "react-toastify"
|
import { toast } from "react-toastify"
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
@ -11,6 +11,12 @@ import { fetchVolunteerPartialAdd } from "../../store/volunteerPartialAdd"
|
|||||||
import FormButton from "../Form/FormButton/FormButton"
|
import FormButton from "../Form/FormButton/FormButton"
|
||||||
import { validEmail } from "../../utils/standardization"
|
import { validEmail } from "../../utils/standardization"
|
||||||
import { toastError } from "../../store/utils"
|
import { toastError } from "../../store/utils"
|
||||||
|
import {
|
||||||
|
sendBooleanRadioboxDispatch,
|
||||||
|
sendTextareaDispatch,
|
||||||
|
sendRadioboxDispatch,
|
||||||
|
sendTextDispatch,
|
||||||
|
} from "../input.utils"
|
||||||
import {
|
import {
|
||||||
fetchMiscMeetingDateListIfNeed,
|
fetchMiscMeetingDateListIfNeed,
|
||||||
selectMiscMeetingDateList,
|
selectMiscMeetingDateList,
|
||||||
@ -57,26 +63,6 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
|
|||||||
}, [changingBackground, setChangingBackground])
|
}, [changingBackground, setChangingBackground])
|
||||||
const transitionClass = (i: number) => animations[changingBackground][i - 1]
|
const transitionClass = (i: number) => animations[changingBackground][i - 1]
|
||||||
|
|
||||||
const sendTextDispatch =
|
|
||||||
(dispatchSetter: React.Dispatch<React.SetStateAction<string>>) =>
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
dispatchSetter(e.target.value)
|
|
||||||
|
|
||||||
const sendTextareaDispatch =
|
|
||||||
(dispatchSetter: React.Dispatch<React.SetStateAction<string>>) =>
|
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
||||||
dispatchSetter(e.target.value)
|
|
||||||
|
|
||||||
const sendBooleanRadioboxDispatch =
|
|
||||||
(dispatchSetter: React.Dispatch<React.SetStateAction<boolean>>, isYes: boolean) =>
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
dispatchSetter(isYes ? !!e.target.value : !e.target.value)
|
|
||||||
|
|
||||||
const sendRadioboxDispatch =
|
|
||||||
(dispatchSetter: React.Dispatch<React.SetStateAction<string>>) =>
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
dispatchSetter(e.target.value)
|
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
if (!validEmail(email)) {
|
if (!validEmail(email)) {
|
||||||
toastError("Cet email est invalid ><")
|
toastError("Cet email est invalid ><")
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* @jest-environment jsdom
|
|
||||||
*/
|
|
||||||
import { render } from "@testing-library/react"
|
|
||||||
import { MemoryRouter } from "react-router-dom"
|
|
||||||
import { volunteerExample } from "../../../services/volunteers"
|
|
||||||
|
|
||||||
import VolunteerSet from "../index"
|
|
||||||
|
|
||||||
describe("<SetVolunteer />", () => {
|
|
||||||
it("renders", () => {
|
|
||||||
const dispatch = jest.fn()
|
|
||||||
const tree = render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<VolunteerSet dispatch={dispatch} volunteer={volunteerExample} />
|
|
||||||
</MemoryRouter>
|
|
||||||
).container.firstChild
|
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,51 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<SetVolunteer /> renders 1`] = `
|
|
||||||
<section
|
|
||||||
class="VolunteerList"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Modifier un volunteer
|
|
||||||
</h2>
|
|
||||||
<form>
|
|
||||||
<label
|
|
||||||
for="postFirstname"
|
|
||||||
>
|
|
||||||
Prénom:
|
|
||||||
<input
|
|
||||||
id="postFirstname"
|
|
||||||
name="postFirstname"
|
|
||||||
type="text"
|
|
||||||
value="Aupeix"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
for="postName"
|
|
||||||
>
|
|
||||||
Nom:
|
|
||||||
<input
|
|
||||||
id="postName"
|
|
||||||
name="postName"
|
|
||||||
type="text"
|
|
||||||
value="Amélie"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
for="postAdult"
|
|
||||||
>
|
|
||||||
Majeur:
|
|
||||||
<input
|
|
||||||
id="postAdult"
|
|
||||||
name="postAdult"
|
|
||||||
type="text"
|
|
||||||
value="1"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Save changes
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
`;
|
|
@ -1,88 +0,0 @@
|
|||||||
import React, { useState, memo } from "react"
|
|
||||||
import { toast } from "react-toastify"
|
|
||||||
|
|
||||||
import { AppDispatch } from "../../store"
|
|
||||||
|
|
||||||
import { fetchVolunteerSet } from "../../store/volunteerSet"
|
|
||||||
import { Volunteer } from "../../services/volunteers"
|
|
||||||
import styles from "./styles.module.scss"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
dispatch: AppDispatch
|
|
||||||
volunteer: Volunteer
|
|
||||||
}
|
|
||||||
|
|
||||||
const VolunteerSet = ({ dispatch, volunteer }: Props) => {
|
|
||||||
const [firstname, setFirstname] = useState(volunteer.firstname)
|
|
||||||
const [lastname, setName] = useState(volunteer.lastname)
|
|
||||||
const [adult, setAdult] = useState(volunteer.adult)
|
|
||||||
|
|
||||||
const onFirstnameChanged = (e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setFirstname(e.target.value)
|
|
||||||
const onNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)
|
|
||||||
const onAdultChanged = (e: React.ChangeEvent<HTMLInputElement>) => setAdult(+e.target.value)
|
|
||||||
|
|
||||||
const onSavePostClicked = () => {
|
|
||||||
if (firstname && lastname) {
|
|
||||||
dispatch(
|
|
||||||
fetchVolunteerSet({
|
|
||||||
...volunteer,
|
|
||||||
firstname,
|
|
||||||
lastname,
|
|
||||||
adult,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
toast.warning("Il faut au moins préciser un prenom et un nom", {
|
|
||||||
position: "top-center",
|
|
||||||
autoClose: 6000,
|
|
||||||
hideProgressBar: true,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<section className={styles.VolunteerList}>
|
|
||||||
<h2>Modifier un volunteer</h2>
|
|
||||||
<form>
|
|
||||||
<label htmlFor="postFirstname">
|
|
||||||
Prénom:
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="postFirstname"
|
|
||||||
name="postFirstname"
|
|
||||||
value={firstname}
|
|
||||||
onChange={onFirstnameChanged}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label htmlFor="postName">
|
|
||||||
Nom:
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="postName"
|
|
||||||
name="postName"
|
|
||||||
value={lastname}
|
|
||||||
onChange={onNameChanged}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label htmlFor="postAdult">
|
|
||||||
Majeur:
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="postAdult"
|
|
||||||
name="postAdult"
|
|
||||||
value={adult}
|
|
||||||
onChange={onAdultChanged}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button type="button" onClick={onSavePostClicked}>
|
|
||||||
Save changes
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default memo(VolunteerSet)
|
|
@ -1,17 +0,0 @@
|
|||||||
@import "../../theme/variables";
|
|
||||||
|
|
||||||
.VolunteerList {
|
|
||||||
color: $color-white;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding-left: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $color-white;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,6 @@
|
|||||||
import AnnouncementLink from "./AnnouncementLink"
|
import AnnouncementLink from "./AnnouncementLink"
|
||||||
|
import DbEdit, { fetchFor as fetchForDbEdit } from "./Admin/DbEdit"
|
||||||
|
import GameDetailsUpdate, { fetchFor as fetchForGameDetailsUpdate } from "./Admin/GameDetailsUpdate"
|
||||||
import Board, { fetchFor as fetchForBoard } from "./VolunteerBoard/Board"
|
import Board, { fetchFor as fetchForBoard } from "./VolunteerBoard/Board"
|
||||||
import DayWishesForm, {
|
import DayWishesForm, {
|
||||||
fetchFor as fetchForDayWishesForm,
|
fetchFor as fetchForDayWishesForm,
|
||||||
@ -20,11 +22,14 @@ import TeamWishesForm, {
|
|||||||
} from "./VolunteerBoard/TeamWishesForm/TeamWishesForm"
|
} from "./VolunteerBoard/TeamWishesForm/TeamWishesForm"
|
||||||
import VolunteerList from "./VolunteerList"
|
import VolunteerList from "./VolunteerList"
|
||||||
import VolunteerInfo from "./VolunteerInfo"
|
import VolunteerInfo from "./VolunteerInfo"
|
||||||
import VolunteerSet from "./VolunteerSet"
|
|
||||||
import WishAdd from "./WishAdd"
|
import WishAdd from "./WishAdd"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AnnouncementLink,
|
AnnouncementLink,
|
||||||
|
DbEdit,
|
||||||
|
fetchForDbEdit,
|
||||||
|
GameDetailsUpdate,
|
||||||
|
fetchForGameDetailsUpdate,
|
||||||
Board,
|
Board,
|
||||||
fetchForBoard,
|
fetchForBoard,
|
||||||
BoxList,
|
BoxList,
|
||||||
@ -48,6 +53,5 @@ export {
|
|||||||
fetchForTeamWishesForm,
|
fetchForTeamWishesForm,
|
||||||
VolunteerInfo,
|
VolunteerInfo,
|
||||||
VolunteerList,
|
VolunteerList,
|
||||||
VolunteerSet,
|
|
||||||
WishAdd,
|
WishAdd,
|
||||||
}
|
}
|
||||||
|
22
src/components/input.utils.ts
Executable file
22
src/components/input.utils.ts
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export const sendTextDispatch =
|
||||||
|
(dispatchSetter: React.Dispatch<React.SetStateAction<string>>) =>
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
dispatchSetter(e.target.value)
|
||||||
|
|
||||||
|
export const sendTextareaDispatch =
|
||||||
|
(dispatchSetter: React.Dispatch<React.SetStateAction<string>>) =>
|
||||||
|
(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
dispatchSetter(e.target.value)
|
||||||
|
|
||||||
|
export const sendBooleanRadioboxDispatch =
|
||||||
|
(dispatchSetter: React.Dispatch<React.SetStateAction<boolean>>, isYes: boolean) =>
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
dispatchSetter(isYes ? !!e.target.value : !e.target.value)
|
||||||
|
|
||||||
|
export const sendRadioboxDispatch =
|
||||||
|
(dispatchSetter: React.Dispatch<React.SetStateAction<string>>) =>
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
dispatchSetter(e.target.value)
|
28
src/pages/Admin/DbEdit/DbEdit.tsx
Normal file
28
src/pages/Admin/DbEdit/DbEdit.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { FC, memo } from "react"
|
||||||
|
import { RouteComponentProps } from "react-router-dom"
|
||||||
|
import { useSelector } from "react-redux"
|
||||||
|
|
||||||
|
import { AppThunk } from "../../../store"
|
||||||
|
import { selectUserJwtToken } from "../../../store/auth"
|
||||||
|
import { DbEdit, fetchForDbEdit } from "../../../components"
|
||||||
|
|
||||||
|
export type Props = RouteComponentProps
|
||||||
|
|
||||||
|
const DbEditPage: FC<Props> = (): JSX.Element => {
|
||||||
|
const jwtToken = useSelector(selectUserJwtToken)
|
||||||
|
|
||||||
|
if (jwtToken === undefined) return <p>Loading...</p>
|
||||||
|
if (jwtToken) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DbEdit />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <div>Besoin d'être identifié</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch server-side data here
|
||||||
|
export const loadData = (): AppThunk[] => [...fetchForDbEdit.map((f) => f())]
|
||||||
|
|
||||||
|
export default memo(DbEditPage)
|
16
src/pages/Admin/DbEdit/index.tsx
Executable file
16
src/pages/Admin/DbEdit/index.tsx
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
import loadable from "@loadable/component"
|
||||||
|
|
||||||
|
import { Loading, ErrorBoundary } from "../../../components"
|
||||||
|
import { Props, loadData } from "./DbEdit"
|
||||||
|
|
||||||
|
const DbEditPage = loadable(() => import("./DbEdit"), {
|
||||||
|
fallback: <Loading />,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default (props: Props): JSX.Element => (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<DbEditPage {...props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
|
||||||
|
export { loadData }
|
29
src/pages/Admin/GameDetailsUpdate/GameDetailsUpdate.tsx
Normal file
29
src/pages/Admin/GameDetailsUpdate/GameDetailsUpdate.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { FC, memo } from "react"
|
||||||
|
import { RouteComponentProps } from "react-router-dom"
|
||||||
|
import { useSelector } from "react-redux"
|
||||||
|
|
||||||
|
import { AppThunk } from "../../../store"
|
||||||
|
import { selectUserJwtToken } from "../../../store/auth"
|
||||||
|
import { GameDetailsUpdate } from "../../../components"
|
||||||
|
import { fetchGameDetailsUpdateIfNeed } from "../../../store/gameDetailsUpdate"
|
||||||
|
|
||||||
|
export type Props = RouteComponentProps
|
||||||
|
|
||||||
|
const GameDetailsUpdatePage: FC<Props> = (): JSX.Element => {
|
||||||
|
const jwtToken = useSelector(selectUserJwtToken)
|
||||||
|
|
||||||
|
if (jwtToken === undefined) return <p>Loading...</p>
|
||||||
|
if (jwtToken) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GameDetailsUpdate />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <div>Besoin d'être identifié</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch server-side data here
|
||||||
|
export const loadData = (): AppThunk[] => [...[fetchGameDetailsUpdateIfNeed].map((f) => f())]
|
||||||
|
|
||||||
|
export default memo(GameDetailsUpdatePage)
|
16
src/pages/Admin/GameDetailsUpdate/index.tsx
Executable file
16
src/pages/Admin/GameDetailsUpdate/index.tsx
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
import loadable from "@loadable/component"
|
||||||
|
|
||||||
|
import { Loading, ErrorBoundary } from "../../../components"
|
||||||
|
import { Props, loadData } from "./GameDetailsUpdate"
|
||||||
|
|
||||||
|
const GameDetailsUpdatePage = loadable(() => import("./GameDetailsUpdate"), {
|
||||||
|
fallback: <Loading />,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default (props: Props): JSX.Element => (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<GameDetailsUpdatePage {...props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
|
||||||
|
export { loadData }
|
@ -1,6 +1,10 @@
|
|||||||
import { RouteConfig } from "react-router-config"
|
import { RouteConfig } from "react-router-config"
|
||||||
|
|
||||||
import App from "../app"
|
import App from "../app"
|
||||||
|
import AsyncDbEdit, { loadData as loadDbEdit } from "../pages/Admin/DbEdit"
|
||||||
|
import AsyncGameDetailsUpdate, {
|
||||||
|
loadData as loadGameDetailsUpdate,
|
||||||
|
} from "../pages/Admin/GameDetailsUpdate"
|
||||||
import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
|
import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
|
||||||
import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements"
|
import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements"
|
||||||
import AsyncTeamAssignment, { loadData as loadTeamAssignmentData } from "../pages/TeamAssignment"
|
import AsyncTeamAssignment, { loadData as loadTeamAssignmentData } from "../pages/TeamAssignment"
|
||||||
@ -24,6 +28,16 @@ export default [
|
|||||||
component: AsyncHome,
|
component: AsyncHome,
|
||||||
loadData: loadHomeData,
|
loadData: loadHomeData,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/edit",
|
||||||
|
component: AsyncDbEdit,
|
||||||
|
loadData: loadDbEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/updateGameDetails",
|
||||||
|
component: AsyncGameDetailsUpdate,
|
||||||
|
loadData: loadGameDetailsUpdate,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/connaissances",
|
path: "/connaissances",
|
||||||
component: AsyncKnowledge,
|
component: AsyncKnowledge,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// eslint-disable-next-line max-classes-per-file
|
// eslint-disable-next-line max-classes-per-file
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import _ from "lodash"
|
import _, { assign, pick } from "lodash"
|
||||||
import { promises as fs, constants } from "fs"
|
import { promises as fs, constants } from "fs"
|
||||||
import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadsheet"
|
import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadsheet"
|
||||||
import { SheetNames, saveLocalDb, loadLocalDb } from "./localDb"
|
import { SheetNames, saveLocalDb, loadLocalDb } from "./localDb"
|
||||||
@ -187,6 +187,33 @@ export class Sheet<
|
|||||||
this._type = db.type as Record<keyof Element, string>
|
this._type = db.type as Record<keyof Element, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseRawPartialElement(
|
||||||
|
rawPartialElement: Partial<Record<keyof Element, string>>
|
||||||
|
): Partial<Element> | undefined {
|
||||||
|
if (this._type === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
// else
|
||||||
|
const rawPartialFrenchElement = _.mapValues(
|
||||||
|
this.invertedTranslation,
|
||||||
|
(englishProp: string) => (rawPartialElement as any)[englishProp]
|
||||||
|
) as Element
|
||||||
|
|
||||||
|
const rawFrenchElement = this.stringifyElement(this.frenchSpecimen, this._type)
|
||||||
|
assign(rawFrenchElement, rawPartialFrenchElement)
|
||||||
|
|
||||||
|
const frenchElement = this.parseElement(rawFrenchElement, this._type)
|
||||||
|
|
||||||
|
const element = _.mapValues(
|
||||||
|
this.translation,
|
||||||
|
(frenchProp: string) => (frenchElement as any)[frenchProp]
|
||||||
|
) as Element
|
||||||
|
|
||||||
|
const partialElement = pick(element, Object.keys(rawPartialElement))
|
||||||
|
|
||||||
|
return partialElement
|
||||||
|
}
|
||||||
|
|
||||||
dbSave(): void {
|
dbSave(): void {
|
||||||
this.saveTimestamp = +new Date()
|
this.saveTimestamp = +new Date()
|
||||||
|
|
||||||
|
@ -39,6 +39,12 @@ export default class ExpressAccessors<
|
|||||||
return this.sheet as Sheet<ElementNoId, Element>
|
return this.sheet as Sheet<ElementNoId, Element>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseRawPartialElement(
|
||||||
|
rawPartialElement: Partial<Record<keyof Element, string>>
|
||||||
|
): Partial<Element> | undefined {
|
||||||
|
return this.sheet?.parseRawPartialElement(rawPartialElement)
|
||||||
|
}
|
||||||
|
|
||||||
listGet() {
|
listGet() {
|
||||||
return async (
|
return async (
|
||||||
_request: Request,
|
_request: Request,
|
||||||
@ -56,6 +62,45 @@ export default class ExpressAccessors<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listSet(
|
||||||
|
custom?: (
|
||||||
|
list: Element[],
|
||||||
|
body: RequestBody,
|
||||||
|
id: number,
|
||||||
|
roles: string[]
|
||||||
|
) => Promise<CustomSetReturn<Element[]>> | CustomSetReturn<Element[]>
|
||||||
|
) {
|
||||||
|
return async (request: Request, response: Response, _next: NextFunction): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const sheet = await this.getSheet()
|
||||||
|
if (!custom) {
|
||||||
|
await sheet.setList(request.body)
|
||||||
|
response.status(200)
|
||||||
|
} else {
|
||||||
|
const memberId = response?.locals?.jwt?.id || -1
|
||||||
|
const roles: string[] = response?.locals?.jwt?.roles || []
|
||||||
|
const list = (await sheet.getList()) || []
|
||||||
|
const { toDatabase, toCaller } = await custom(
|
||||||
|
list,
|
||||||
|
request.body,
|
||||||
|
memberId,
|
||||||
|
roles
|
||||||
|
)
|
||||||
|
if (toDatabase !== undefined) {
|
||||||
|
await sheet.setList(toDatabase)
|
||||||
|
}
|
||||||
|
if (toCaller !== undefined) {
|
||||||
|
response.status(200).json(toCaller)
|
||||||
|
} else {
|
||||||
|
response.status(200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
response.status(200).json({ error: e.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// custom can be async
|
// custom can be async
|
||||||
get<Ret = Element>(
|
get<Ret = Element>(
|
||||||
custom?: (
|
custom?: (
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { cloneDeep } from "lodash"
|
||||||
import ExpressAccessors from "./expressAccessors"
|
import ExpressAccessors from "./expressAccessors"
|
||||||
import { Game, GameWithoutId, translationGame } from "../../services/games"
|
import { Game, GameWithoutId, translationGame } from "../../services/games"
|
||||||
|
|
||||||
@ -11,3 +12,21 @@ export const gameListGet = expressAccessor.listGet()
|
|||||||
// export const gameGet = expressAccessor.get()
|
// export const gameGet = expressAccessor.get()
|
||||||
// export const gameAdd = expressAccessor.add()
|
// export const gameAdd = expressAccessor.add()
|
||||||
// export const gameSet = expressAccessor.set()
|
// export const gameSet = expressAccessor.set()
|
||||||
|
|
||||||
|
export const gameDetailsUpdate = expressAccessor.listSet(async (list, _body, _id, roles) => {
|
||||||
|
if (!roles.includes("admin")) {
|
||||||
|
throw Error(
|
||||||
|
`À moins d'être admin, on ne peut pas modifier n'importe quel jeu, ${JSON.stringify(
|
||||||
|
roles
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const newList = cloneDeep(list)
|
||||||
|
|
||||||
|
// TODO update game list details from BGG
|
||||||
|
|
||||||
|
return {
|
||||||
|
toDatabase: newList,
|
||||||
|
toCaller: newList,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { assign, cloneDeep, keys, omit, pick } from "lodash"
|
import { assign, cloneDeep, omit, pick } from "lodash"
|
||||||
import bcrypt from "bcrypt"
|
import bcrypt from "bcrypt"
|
||||||
import sgMail from "@sendgrid/mail"
|
import sgMail from "@sendgrid/mail"
|
||||||
|
|
||||||
@ -30,7 +30,30 @@ export const volunteerListGet = expressAccessor.get(async (list, _body, id) => {
|
|||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
export const volunteerSet = expressAccessor.set()
|
|
||||||
|
export const volunteerSet = expressAccessor.set(async (list, body, _id, roles) => {
|
||||||
|
if (!roles.includes("admin")) {
|
||||||
|
throw Error(`À moins d'être admin, on ne peut pas modifier n'importe quel utilisateur`)
|
||||||
|
}
|
||||||
|
const newPartialVolunteer = body[0] as Partial<Record<keyof Volunteer, string>> & { id: number }
|
||||||
|
const volunteer: Volunteer | undefined = list.find((v) => v.id === newPartialVolunteer.id)
|
||||||
|
if (!volunteer) {
|
||||||
|
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${newPartialVolunteer.id}`)
|
||||||
|
}
|
||||||
|
const newVolunteer: Volunteer = cloneDeep(volunteer)
|
||||||
|
|
||||||
|
const parsedPartialVolunteer = expressAccessor.parseRawPartialElement(newPartialVolunteer)
|
||||||
|
if (parsedPartialVolunteer === undefined) {
|
||||||
|
throw Error(`Erreur au parsing dans volunteerSet`)
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(newVolunteer, parsedPartialVolunteer)
|
||||||
|
|
||||||
|
return {
|
||||||
|
toDatabase: newVolunteer,
|
||||||
|
toCaller: newVolunteer,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const volunteerDiscordId = expressAccessor.get(async (list, body, id) => {
|
export const volunteerDiscordId = expressAccessor.get(async (list, body, id) => {
|
||||||
const requestedId = +body[0] || id
|
const requestedId = +body[0] || id
|
||||||
@ -132,7 +155,7 @@ export const volunteerLogin = expressAccessor.get<VolunteerLogin>(async (list, b
|
|||||||
const lastForgot: { [id: string]: number } = {}
|
const lastForgot: { [id: string]: number } = {}
|
||||||
export const volunteerForgot = expressAccessor.set(async (list, bodyArray) => {
|
export const volunteerForgot = expressAccessor.set(async (list, bodyArray) => {
|
||||||
const [body] = bodyArray
|
const [body] = bodyArray
|
||||||
const volunteer = getByEmail(list, body.email)
|
const volunteer: Volunteer | undefined = 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")
|
||||||
}
|
}
|
||||||
@ -199,15 +222,17 @@ export const volunteerAsksSet = expressAccessor.set(async (list, body, id) => {
|
|||||||
}
|
}
|
||||||
const newVolunteer = cloneDeep(volunteer)
|
const newVolunteer = cloneDeep(volunteer)
|
||||||
|
|
||||||
assign(newVolunteer, pick(notifChanges, keys(newVolunteer)))
|
if (notifChanges.teamWishes !== undefined) newVolunteer.hiddenAsks = notifChanges.hiddenAsks
|
||||||
|
if (notifChanges.acceptsNotifs !== undefined)
|
||||||
|
newVolunteer.acceptsNotifs = notifChanges.acceptsNotifs
|
||||||
|
if (notifChanges.pushNotifSubscription !== undefined)
|
||||||
|
newVolunteer.pushNotifSubscription = notifChanges.pushNotifSubscription
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toDatabase: newVolunteer,
|
toDatabase: newVolunteer,
|
||||||
toCaller: {
|
toCaller: {
|
||||||
id: newVolunteer.id,
|
id: newVolunteer.id,
|
||||||
firstname: newVolunteer.firstname,
|
firstname: newVolunteer.firstname,
|
||||||
adult: newVolunteer.adult,
|
|
||||||
active: newVolunteer.active,
|
|
||||||
hiddenAsks: newVolunteer.hiddenAsks,
|
hiddenAsks: newVolunteer.hiddenAsks,
|
||||||
pushNotifSubscription: newVolunteer.pushNotifSubscription,
|
pushNotifSubscription: newVolunteer.pushNotifSubscription,
|
||||||
acceptsNotifs: newVolunteer.acceptsNotifs,
|
acceptsNotifs: newVolunteer.acceptsNotifs,
|
||||||
@ -223,11 +248,11 @@ export const volunteerTeamWishesSet = expressAccessor.set(async (list, body, id,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
const wishes = body[1] as VolunteerTeamWishes
|
const wishes = body[1] as VolunteerTeamWishes
|
||||||
const volunteer = list.find((v) => v.id === requestedId)
|
const volunteer: Volunteer | undefined = list.find((v) => v.id === requestedId)
|
||||||
if (!volunteer) {
|
if (!volunteer) {
|
||||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
||||||
}
|
}
|
||||||
const newVolunteer = cloneDeep(volunteer)
|
const newVolunteer: Volunteer = cloneDeep(volunteer)
|
||||||
|
|
||||||
if (wishes.teamWishes !== undefined) {
|
if (wishes.teamWishes !== undefined) {
|
||||||
newVolunteer.teamWishes = wishes.teamWishes
|
newVolunteer.teamWishes = wishes.teamWishes
|
||||||
@ -252,7 +277,7 @@ export const volunteerDayWishesSet = expressAccessor.set(async (list, body, id)
|
|||||||
throw Error(`On ne peut acceder qu'à ses propres envies de jours`)
|
throw Error(`On ne peut acceder qu'à ses propres envies de jours`)
|
||||||
}
|
}
|
||||||
const wishes = body[1] as VolunteerDayWishes
|
const wishes = body[1] as VolunteerDayWishes
|
||||||
const volunteer = list.find((v) => v.id === requestedId)
|
const volunteer: Volunteer | undefined = list.find((v) => v.id === requestedId)
|
||||||
if (!volunteer) {
|
if (!volunteer) {
|
||||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
||||||
}
|
}
|
||||||
@ -318,10 +343,8 @@ export const volunteerParticipationDetailsSet = expressAccessor.set(async (list,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const volunteerTeamAssignSet = expressAccessor.set(async (list, body, id) => {
|
export const volunteerTeamAssignSet = expressAccessor.set(async (list, body, _id, roles) => {
|
||||||
const requestedId = +body[0] || id
|
if (!roles.includes("répartiteur")) {
|
||||||
const assigner = list.find((v) => v.id === requestedId)
|
|
||||||
if (!assigner || !assigner.roles.includes("répartiteur")) {
|
|
||||||
throw Error(`Vous n'avez pas les droits pas assigner les équipes.`)
|
throw Error(`Vous n'avez pas les droits pas assigner les équipes.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import certbotRouter from "../routes/certbot"
|
|||||||
import { hasSecret, secure } from "./secure"
|
import { hasSecret, secure } from "./secure"
|
||||||
import { announcementListGet } from "./gsheets/announcements"
|
import { announcementListGet } from "./gsheets/announcements"
|
||||||
import { detailedBoxListGet } from "./gsheets/boxes"
|
import { detailedBoxListGet } from "./gsheets/boxes"
|
||||||
import { gameListGet } from "./gsheets/games"
|
import { gameListGet, gameDetailsUpdate } from "./gsheets/games"
|
||||||
import { postulantAdd } from "./gsheets/postulants"
|
import { postulantAdd } from "./gsheets/postulants"
|
||||||
import { teamListGet } from "./gsheets/teams"
|
import { teamListGet } from "./gsheets/teams"
|
||||||
import {
|
import {
|
||||||
@ -100,7 +100,6 @@ app.get("/VolunteerListGet", secure as RequestHandler, volunteerListGet)
|
|||||||
// Secured APIs
|
// Secured APIs
|
||||||
app.get("/AnnouncementListGet", secure as RequestHandler, announcementListGet)
|
app.get("/AnnouncementListGet", secure as RequestHandler, announcementListGet)
|
||||||
app.get("/MiscDiscordInvitationGet", secure as RequestHandler, miscDiscordInvitation)
|
app.get("/MiscDiscordInvitationGet", secure as RequestHandler, miscDiscordInvitation)
|
||||||
app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
|
|
||||||
app.get("/TeamListGet", teamListGet)
|
app.get("/TeamListGet", teamListGet)
|
||||||
app.get("/VolunteerDiscordId", secure as RequestHandler, volunteerDiscordId)
|
app.get("/VolunteerDiscordId", secure as RequestHandler, volunteerDiscordId)
|
||||||
app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet)
|
app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet)
|
||||||
@ -114,6 +113,10 @@ app.post("/VolunteerDayWishesSet", secure as RequestHandler, volunteerDayWishesS
|
|||||||
app.post("/VolunteerTeamWishesSet", secure as RequestHandler, volunteerTeamWishesSet)
|
app.post("/VolunteerTeamWishesSet", secure as RequestHandler, volunteerTeamWishesSet)
|
||||||
app.post("/VolunteerTeamAssignSet", secure as RequestHandler, volunteerTeamAssignSet)
|
app.post("/VolunteerTeamAssignSet", secure as RequestHandler, volunteerTeamAssignSet)
|
||||||
|
|
||||||
|
// Admin only
|
||||||
|
app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
|
||||||
|
app.get("/GameDetailsUpdate", secure as RequestHandler, gameDetailsUpdate)
|
||||||
|
|
||||||
// Push notification subscription
|
// Push notification subscription
|
||||||
app.post("/notifications/subscribe", notificationsSubscribe)
|
app.post("/notifications/subscribe", notificationsSubscribe)
|
||||||
|
|
||||||
|
@ -3,10 +3,6 @@ export class Game {
|
|||||||
|
|
||||||
title = ""
|
title = ""
|
||||||
|
|
||||||
author = ""
|
|
||||||
|
|
||||||
editor = ""
|
|
||||||
|
|
||||||
playersMin = 0
|
playersMin = 0
|
||||||
|
|
||||||
playersMax = 0
|
playersMax = 0
|
||||||
@ -27,8 +23,6 @@ export class Game {
|
|||||||
export const translationGame: { [k in keyof Game]: string } = {
|
export const translationGame: { [k in keyof Game]: string } = {
|
||||||
id: "id",
|
id: "id",
|
||||||
title: "titre",
|
title: "titre",
|
||||||
author: "auteur",
|
|
||||||
editor: "editeur",
|
|
||||||
playersMin: "minJoueurs",
|
playersMin: "minJoueurs",
|
||||||
playersMax: "maxJoueurs",
|
playersMax: "maxJoueurs",
|
||||||
duration: "duree",
|
duration: "duree",
|
||||||
|
@ -7,3 +7,5 @@ export const gameListGet = serviceAccessors.listGet()
|
|||||||
// export const gameGet = serviceAccessors.get()
|
// export const gameGet = serviceAccessors.get()
|
||||||
// export const gameAdd = serviceAccessors.add()
|
// export const gameAdd = serviceAccessors.add()
|
||||||
// export const gameSet = serviceAccessors.set()
|
// export const gameSet = serviceAccessors.set()
|
||||||
|
|
||||||
|
export const gameDetailsUpdate = serviceAccessors.securedCustomGet<[], Game[]>("DetailsUpdate")
|
||||||
|
@ -163,8 +163,6 @@ export interface VolunteerDiscordId {
|
|||||||
export interface VolunteerAsks {
|
export interface VolunteerAsks {
|
||||||
id: Volunteer["id"]
|
id: Volunteer["id"]
|
||||||
firstname: Volunteer["firstname"]
|
firstname: Volunteer["firstname"]
|
||||||
adult: Volunteer["adult"]
|
|
||||||
active: Volunteer["active"]
|
|
||||||
hiddenAsks: Volunteer["hiddenAsks"]
|
hiddenAsks: Volunteer["hiddenAsks"]
|
||||||
pushNotifSubscription: Volunteer["pushNotifSubscription"]
|
pushNotifSubscription: Volunteer["pushNotifSubscription"]
|
||||||
acceptsNotifs: Volunteer["acceptsNotifs"]
|
acceptsNotifs: Volunteer["acceptsNotifs"]
|
||||||
|
@ -20,7 +20,8 @@ export const volunteerDiscordIdGet = serviceAccessors.securedCustomGet<
|
|||||||
VolunteerDiscordId
|
VolunteerDiscordId
|
||||||
>("DiscordId")
|
>("DiscordId")
|
||||||
export const volunteerPartialAdd = serviceAccessors.customPost<[Partial<Volunteer>]>("PartialAdd")
|
export const volunteerPartialAdd = serviceAccessors.customPost<[Partial<Volunteer>]>("PartialAdd")
|
||||||
export const volunteerSet = serviceAccessors.set()
|
|
||||||
|
export const volunteerSet = serviceAccessors.securedCustomPost<[Partial<Volunteer>]>("Set")
|
||||||
|
|
||||||
export const volunteerLogin =
|
export const volunteerLogin =
|
||||||
serviceAccessors.customPost<[{ email: string; password: string }]>("Login")
|
serviceAccessors.customPost<[{ email: string; password: string }]>("Login")
|
||||||
|
@ -17,8 +17,6 @@ const mockData: Game[] = [
|
|||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
title: "6 qui prend!",
|
title: "6 qui prend!",
|
||||||
author: "Wolfgang Kramer",
|
|
||||||
editor: "(uncredited) , Design Edge , B",
|
|
||||||
playersMin: 2,
|
playersMin: 2,
|
||||||
playersMax: 10,
|
playersMax: 10,
|
||||||
duration: 45,
|
duration: 45,
|
||||||
|
50
src/store/gameDetailsUpdate.ts
Normal file
50
src/store/gameDetailsUpdate.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit"
|
||||||
|
|
||||||
|
import { StateRequest, toastError, toastSuccess, elementListFetch } from "./utils"
|
||||||
|
import { Game } from "../services/games"
|
||||||
|
import { gameDetailsUpdate } from "../services/gamesAccessors"
|
||||||
|
import { AppState, AppThunk } from "."
|
||||||
|
|
||||||
|
const gameAdapter = createEntityAdapter<Game>()
|
||||||
|
|
||||||
|
const gameDetailsUpdateSlice = createSlice({
|
||||||
|
name: "gameDetailsUpdate",
|
||||||
|
initialState: gameAdapter.getInitialState({
|
||||||
|
readyStatus: "idle",
|
||||||
|
} as StateRequest),
|
||||||
|
reducers: {
|
||||||
|
getRequesting: (state) => {
|
||||||
|
state.readyStatus = "request"
|
||||||
|
},
|
||||||
|
getSuccess: (state, { payload }: PayloadAction<Game[]>) => {
|
||||||
|
state.readyStatus = "success"
|
||||||
|
gameAdapter.setAll(state, payload)
|
||||||
|
},
|
||||||
|
getFailure: (state, { payload }: PayloadAction<string>) => {
|
||||||
|
state.readyStatus = "failure"
|
||||||
|
state.error = payload
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default gameDetailsUpdateSlice.reducer
|
||||||
|
export const { getRequesting, getSuccess, getFailure } = gameDetailsUpdateSlice.actions
|
||||||
|
|
||||||
|
export const fetchGameDetailsUpdate = elementListFetch(
|
||||||
|
gameDetailsUpdate,
|
||||||
|
getRequesting,
|
||||||
|
getSuccess,
|
||||||
|
getFailure,
|
||||||
|
(error: Error) => toastError(`Erreur lors de la modification d'un bénévole: ${error.message}`),
|
||||||
|
() => toastSuccess("Bénévole modifié !")
|
||||||
|
)
|
||||||
|
|
||||||
|
const shouldFetchGameDetailsUpdate = (state: AppState) =>
|
||||||
|
state.volunteerList.readyStatus !== "success"
|
||||||
|
|
||||||
|
export const fetchGameDetailsUpdateIfNeed = (): AppThunk => (dispatch, getState) => {
|
||||||
|
const { jwt } = getState().auth
|
||||||
|
if (shouldFetchGameDetailsUpdate(getState())) return dispatch(fetchGameDetailsUpdate(jwt))
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
@ -5,6 +5,7 @@ import announcementList from "./announcementList"
|
|||||||
import auth from "./auth"
|
import auth from "./auth"
|
||||||
import boxList from "./boxList"
|
import boxList from "./boxList"
|
||||||
import gameList from "./gameList"
|
import gameList from "./gameList"
|
||||||
|
import gameDetailsUpdate from "./gameDetailsUpdate"
|
||||||
import miscDiscordInvitation from "./miscDiscordInvitation"
|
import miscDiscordInvitation from "./miscDiscordInvitation"
|
||||||
import miscMeetingDateList from "./miscMeetingDateList"
|
import miscMeetingDateList from "./miscMeetingDateList"
|
||||||
import postulantAdd from "./postulantAdd"
|
import postulantAdd from "./postulantAdd"
|
||||||
@ -32,6 +33,7 @@ export default (history: History) => ({
|
|||||||
auth,
|
auth,
|
||||||
boxList,
|
boxList,
|
||||||
gameList,
|
gameList,
|
||||||
|
gameDetailsUpdate,
|
||||||
miscDiscordInvitation,
|
miscDiscordInvitation,
|
||||||
miscMeetingDateList,
|
miscMeetingDateList,
|
||||||
postulantAdd,
|
postulantAdd,
|
||||||
|
@ -118,33 +118,6 @@ export function elementListFetch<Element, ServiceInput extends Array<any>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function elementSet<Element>(
|
|
||||||
elementSetService: (element: Element) => Promise<{
|
|
||||||
data?: Element | undefined
|
|
||||||
error?: Error | undefined
|
|
||||||
}>,
|
|
||||||
getRequesting: ActionCreatorWithoutPayload<string>,
|
|
||||||
getSuccess: ActionCreatorWithPayload<Element, string>,
|
|
||||||
getFailure: ActionCreatorWithPayload<string, string>,
|
|
||||||
errorMessage?: (error: Error) => void,
|
|
||||||
successMessage?: () => void
|
|
||||||
): (element: Element) => AppThunk {
|
|
||||||
return (element: Element): AppThunk =>
|
|
||||||
async (dispatch) => {
|
|
||||||
dispatch(getRequesting())
|
|
||||||
|
|
||||||
const { error, data } = await elementSetService(element)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
dispatch(getFailure(error.message))
|
|
||||||
errorMessage?.(error)
|
|
||||||
} else {
|
|
||||||
dispatch(getSuccess(data as Element))
|
|
||||||
successMessage?.()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function elementValueFetch<Element>(
|
export function elementValueFetch<Element>(
|
||||||
elementListService: () => Promise<{
|
elementListService: () => Promise<{
|
||||||
data?: Element | undefined
|
data?: Element | undefined
|
||||||
|
@ -1,35 +1,38 @@
|
|||||||
import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit"
|
import { PayloadAction, createSlice, createSelector } from "@reduxjs/toolkit"
|
||||||
|
|
||||||
import { StateRequest, toastError, toastSuccess, elementSet } from "./utils"
|
import { StateRequest, toastError, toastSuccess, elementFetch } from "./utils"
|
||||||
import { Volunteer } from "../services/volunteers"
|
import { Volunteer } from "../services/volunteers"
|
||||||
import { volunteerSet } from "../services/volunteersAccessors"
|
import { volunteerSet } from "../services/volunteersAccessors"
|
||||||
|
import { AppState, AppThunk } from "."
|
||||||
|
|
||||||
const volunteerAdapter = createEntityAdapter<Volunteer>()
|
type StateVolunteer = { entity?: Volunteer } & StateRequest
|
||||||
|
|
||||||
|
export const initialState: StateVolunteer = {
|
||||||
|
readyStatus: "idle",
|
||||||
|
}
|
||||||
|
|
||||||
const volunteerSetSlice = createSlice({
|
const volunteerSetSlice = createSlice({
|
||||||
name: "volunteerSet",
|
name: "volunteerSet",
|
||||||
initialState: volunteerAdapter.getInitialState({
|
initialState,
|
||||||
readyStatus: "idle",
|
|
||||||
} as StateRequest),
|
|
||||||
reducers: {
|
reducers: {
|
||||||
getRequesting: (state) => {
|
getRequesting: (_) => ({
|
||||||
state.readyStatus = "request"
|
readyStatus: "request",
|
||||||
},
|
}),
|
||||||
getSuccess: (state, { payload }: PayloadAction<Volunteer>) => {
|
getSuccess: (_, { payload }: PayloadAction<Volunteer>) => ({
|
||||||
state.readyStatus = "success"
|
readyStatus: "success",
|
||||||
volunteerAdapter.setOne(state, payload)
|
entity: payload,
|
||||||
},
|
}),
|
||||||
getFailure: (state, { payload }: PayloadAction<string>) => {
|
getFailure: (_, { payload }: PayloadAction<string>) => ({
|
||||||
state.readyStatus = "failure"
|
readyStatus: "failure",
|
||||||
state.error = payload
|
error: payload,
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default volunteerSetSlice.reducer
|
export default volunteerSetSlice.reducer
|
||||||
export const { getRequesting, getSuccess, getFailure } = volunteerSetSlice.actions
|
export const { getRequesting, getSuccess, getFailure } = volunteerSetSlice.actions
|
||||||
|
|
||||||
export const fetchVolunteerSet = elementSet(
|
export const fetchVolunteerSet = elementFetch(
|
||||||
volunteerSet,
|
volunteerSet,
|
||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
@ -37,3 +40,20 @@ export const fetchVolunteerSet = elementSet(
|
|||||||
(error: Error) => toastError(`Erreur lors de la modification d'un bénévole: ${error.message}`),
|
(error: Error) => toastError(`Erreur lors de la modification d'un bénévole: ${error.message}`),
|
||||||
() => toastSuccess("Bénévole modifié !")
|
() => toastSuccess("Bénévole modifié !")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const shouldFetchVolunteerSet = (_state: AppState) => true
|
||||||
|
|
||||||
|
export const fetchVolunteerSetIfNeed =
|
||||||
|
(newPartialVolunteer: Partial<Volunteer>): AppThunk =>
|
||||||
|
(dispatch, getState) => {
|
||||||
|
const { jwt } = getState().auth
|
||||||
|
if (shouldFetchVolunteerSet(getState()))
|
||||||
|
return dispatch(fetchVolunteerSet(jwt, newPartialVolunteer))
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectVolunteerSet = createSelector(
|
||||||
|
(state: AppState) => state,
|
||||||
|
(state): string | undefined => state.volunteerSet?.entity?.discordId
|
||||||
|
)
|
||||||
|
@ -3,6 +3,7 @@ type rolesType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ROLES: rolesType = {
|
const ROLES: rolesType = {
|
||||||
|
ADMIN: "admin",
|
||||||
ASSIGNER: "répartiteur",
|
ASSIGNER: "répartiteur",
|
||||||
TEAMLEAD: "référent",
|
TEAMLEAD: "référent",
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,11 @@ import { selectUserRoles } from "../store/auth"
|
|||||||
function withUserRole<T>(requiredRole: string, Component: React.ComponentType<T>) {
|
function withUserRole<T>(requiredRole: string, Component: React.ComponentType<T>) {
|
||||||
return (props: T): JSX.Element => {
|
return (props: T): JSX.Element => {
|
||||||
const roles = useSelector(selectUserRoles)
|
const roles = useSelector(selectUserRoles)
|
||||||
return roles.includes(requiredRole) ? <Component {...props} /> : <div />
|
return roles.includes(requiredRole) ? (
|
||||||
|
<Component {...props} />
|
||||||
|
) : (
|
||||||
|
<div>Missing role {requiredRole}</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user