Add db edit feature for admin

This commit is contained in:
pikiou
2022-05-24 15:30:15 +02:00
parent df33d3a951
commit da643df6a6
34 changed files with 593 additions and 276 deletions

View 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]

View 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]

View 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 = []

View 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;
}

View File

@@ -29,8 +29,6 @@ describe("<List />", () => {
"5": {
id: 5,
title: "6 qui prend!",
author: "Wolfgang Kramer",
editor: "(uncredited) , Design Edge , B",
playersMin: 2,
playersMax: 10,
duration: 45,

View File

@@ -10,7 +10,7 @@ const KnowledgeIntro: React.FC = (): JSX.Element => (
</p>
<p>
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 />
Bof signifie que tu seras plus utile que la lecture des règles.
</p>

View File

@@ -1,4 +1,4 @@
import React, { memo, useEffect, useState } from "react"
import { memo, useEffect, useState } from "react"
import { useSelector, shallowEqual } from "react-redux"
import { toast } from "react-toastify"
import _ from "lodash"
@@ -11,6 +11,12 @@ import { fetchVolunteerPartialAdd } from "../../store/volunteerPartialAdd"
import FormButton from "../Form/FormButton/FormButton"
import { validEmail } from "../../utils/standardization"
import { toastError } from "../../store/utils"
import {
sendBooleanRadioboxDispatch,
sendTextareaDispatch,
sendRadioboxDispatch,
sendTextDispatch,
} from "../input.utils"
import {
fetchMiscMeetingDateListIfNeed,
selectMiscMeetingDateList,
@@ -57,26 +63,6 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
}, [changingBackground, setChangingBackground])
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 = () => {
if (!validEmail(email)) {
toastError("Cet email est invalid ><")

View File

@@ -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()
})
})

View File

@@ -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>
`;

View File

@@ -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)

View File

@@ -1,17 +0,0 @@
@import "../../theme/variables";
.VolunteerList {
color: $color-white;
ul {
padding-left: 17px;
}
li {
margin-bottom: 0.5em;
}
a {
color: $color-white;
}
}

View File

@@ -1,4 +1,6 @@
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 DayWishesForm, {
fetchFor as fetchForDayWishesForm,
@@ -20,11 +22,14 @@ import TeamWishesForm, {
} from "./VolunteerBoard/TeamWishesForm/TeamWishesForm"
import VolunteerList from "./VolunteerList"
import VolunteerInfo from "./VolunteerInfo"
import VolunteerSet from "./VolunteerSet"
import WishAdd from "./WishAdd"
export {
AnnouncementLink,
DbEdit,
fetchForDbEdit,
GameDetailsUpdate,
fetchForGameDetailsUpdate,
Board,
fetchForBoard,
BoxList,
@@ -48,6 +53,5 @@ export {
fetchForTeamWishesForm,
VolunteerInfo,
VolunteerList,
VolunteerSet,
WishAdd,
}

22
src/components/input.utils.ts Executable file
View 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)