mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-09 17:14:21 +02:00
Add Mes connaissances
This commit is contained in:
parent
0d69bc93db
commit
1b93b41225
BIN
src/app/img/poufpaf.png
Executable file
BIN
src/app/img/poufpaf.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
97
src/components/Knowledge/BoxItem.tsx
Normal file
97
src/components/Knowledge/BoxItem.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { cloneDeep, pull, sortBy } from "lodash"
|
||||||
|
import React, { memo, useCallback, useEffect, useState } from "react"
|
||||||
|
import classnames from "classnames"
|
||||||
|
import styles from "./styles.module.scss"
|
||||||
|
import { DetailedBox } from "../../services/boxes"
|
||||||
|
import { VolunteerKnowledge, VolunteerKnowledgeWithoutId } from "../../services/volunteers"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
detailedBox: DetailedBox
|
||||||
|
volunteerKnowledge: VolunteerKnowledge | undefined
|
||||||
|
saveVolunteerKnowledge: (newVolunteerKnowledge: VolunteerKnowledge) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const BoxItem: React.FC<Props> = ({
|
||||||
|
detailedBox,
|
||||||
|
volunteerKnowledge,
|
||||||
|
saveVolunteerKnowledge,
|
||||||
|
}): JSX.Element => {
|
||||||
|
type ChoiceValue = keyof VolunteerKnowledgeWithoutId | "unknown"
|
||||||
|
const [choice, setChoice] = useState("unknown")
|
||||||
|
// const discordInvitation = useSelector(selectMiscDiscordInvitation)
|
||||||
|
const { gameId, bggPhoto, title, bggId, poufpaf } = detailedBox
|
||||||
|
const knowledgeChoices: { name: string; value: ChoiceValue }[] = [
|
||||||
|
{ name: "?", value: "unknown" },
|
||||||
|
{ name: "OK", value: "ok" },
|
||||||
|
{ name: "Bof", value: "bof" },
|
||||||
|
{ name: "Niet", value: "niet" },
|
||||||
|
]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!volunteerKnowledge) return
|
||||||
|
let providedchoice = "unknown"
|
||||||
|
if (volunteerKnowledge.ok.includes(gameId)) providedchoice = "ok"
|
||||||
|
if (volunteerKnowledge.bof.includes(gameId)) providedchoice = "bof"
|
||||||
|
if (volunteerKnowledge.niet.includes(gameId)) providedchoice = "niet"
|
||||||
|
setChoice(providedchoice)
|
||||||
|
}, [gameId, setChoice, volunteerKnowledge])
|
||||||
|
|
||||||
|
const onChoiceClick = useCallback(
|
||||||
|
(value: ChoiceValue) => {
|
||||||
|
if (!volunteerKnowledge) return
|
||||||
|
const newVolunteerKnowledge: VolunteerKnowledge = cloneDeep(volunteerKnowledge)
|
||||||
|
pull(newVolunteerKnowledge.ok, gameId)
|
||||||
|
pull(newVolunteerKnowledge.bof, gameId)
|
||||||
|
pull(newVolunteerKnowledge.niet, gameId)
|
||||||
|
if (value !== "unknown") {
|
||||||
|
newVolunteerKnowledge[value] = sortBy([...newVolunteerKnowledge[value], gameId])
|
||||||
|
}
|
||||||
|
saveVolunteerKnowledge(newVolunteerKnowledge)
|
||||||
|
},
|
||||||
|
[volunteerKnowledge, gameId, saveVolunteerKnowledge]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={styles.boxItem}>
|
||||||
|
<div className={styles.photoContainer}>
|
||||||
|
<img className={styles.photo} src={bggPhoto} alt="" />
|
||||||
|
</div>
|
||||||
|
<div className={classnames(styles.titleContainer, poufpaf && styles.shorterTitle)}>
|
||||||
|
<a
|
||||||
|
href={`https://boardgamegeek.com/boardgame/${bggId}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.title}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={`https://sites.google.com/site/poufpafpasteque/fiches_alpha/${poufpaf}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<div className={poufpaf ? styles.poufpaf : styles.noPoufpaf}> </div>
|
||||||
|
</a>
|
||||||
|
<ul className={styles.knowledgeList}>
|
||||||
|
{knowledgeChoices.map(({ name, value }) => (
|
||||||
|
<li key={value} className={styles.knowledgeItem}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChoiceClick(value)}
|
||||||
|
className={classnames(
|
||||||
|
styles.knowledgeButton,
|
||||||
|
styles[value],
|
||||||
|
choice === value && styles.active
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(BoxItem)
|
33
src/components/Knowledge/BoxList.tsx
Normal file
33
src/components/Knowledge/BoxList.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React, { memo } from "react"
|
||||||
|
import { useSelector } from "react-redux"
|
||||||
|
import styles from "./styles.module.scss"
|
||||||
|
import BoxItem from "./BoxItem"
|
||||||
|
import { fetchBoxListIfNeed, selectSortedUniqueDetailedBoxes } from "../../store/boxList"
|
||||||
|
import {
|
||||||
|
fetchVolunteerKnowledgeSetIfNeed,
|
||||||
|
useVolunteerKnowledge,
|
||||||
|
} from "../../store/volunteerKnowledgeSet"
|
||||||
|
|
||||||
|
const BoxList: React.FC = (): JSX.Element | null => {
|
||||||
|
const detailedBoxes = useSelector(selectSortedUniqueDetailedBoxes)
|
||||||
|
const [volunteerKnowledge, saveVolunteerKnowledge] = useVolunteerKnowledge()
|
||||||
|
|
||||||
|
if (!detailedBoxes || detailedBoxes.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={styles.boxList}>
|
||||||
|
{detailedBoxes.map((detailedBox: any) => (
|
||||||
|
<BoxItem
|
||||||
|
detailedBox={detailedBox}
|
||||||
|
volunteerKnowledge={volunteerKnowledge}
|
||||||
|
saveVolunteerKnowledge={saveVolunteerKnowledge}
|
||||||
|
key={detailedBox.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(BoxList)
|
||||||
|
|
||||||
|
export const fetchFor = [fetchBoxListIfNeed, fetchVolunteerKnowledgeSetIfNeed]
|
20
src/components/Knowledge/KnowledgeIntro.tsx
Normal file
20
src/components/Knowledge/KnowledgeIntro.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
const KnowledgeIntro: React.FC = (): JSX.Element => (
|
||||||
|
<div>
|
||||||
|
<h1>Tes connaissances en jeux</h1>
|
||||||
|
<p>
|
||||||
|
Lors du festival, si tu es aux Jeux à Volonté, il sera très utile de savoir qui peut
|
||||||
|
expliquer quoi. Ce sera même indiqué dans chaque boîte de jeu ! Mais pour ça il faut que
|
||||||
|
tu nous dises à quel point tu peux les expliquer.
|
||||||
|
</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.
|
||||||
|
<br />
|
||||||
|
Bof signifie que tu seras plus utile que la lecture des règles.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default KnowledgeIntro
|
117
src/components/Knowledge/styles.module.scss
Executable file
117
src/components/Knowledge/styles.module.scss
Executable file
@ -0,0 +1,117 @@
|
|||||||
|
@import "../../theme/variables";
|
||||||
|
@import "../../theme/mixins";
|
||||||
|
|
||||||
|
.boxList {
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxItem {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photoContainer {
|
||||||
|
height: 50px;
|
||||||
|
width: 73px;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.photo {
|
||||||
|
height: 50px;
|
||||||
|
max-width: 73px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleContainer {
|
||||||
|
height: 50px;
|
||||||
|
width: 198px;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.shorterTitle {
|
||||||
|
width: 168px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
height: 50px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poufpaf {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
background: url("../../app/img/poufpaf.png");
|
||||||
|
background-size: contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noPoufpaf {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledgeList {
|
||||||
|
@include clear-ul-style;
|
||||||
|
|
||||||
|
width: 266px;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledgeItem {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledgeButton {
|
||||||
|
margin: 0;
|
||||||
|
padding: 7px 2px 6px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
color: $color-grey-medium;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $color-yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unknown {
|
||||||
|
background-color: $color-unknown;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $color-active-unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ok {
|
||||||
|
background-color: $color-ok;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $color-active-ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bof {
|
||||||
|
background-color: $color-bof;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $color-active-bof;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.niet {
|
||||||
|
background-color: $color-niet;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $color-active-niet;
|
||||||
|
}
|
||||||
|
}
|
@ -52,6 +52,7 @@ const MainMenu: FC = (): JSX.Element => {
|
|||||||
<MenuItem name="Questions" pathname="/" />
|
<MenuItem name="Questions" pathname="/" />
|
||||||
<MenuItem name="Annonces" pathname="/annonces" />
|
<MenuItem name="Annonces" pathname="/annonces" />
|
||||||
<MenuItem name="Mon profil" pathname="/profil" />
|
<MenuItem name="Mon profil" pathname="/profil" />
|
||||||
|
<MenuItem name="Mes connaissances" pathname="/connaissances" />
|
||||||
<RestrictMenuItem
|
<RestrictMenuItem
|
||||||
role={ROLES.ASSIGNER}
|
role={ROLES.ASSIGNER}
|
||||||
name="Gestion équipes"
|
name="Gestion équipes"
|
||||||
|
@ -462,6 +462,7 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
|
|||||||
Ces rencontres ont lieu à 19h dans un bar/resto calme à Châtelet, le{" "}
|
Ces rencontres ont lieu à 19h dans un bar/resto calme à Châtelet, le{" "}
|
||||||
<a
|
<a
|
||||||
href="https://goo.gl/maps/N5NYWDF66vNQDFMh8"
|
href="https://goo.gl/maps/N5NYWDF66vNQDFMh8"
|
||||||
|
id="sfmMap"
|
||||||
key="sfmMap"
|
key="sfmMap"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@ -471,6 +472,7 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
|
|||||||
, ou à une soirée festive à 2 pas du lieu du festival, aux{" "}
|
, ou à une soirée festive à 2 pas du lieu du festival, aux{" "}
|
||||||
<a
|
<a
|
||||||
href="https://www.captainturtle.fr/aperos-petanque-paris/"
|
href="https://www.captainturtle.fr/aperos-petanque-paris/"
|
||||||
|
id="petanque"
|
||||||
key="petanque"
|
key="petanque"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
|
@ -7,6 +7,8 @@ import ErrorBoundary from "./ErrorBoundary"
|
|||||||
import GameList from "./GameList"
|
import GameList from "./GameList"
|
||||||
import Loading from "./Loading"
|
import Loading from "./Loading"
|
||||||
import LoginForm from "./LoginForm"
|
import LoginForm from "./LoginForm"
|
||||||
|
import BoxList, { fetchFor as fetchForKnowledge } from "./Knowledge/BoxList"
|
||||||
|
import KnowledgeIntro from "./Knowledge/KnowledgeIntro"
|
||||||
import Asks, { fetchFor as fetchForAsks } from "./Asks"
|
import Asks, { fetchFor as fetchForAsks } from "./Asks"
|
||||||
import ParticipationDetailsForm, {
|
import ParticipationDetailsForm, {
|
||||||
fetchFor as fetchForParticipationDetailsForm,
|
fetchFor as fetchForParticipationDetailsForm,
|
||||||
@ -25,10 +27,13 @@ export {
|
|||||||
AnnouncementLink,
|
AnnouncementLink,
|
||||||
Board,
|
Board,
|
||||||
fetchForBoard,
|
fetchForBoard,
|
||||||
|
BoxList,
|
||||||
|
fetchForKnowledge,
|
||||||
DayWishesForm,
|
DayWishesForm,
|
||||||
fetchForDayWishesForm,
|
fetchForDayWishesForm,
|
||||||
ErrorBoundary,
|
ErrorBoundary,
|
||||||
GameList,
|
GameList,
|
||||||
|
KnowledgeIntro,
|
||||||
Loading,
|
Loading,
|
||||||
LoginForm,
|
LoginForm,
|
||||||
Asks,
|
Asks,
|
||||||
|
24
src/pages/Knowledge/KnowledgesPage.tsx
Normal file
24
src/pages/Knowledge/KnowledgesPage.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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"
|
||||||
|
import { BoxList, KnowledgeIntro, fetchForKnowledge } from "../../components"
|
||||||
|
|
||||||
|
export type Props = RouteComponentProps
|
||||||
|
|
||||||
|
const KnowledgesPage: FC<Props> = (): JSX.Element => (
|
||||||
|
<div className={styles.knowledgesPage}>
|
||||||
|
<div className={styles.knowledgesContent}>
|
||||||
|
<Helmet title="KnowledgesPage" />
|
||||||
|
<KnowledgeIntro />
|
||||||
|
<BoxList />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch server-side data here
|
||||||
|
export const loadData = (): AppThunk[] => [...fetchForKnowledge.map((f) => f())]
|
||||||
|
|
||||||
|
export default memo(KnowledgesPage)
|
16
src/pages/Knowledge/index.tsx
Executable file
16
src/pages/Knowledge/index.tsx
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
import loadable from "@loadable/component"
|
||||||
|
|
||||||
|
import { Loading, ErrorBoundary } from "../../components"
|
||||||
|
import { Props, loadData } from "./KnowledgesPage"
|
||||||
|
|
||||||
|
const Knowledges = loadable(() => import("./KnowledgesPage"), {
|
||||||
|
fallback: <Loading />,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default (props: Props): JSX.Element => (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Knowledges {...props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
|
||||||
|
export { loadData }
|
9
src/pages/Knowledge/styles.module.scss
Executable file
9
src/pages/Knowledge/styles.module.scss
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
@import "../../theme/mixins";
|
||||||
|
|
||||||
|
.knowledgesPage {
|
||||||
|
@include page-wrapper-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledgesContent {
|
||||||
|
@include page-content-wrapper(700px);
|
||||||
|
}
|
@ -5,6 +5,7 @@ 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"
|
||||||
import AsyncRegisterPage, { loadData as loadRegisterPage } from "../pages/Register"
|
import AsyncRegisterPage, { loadData as loadRegisterPage } from "../pages/Register"
|
||||||
|
import AsyncKnowledge, { loadData as loadKnowledgeData } from "../pages/Knowledge"
|
||||||
import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
|
import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
|
||||||
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
|
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
|
||||||
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
|
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
|
||||||
@ -23,6 +24,11 @@ export default [
|
|||||||
component: AsyncHome,
|
component: AsyncHome,
|
||||||
loadData: loadHomeData,
|
loadData: loadHomeData,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/connaissances",
|
||||||
|
component: AsyncKnowledge,
|
||||||
|
loadData: loadKnowledgeData,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/preRegister",
|
path: "/preRegister",
|
||||||
component: AsyncRegisterPage,
|
component: AsyncRegisterPage,
|
||||||
|
@ -406,8 +406,7 @@ export class Sheet<
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
const matchArrayType = type.match(
|
const matchArrayType = type.match(
|
||||||
/^(number|string|boolean|date)\[([^\]]+)\]$/
|
/^(number|string|boolean|date)\[([^\]]+)\]$/
|
||||||
)
|
)
|
||||||
@ -438,11 +437,9 @@ export class Sheet<
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
case "date":
|
case "date": {
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
const rawDates = rawProp.split(delimiter)
|
const rawDates = rawProp.split(delimiter)
|
||||||
element[prop] = []
|
element[prop] = []
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
rawDates.forEach((rawDate) => {
|
rawDates.forEach((rawDate) => {
|
||||||
try {
|
try {
|
||||||
element[prop].push(parseDate(rawDate))
|
element[prop].push(parseDate(rawDate))
|
||||||
@ -453,12 +450,14 @@ export class Sheet<
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown array type ${arrayType} in sheet ${this.name} at prop ${prop}`
|
`Unknown array type ${arrayType} in sheet ${this.name} at prop ${prop}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return element
|
return element
|
||||||
},
|
},
|
||||||
@ -492,19 +491,20 @@ export class Sheet<
|
|||||||
stringifiedElement[prop as keyof Element] = stringifiedDate(value)
|
stringifiedElement[prop as keyof Element] = stringifiedDate(value)
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
const matchArrayType = type.match(
|
const matchArrayType = type.match(
|
||||||
/^(number|string|boolean|date)\[([^\]]+)\]$/
|
/^(number|string|boolean|date)\[([^\]]+)\]$/
|
||||||
)
|
)
|
||||||
if (!matchArrayType || !_.isArray(value)) {
|
if (!matchArrayType || !_.isArray(value)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Unknown matchArrayType or not an array in stringifyElement"
|
`Unknown matchArrayType ${JSON.stringify(
|
||||||
|
matchArrayType
|
||||||
|
)} or not an array in stringifyElement for prop ${prop} and value ${JSON.stringify(
|
||||||
|
value
|
||||||
|
)}, ${JSON.stringify(element)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
const arrayType = matchArrayType[1]
|
const arrayType = matchArrayType[1]
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
const delimiter = matchArrayType[2]
|
const delimiter = matchArrayType[2]
|
||||||
|
|
||||||
switch (arrayType) {
|
switch (arrayType) {
|
||||||
@ -554,6 +554,7 @@ export class Sheet<
|
|||||||
default:
|
default:
|
||||||
throw new Error(`Unknown array type ${arrayType}`)
|
throw new Error(`Unknown array type ${arrayType}`)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return stringifiedElement
|
return stringifiedElement
|
||||||
@ -586,7 +587,6 @@ function stringifiedDate(value: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseDate(value: string): Date {
|
function parseDate(value: string): Date {
|
||||||
// eslint-disable-next-line no-case-declarations
|
|
||||||
const matchDate = value.match(/^([0-9]+)\/([0-9]+)\/([0-9]+)$/)
|
const matchDate = value.match(/^([0-9]+)\/([0-9]+)\/([0-9]+)$/)
|
||||||
if (!matchDate) {
|
if (!matchDate) {
|
||||||
throw new Error(`Unable to read date from val ${value}`)
|
throw new Error(`Unable to read date from val ${value}`)
|
||||||
|
32
src/server/gsheets/boxes.ts
Normal file
32
src/server/gsheets/boxes.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import ExpressAccessors from "./expressAccessors"
|
||||||
|
import { Box, BoxWithoutId, translationBox, DetailedBox } from "../../services/boxes"
|
||||||
|
import { Game, GameWithoutId, translationGame } from "../../services/games"
|
||||||
|
import { getSheet } from "./accessors"
|
||||||
|
|
||||||
|
const expressAccessor = new ExpressAccessors<BoxWithoutId, Box>("Boxes", new Box(), translationBox)
|
||||||
|
|
||||||
|
export const detailedBoxListGet = expressAccessor.get(async (list) => {
|
||||||
|
const gameSheet = await getSheet<GameWithoutId, Game>("Games", new Game(), translationGame)
|
||||||
|
|
||||||
|
const gameList = await gameSheet.getList()
|
||||||
|
if (!gameList) {
|
||||||
|
throw Error("Unable to load gameList")
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
.filter((box) => !box.unplayable)
|
||||||
|
.map((box) => {
|
||||||
|
const game = gameList.find((g) => g.id === box.gameId)
|
||||||
|
if (!game) {
|
||||||
|
throw Error(`Unable to find game #${box.gameId}`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: box.id,
|
||||||
|
gameId: box.gameId,
|
||||||
|
title: game.title,
|
||||||
|
bggPhoto: game.bggPhoto,
|
||||||
|
poufpaf: game.poufpaf,
|
||||||
|
bggId: game.bggId,
|
||||||
|
} as DetailedBox
|
||||||
|
})
|
||||||
|
})
|
@ -8,6 +8,6 @@ const expressAccessor = new ExpressAccessors<GameWithoutId, Game>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const gameListGet = expressAccessor.listGet()
|
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()
|
||||||
|
@ -12,6 +12,8 @@ const ANONYMIZED_DB_PATH = path.resolve(process.cwd(), "access/dbAnonymized.json
|
|||||||
export class SheetNames {
|
export class SheetNames {
|
||||||
Announcements = "Annonces"
|
Announcements = "Annonces"
|
||||||
|
|
||||||
|
Boxes = "Boîtes"
|
||||||
|
|
||||||
Games = "Jeux"
|
Games = "Jeux"
|
||||||
|
|
||||||
Miscs = "Divers"
|
Miscs = "Divers"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import _ from "lodash"
|
import { assign, cloneDeep, keys, omit, pick } from "lodash"
|
||||||
import bcrypt from "bcrypt"
|
import bcrypt from "bcrypt"
|
||||||
import sgMail from "@sendgrid/mail"
|
import sgMail from "@sendgrid/mail"
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
VolunteerDayWishes,
|
VolunteerDayWishes,
|
||||||
VolunteerParticipationDetails,
|
VolunteerParticipationDetails,
|
||||||
VolunteerTeamAssign,
|
VolunteerTeamAssign,
|
||||||
|
VolunteerKnowledge,
|
||||||
} from "../../services/volunteers"
|
} from "../../services/volunteers"
|
||||||
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
|
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
|
||||||
import { getJwt } from "../secure"
|
import { getJwt } from "../secure"
|
||||||
@ -40,7 +41,7 @@ export const volunteerDiscordId = expressAccessor.get(async (list, body, id) =>
|
|||||||
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}`)
|
||||||
}
|
}
|
||||||
return _.pick(volunteer, "id", "discordId")
|
return pick(volunteer, "id", "discordId")
|
||||||
})
|
})
|
||||||
|
|
||||||
export const volunteerPartialAdd = expressAccessor.add(async (list, body) => {
|
export const volunteerPartialAdd = expressAccessor.add(async (list, body) => {
|
||||||
@ -58,9 +59,9 @@ export const volunteerPartialAdd = expressAccessor.add(async (list, body) => {
|
|||||||
const password = generatePassword()
|
const password = generatePassword()
|
||||||
const passwordHash = await bcrypt.hash(password, 10)
|
const passwordHash = await bcrypt.hash(password, 10)
|
||||||
|
|
||||||
const newVolunteer = _.omit(new Volunteer(), "id")
|
const newVolunteer = omit(new Volunteer(), "id")
|
||||||
|
|
||||||
_.assign(newVolunteer, {
|
assign(newVolunteer, {
|
||||||
lastname: trim(params.lastname),
|
lastname: trim(params.lastname),
|
||||||
firstname: trim(params.firstname),
|
firstname: trim(params.firstname),
|
||||||
email: trim(params.email),
|
email: trim(params.email),
|
||||||
@ -135,7 +136,7 @@ export const volunteerForgot = expressAccessor.set(async (list, bodyArray) => {
|
|||||||
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")
|
||||||
}
|
}
|
||||||
const newVolunteer = _.cloneDeep(volunteer)
|
const newVolunteer = cloneDeep(volunteer)
|
||||||
|
|
||||||
const now = +new Date()
|
const now = +new Date()
|
||||||
const timeSinceLastSent = now - lastForgot[volunteer.id]
|
const timeSinceLastSent = now - lastForgot[volunteer.id]
|
||||||
@ -196,9 +197,9 @@ export const volunteerAsksSet = expressAccessor.set(async (list, body, id) => {
|
|||||||
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 = cloneDeep(volunteer)
|
||||||
|
|
||||||
_.assign(newVolunteer, _.pick(notifChanges, _.keys(newVolunteer)))
|
assign(newVolunteer, pick(notifChanges, keys(newVolunteer)))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toDatabase: newVolunteer,
|
toDatabase: newVolunteer,
|
||||||
@ -226,7 +227,7 @@ export const volunteerTeamWishesSet = expressAccessor.set(async (list, body, id,
|
|||||||
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 = cloneDeep(volunteer)
|
||||||
|
|
||||||
if (wishes.teamWishes !== undefined) {
|
if (wishes.teamWishes !== undefined) {
|
||||||
newVolunteer.teamWishes = wishes.teamWishes
|
newVolunteer.teamWishes = wishes.teamWishes
|
||||||
@ -255,7 +256,7 @@ export const volunteerDayWishesSet = expressAccessor.set(async (list, body, id)
|
|||||||
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 = cloneDeep(volunteer)
|
||||||
|
|
||||||
if (wishes.active !== undefined) {
|
if (wishes.active !== undefined) {
|
||||||
newVolunteer.active = wishes.active
|
newVolunteer.active = wishes.active
|
||||||
@ -290,7 +291,7 @@ export const volunteerParticipationDetailsSet = expressAccessor.set(async (list,
|
|||||||
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 = cloneDeep(volunteer)
|
||||||
|
|
||||||
if (wishes.tshirtSize !== undefined) {
|
if (wishes.tshirtSize !== undefined) {
|
||||||
newVolunteer.tshirtSize = wishes.tshirtSize
|
newVolunteer.tshirtSize = wishes.tshirtSize
|
||||||
@ -329,7 +330,7 @@ export const volunteerTeamAssignSet = expressAccessor.set(async (list, body, id)
|
|||||||
if (!volunteer) {
|
if (!volunteer) {
|
||||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${teamAssign.volunteer}`)
|
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${teamAssign.volunteer}`)
|
||||||
}
|
}
|
||||||
const newVolunteer = _.cloneDeep(volunteer)
|
const newVolunteer = cloneDeep(volunteer)
|
||||||
newVolunteer.team = teamAssign.team
|
newVolunteer.team = teamAssign.team
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -345,3 +346,26 @@ function getByEmail<T extends { email: string }>(list: T[], rawEmail: string): T
|
|||||||
const email = canonicalEmail(rawEmail || "")
|
const email = canonicalEmail(rawEmail || "")
|
||||||
return list.find((v) => canonicalEmail(v.email) === email)
|
return list.find((v) => canonicalEmail(v.email) === email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const volunteerKnowledgeSet = expressAccessor.set(async (list, body, id) => {
|
||||||
|
const requestedId = +body[0] || id
|
||||||
|
const volunteer = list.find((v) => v.id === requestedId)
|
||||||
|
if (!volunteer) {
|
||||||
|
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
||||||
|
}
|
||||||
|
const knowledge = body[1] as VolunteerKnowledge
|
||||||
|
const newVolunteer = cloneDeep(volunteer)
|
||||||
|
if (knowledge?.ok !== undefined) newVolunteer.ok = knowledge.ok
|
||||||
|
if (knowledge?.bof !== undefined) newVolunteer.bof = knowledge.bof
|
||||||
|
if (knowledge?.niet !== undefined) newVolunteer.niet = knowledge.niet
|
||||||
|
|
||||||
|
return {
|
||||||
|
toDatabase: newVolunteer,
|
||||||
|
toCaller: {
|
||||||
|
id: newVolunteer.id,
|
||||||
|
ok: newVolunteer.ok,
|
||||||
|
bof: newVolunteer.bof,
|
||||||
|
niet: newVolunteer.niet,
|
||||||
|
} as VolunteerKnowledge,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ -18,6 +18,7 @@ import ssr from "./ssr"
|
|||||||
import certbotRouter from "../routes/certbot"
|
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 { gameListGet } from "./gsheets/games"
|
import { gameListGet } from "./gsheets/games"
|
||||||
import { postulantAdd } from "./gsheets/postulants"
|
import { postulantAdd } from "./gsheets/postulants"
|
||||||
import { teamListGet } from "./gsheets/teams"
|
import { teamListGet } from "./gsheets/teams"
|
||||||
@ -33,6 +34,7 @@ import {
|
|||||||
volunteerTeamWishesSet,
|
volunteerTeamWishesSet,
|
||||||
volunteerTeamAssignSet,
|
volunteerTeamAssignSet,
|
||||||
volunteerListGet,
|
volunteerListGet,
|
||||||
|
volunteerKnowledgeSet,
|
||||||
} from "./gsheets/volunteers"
|
} from "./gsheets/volunteers"
|
||||||
import { wishListGet, wishAdd } from "./gsheets/wishes"
|
import { wishListGet, wishAdd } from "./gsheets/wishes"
|
||||||
import config from "../config"
|
import config from "../config"
|
||||||
@ -84,6 +86,7 @@ app.get(
|
|||||||
* APIs
|
* APIs
|
||||||
*/
|
*/
|
||||||
// Google Sheets API
|
// Google Sheets API
|
||||||
|
app.get("/BoxDetailedListGet", detailedBoxListGet)
|
||||||
app.get("/GameListGet", gameListGet)
|
app.get("/GameListGet", gameListGet)
|
||||||
app.get("/MiscMeetingDateListGet", miscMeetingDateListGet)
|
app.get("/MiscMeetingDateListGet", miscMeetingDateListGet)
|
||||||
app.get("/WishListGet", wishListGet)
|
app.get("/WishListGet", wishListGet)
|
||||||
@ -101,6 +104,7 @@ 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)
|
||||||
|
app.post("/VolunteerKnowledgeSet", secure as RequestHandler, volunteerKnowledgeSet)
|
||||||
app.post(
|
app.post(
|
||||||
"/VolunteerParticipationDetailsSet",
|
"/VolunteerParticipationDetailsSet",
|
||||||
secure as RequestHandler,
|
secure as RequestHandler,
|
||||||
|
48
src/services/boxes.ts
Normal file
48
src/services/boxes.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
import { Game } from "./games"
|
||||||
|
|
||||||
|
export class Box {
|
||||||
|
id = 0
|
||||||
|
|
||||||
|
gameId = 0
|
||||||
|
|
||||||
|
container = ""
|
||||||
|
|
||||||
|
unplayable = false
|
||||||
|
|
||||||
|
specificEan = 0
|
||||||
|
|
||||||
|
missingParts = ""
|
||||||
|
|
||||||
|
verified = new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const translationBox: { [k in keyof Box]: string } = {
|
||||||
|
id: "id",
|
||||||
|
gameId: "jeuId",
|
||||||
|
container: "caisse",
|
||||||
|
unplayable: "injouable",
|
||||||
|
specificEan: "eanSpécifique",
|
||||||
|
missingParts: "partiesManquantes",
|
||||||
|
verified: "verifié",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const elementName = "Box"
|
||||||
|
|
||||||
|
export type BoxWithoutId = Omit<Box, "id">
|
||||||
|
|
||||||
|
export class DetailedBox {
|
||||||
|
id = 0
|
||||||
|
|
||||||
|
gameId = 0
|
||||||
|
|
||||||
|
title = new Game().title
|
||||||
|
|
||||||
|
bggPhoto = new Game().bggPhoto
|
||||||
|
|
||||||
|
poufpaf = new Game().poufpaf
|
||||||
|
|
||||||
|
bggId = new Game().bggId
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DetailedBoxWithoutId = Omit<DetailedBox, "id">
|
9
src/services/boxesAccessors.ts
Normal file
9
src/services/boxesAccessors.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import ServiceAccessors from "./accessors"
|
||||||
|
import { elementName, Box, BoxWithoutId, DetailedBox } from "./boxes"
|
||||||
|
|
||||||
|
const serviceAccessors = new ServiceAccessors<BoxWithoutId, Box>(elementName)
|
||||||
|
|
||||||
|
export const detailedBoxListGet = serviceAccessors.customGet<[], DetailedBox>("DetailedListGet")
|
||||||
|
// export const boxGet = serviceAccessors.get()
|
||||||
|
// export const boxAdd = serviceAccessors.add()
|
||||||
|
// export const boxSet = serviceAccessors.set()
|
@ -4,6 +4,6 @@ import { elementName, Game, GameWithoutId } from "./games"
|
|||||||
const serviceAccessors = new ServiceAccessors<GameWithoutId, Game>(elementName)
|
const serviceAccessors = new ServiceAccessors<GameWithoutId, Game>(elementName)
|
||||||
|
|
||||||
export const gameListGet = serviceAccessors.listGet()
|
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()
|
||||||
|
@ -53,6 +53,12 @@ export class Volunteer implements VolunteerPartial {
|
|||||||
pushNotifSubscription = ""
|
pushNotifSubscription = ""
|
||||||
|
|
||||||
acceptsNotifs = ""
|
acceptsNotifs = ""
|
||||||
|
|
||||||
|
ok: number[] = []
|
||||||
|
|
||||||
|
bof: number[] = []
|
||||||
|
|
||||||
|
niet: number[] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const translationVolunteer: { [k in keyof Volunteer]: string } = {
|
export const translationVolunteer: { [k in keyof Volunteer]: string } = {
|
||||||
@ -83,6 +89,9 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
|
|||||||
password2: "passe2",
|
password2: "passe2",
|
||||||
pushNotifSubscription: "pushNotifSubscription",
|
pushNotifSubscription: "pushNotifSubscription",
|
||||||
acceptsNotifs: "accepteLesNotifs",
|
acceptsNotifs: "accepteLesNotifs",
|
||||||
|
ok: "OK",
|
||||||
|
bof: "Bof",
|
||||||
|
niet: "Niet",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VolunteerPartial {
|
export class VolunteerPartial {
|
||||||
@ -125,6 +134,9 @@ export const volunteerExample: Volunteer = {
|
|||||||
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||||
pushNotifSubscription: "",
|
pushNotifSubscription: "",
|
||||||
acceptsNotifs: "",
|
acceptsNotifs: "",
|
||||||
|
ok: [5, 7, 24, 26, 31, 38, 50, 52, 54, 58],
|
||||||
|
bof: [9, 12, 16, 27, 34, 35, 36],
|
||||||
|
niet: [13, 18, 19, 23, 47, 53, 59, 67],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const emailRegexp =
|
export const emailRegexp =
|
||||||
@ -184,3 +196,11 @@ export interface VolunteerTeamAssign {
|
|||||||
volunteer: number
|
volunteer: number
|
||||||
team: Volunteer["team"]
|
team: Volunteer["team"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VolunteerKnowledgeWithoutId = Omit<VolunteerKnowledge, "id">
|
||||||
|
export interface VolunteerKnowledge {
|
||||||
|
id: Volunteer["id"]
|
||||||
|
ok: Volunteer["ok"]
|
||||||
|
bof: Volunteer["bof"]
|
||||||
|
niet: Volunteer["niet"]
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
VolunteerTeamAssign,
|
VolunteerTeamAssign,
|
||||||
VolunteerWithoutId,
|
VolunteerWithoutId,
|
||||||
VolunteerDiscordId,
|
VolunteerDiscordId,
|
||||||
|
VolunteerKnowledge,
|
||||||
} from "./volunteers"
|
} from "./volunteers"
|
||||||
|
|
||||||
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
|
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
|
||||||
@ -42,3 +43,6 @@ export const volunteerParticipationDetailsSet =
|
|||||||
|
|
||||||
export const volunteerTeamAssignSet =
|
export const volunteerTeamAssignSet =
|
||||||
serviceAccessors.securedCustomPost<[number, Partial<VolunteerTeamAssign>]>("TeamAssignSet")
|
serviceAccessors.securedCustomPost<[number, Partial<VolunteerTeamAssign>]>("TeamAssignSet")
|
||||||
|
|
||||||
|
export const volunteerKnowledgeSet =
|
||||||
|
serviceAccessors.securedCustomPost<[number, Partial<VolunteerKnowledge>]>("KnowledgeSet")
|
||||||
|
70
src/store/boxList.ts
Normal file
70
src/store/boxList.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { PayloadAction, createSlice, createEntityAdapter, createSelector } from "@reduxjs/toolkit"
|
||||||
|
import { sortedUniqBy, sortBy } from "lodash"
|
||||||
|
|
||||||
|
import { StateRequest, toastError, elementListFetch } from "./utils"
|
||||||
|
import { Box } from "../services/boxes"
|
||||||
|
import { AppThunk, AppState, EntitiesRequest } from "."
|
||||||
|
import { detailedBoxListGet } from "../services/boxesAccessors"
|
||||||
|
|
||||||
|
const boxAdapter = createEntityAdapter<Box>()
|
||||||
|
|
||||||
|
export const initialState = boxAdapter.getInitialState({
|
||||||
|
readyStatus: "idle",
|
||||||
|
} as StateRequest)
|
||||||
|
|
||||||
|
const boxList = createSlice({
|
||||||
|
name: "boxList",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
getRequesting: (state) => {
|
||||||
|
state.readyStatus = "request"
|
||||||
|
},
|
||||||
|
getSuccess: (state, { payload }: PayloadAction<Box[]>) => {
|
||||||
|
state.readyStatus = "success"
|
||||||
|
boxAdapter.setAll(state, payload)
|
||||||
|
},
|
||||||
|
getFailure: (state, { payload }: PayloadAction<string>) => {
|
||||||
|
state.readyStatus = "failure"
|
||||||
|
state.error = payload
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default boxList.reducer
|
||||||
|
export const { getRequesting, getSuccess, getFailure } = boxList.actions
|
||||||
|
|
||||||
|
export const fetchBoxList = elementListFetch(
|
||||||
|
detailedBoxListGet,
|
||||||
|
getRequesting,
|
||||||
|
getSuccess,
|
||||||
|
getFailure,
|
||||||
|
(error: Error) => toastError(`Erreur lors du chargement des jeux JAV: ${error.message}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
const shouldFetchBoxList = (state: AppState) => state.boxList.readyStatus !== "success"
|
||||||
|
|
||||||
|
export const fetchBoxListIfNeed = (): AppThunk => (dispatch, getState) => {
|
||||||
|
if (shouldFetchBoxList(getState())) return dispatch(fetchBoxList())
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectBoxListState = (state: AppState): EntitiesRequest<Box> => state.boxList
|
||||||
|
|
||||||
|
export const selectBoxList = createSelector(
|
||||||
|
selectBoxListState,
|
||||||
|
({ ids, entities, readyStatus }) => {
|
||||||
|
if (readyStatus !== "success") return []
|
||||||
|
return ids.map((id) => entities[id])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const selectSortedUniqueDetailedBoxes = createSelector(selectBoxList, (boxes) =>
|
||||||
|
sortedUniqBy(
|
||||||
|
sortBy(
|
||||||
|
boxes.filter((box) => box && !box.unplayable),
|
||||||
|
"title"
|
||||||
|
),
|
||||||
|
"title"
|
||||||
|
)
|
||||||
|
)
|
@ -3,6 +3,7 @@ import { connectRouter } from "connected-react-router"
|
|||||||
|
|
||||||
import announcementList from "./announcementList"
|
import announcementList from "./announcementList"
|
||||||
import auth from "./auth"
|
import auth from "./auth"
|
||||||
|
import boxList from "./boxList"
|
||||||
import gameList from "./gameList"
|
import gameList from "./gameList"
|
||||||
import miscDiscordInvitation from "./miscDiscordInvitation"
|
import miscDiscordInvitation from "./miscDiscordInvitation"
|
||||||
import miscMeetingDateList from "./miscMeetingDateList"
|
import miscMeetingDateList from "./miscMeetingDateList"
|
||||||
@ -10,16 +11,17 @@ import postulantAdd from "./postulantAdd"
|
|||||||
import teamList from "./teamList"
|
import teamList from "./teamList"
|
||||||
import ui from "./ui"
|
import ui from "./ui"
|
||||||
import volunteerAdd from "./volunteerPartialAdd"
|
import volunteerAdd from "./volunteerPartialAdd"
|
||||||
import volunteerDiscordId from "./volunteerDiscordId"
|
|
||||||
import volunteerList from "./volunteerList"
|
|
||||||
import volunteerSet from "./volunteerSet"
|
|
||||||
import volunteerLogin from "./volunteerLogin"
|
|
||||||
import volunteerForgot from "./volunteerForgot"
|
|
||||||
import volunteerAsksSet from "./volunteerAsksSet"
|
import volunteerAsksSet from "./volunteerAsksSet"
|
||||||
import volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
|
|
||||||
import volunteerDayWishesSet from "./volunteerDayWishesSet"
|
import volunteerDayWishesSet from "./volunteerDayWishesSet"
|
||||||
import volunteerTeamWishesSet from "./volunteerTeamWishesSet"
|
import volunteerDiscordId from "./volunteerDiscordId"
|
||||||
|
import volunteerForgot from "./volunteerForgot"
|
||||||
|
import volunteerList from "./volunteerList"
|
||||||
|
import volunteerLogin from "./volunteerLogin"
|
||||||
|
import volunteerKnowledgeSet from "./volunteerKnowledgeSet"
|
||||||
|
import volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
|
||||||
|
import volunteerSet from "./volunteerSet"
|
||||||
import volunteerTeamAssignSet from "./volunteerTeamAssignSet"
|
import volunteerTeamAssignSet from "./volunteerTeamAssignSet"
|
||||||
|
import volunteerTeamWishesSet from "./volunteerTeamWishesSet"
|
||||||
import wishAdd from "./wishAdd"
|
import wishAdd from "./wishAdd"
|
||||||
import wishList from "./wishList"
|
import wishList from "./wishList"
|
||||||
|
|
||||||
@ -28,6 +30,7 @@ import wishList from "./wishList"
|
|||||||
export default (history: History) => ({
|
export default (history: History) => ({
|
||||||
announcementList,
|
announcementList,
|
||||||
auth,
|
auth,
|
||||||
|
boxList,
|
||||||
gameList,
|
gameList,
|
||||||
miscDiscordInvitation,
|
miscDiscordInvitation,
|
||||||
miscMeetingDateList,
|
miscMeetingDateList,
|
||||||
@ -35,16 +38,17 @@ export default (history: History) => ({
|
|||||||
teamList,
|
teamList,
|
||||||
ui,
|
ui,
|
||||||
volunteerAdd,
|
volunteerAdd,
|
||||||
volunteerDiscordId,
|
|
||||||
volunteerList,
|
|
||||||
volunteerSet,
|
|
||||||
volunteerLogin,
|
|
||||||
volunteerForgot,
|
|
||||||
volunteerAsksSet,
|
volunteerAsksSet,
|
||||||
volunteerParticipationDetailsSet,
|
|
||||||
volunteerDayWishesSet,
|
volunteerDayWishesSet,
|
||||||
volunteerTeamWishesSet,
|
volunteerDiscordId,
|
||||||
|
volunteerForgot,
|
||||||
|
volunteerList,
|
||||||
|
volunteerLogin,
|
||||||
|
volunteerKnowledgeSet,
|
||||||
|
volunteerParticipationDetailsSet,
|
||||||
|
volunteerSet,
|
||||||
volunteerTeamAssignSet,
|
volunteerTeamAssignSet,
|
||||||
|
volunteerTeamWishesSet,
|
||||||
wishAdd,
|
wishAdd,
|
||||||
wishList,
|
wishList,
|
||||||
router: connectRouter(history) as any,
|
router: connectRouter(history) as any,
|
||||||
|
@ -37,7 +37,8 @@ export const fetchVolunteerDayWishesSet = elementFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
getFailure,
|
||||||
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
|
(error: Error) =>
|
||||||
|
toastError(`Erreur lors du chargement des choix de jours de présence: ${error.message}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
const shouldFetchVolunteerDayWishesSet = (state: AppState, id: number) =>
|
const shouldFetchVolunteerDayWishesSet = (state: AppState, id: number) =>
|
||||||
|
86
src/store/volunteerKnowledgeSet.ts
Normal file
86
src/store/volunteerKnowledgeSet.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||||
|
import { shallowEqual, useSelector } from "react-redux"
|
||||||
|
import { useCallback } from "react"
|
||||||
|
import { StateRequest, toastError, elementFetch } from "./utils"
|
||||||
|
import { VolunteerKnowledge } from "../services/volunteers"
|
||||||
|
import { AppThunk, AppState } from "."
|
||||||
|
import { volunteerKnowledgeSet } from "../services/volunteersAccessors"
|
||||||
|
import useAction from "../utils/useAction"
|
||||||
|
import { selectUserJwtToken } from "./auth"
|
||||||
|
|
||||||
|
type StateVolunteerKnowledgeSet = {
|
||||||
|
entity?: VolunteerKnowledge
|
||||||
|
} & StateRequest
|
||||||
|
|
||||||
|
export const initialState: StateVolunteerKnowledgeSet = {
|
||||||
|
readyStatus: "idle",
|
||||||
|
}
|
||||||
|
|
||||||
|
const volunteerKnowledgeSetSlice = createSlice({
|
||||||
|
name: "volunteerKnowledgeSet",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
getRequesting: (_) => ({
|
||||||
|
readyStatus: "request",
|
||||||
|
}),
|
||||||
|
getSuccess: (_, { payload }: PayloadAction<VolunteerKnowledge>) => ({
|
||||||
|
readyStatus: "success",
|
||||||
|
entity: payload,
|
||||||
|
}),
|
||||||
|
getFailure: (_, { payload }: PayloadAction<string>) => ({
|
||||||
|
readyStatus: "failure",
|
||||||
|
error: payload,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default volunteerKnowledgeSetSlice.reducer
|
||||||
|
export const { getRequesting, getSuccess, getFailure } = volunteerKnowledgeSetSlice.actions
|
||||||
|
|
||||||
|
export const fetchVolunteerKnowledgeSet = elementFetch(
|
||||||
|
volunteerKnowledgeSet,
|
||||||
|
getRequesting,
|
||||||
|
getSuccess,
|
||||||
|
getFailure,
|
||||||
|
(error: Error) => toastError(`Erreur lors du chargement des connaissances: ${error.message}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
const shouldFetchVolunteerKnowledgeSet = (state: AppState, id: number) =>
|
||||||
|
state.volunteerKnowledgeSet?.readyStatus !== "success" ||
|
||||||
|
(state.volunteerKnowledgeSet?.entity && state.volunteerKnowledgeSet?.entity?.id !== id)
|
||||||
|
|
||||||
|
export const fetchVolunteerKnowledgeSetIfNeed =
|
||||||
|
(id = 0, knowledge: Partial<VolunteerKnowledge> = {}): AppThunk =>
|
||||||
|
(dispatch, getState) => {
|
||||||
|
let jwt = ""
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
;({ jwt, id } = getState().auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldFetchVolunteerKnowledgeSet(getState(), id))
|
||||||
|
return dispatch(fetchVolunteerKnowledgeSet(jwt, id, knowledge))
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVolunteerKnowledge = (): [
|
||||||
|
VolunteerKnowledge | undefined,
|
||||||
|
(newVolunteerKnowledge: VolunteerKnowledge) => void
|
||||||
|
] => {
|
||||||
|
const save = useAction(fetchVolunteerKnowledgeSet)
|
||||||
|
const jwtToken = useSelector(selectUserJwtToken)
|
||||||
|
const volunteerKnowledge = useSelector(
|
||||||
|
(state: AppState) => state.volunteerKnowledgeSet?.entity,
|
||||||
|
shallowEqual
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveVolunteerKnowledge = useCallback(
|
||||||
|
(newVolunteerKnowledge: VolunteerKnowledge) => {
|
||||||
|
save(jwtToken, 0, newVolunteerKnowledge)
|
||||||
|
},
|
||||||
|
[save, jwtToken]
|
||||||
|
)
|
||||||
|
|
||||||
|
return [volunteerKnowledge, saveVolunteerKnowledge]
|
||||||
|
}
|
@ -40,7 +40,8 @@ export const fetchVolunteerParticipationDetailsSet = elementFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
getFailure,
|
||||||
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
|
(error: Error) =>
|
||||||
|
toastError(`Erreur lors du chargement des détails de participation: ${error.message}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
const shouldFetchVolunteerParticipationDetailsSet = (state: AppState, id: number) =>
|
const shouldFetchVolunteerParticipationDetailsSet = (state: AppState, id: number) =>
|
||||||
|
@ -37,7 +37,8 @@ export const fetchVolunteerTeamAssignSet = elementFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
getFailure,
|
||||||
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
|
(error: Error) =>
|
||||||
|
toastError(`Erreur lors du chargement des assignation d'équipe: ${error.message}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
const shouldFetchVolunteerTeamAssignSet = (state: AppState, id: number) =>
|
const shouldFetchVolunteerTeamAssignSet = (state: AppState, id: number) =>
|
||||||
|
@ -37,7 +37,7 @@ export const fetchVolunteerTeamWishesSet = elementFetch(
|
|||||||
getRequesting,
|
getRequesting,
|
||||||
getSuccess,
|
getSuccess,
|
||||||
getFailure,
|
getFailure,
|
||||||
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
|
(error: Error) => toastError(`Erreur lors du chargement des choix d'équipe: ${error.message}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
const shouldFetchVolunteerTeamWishesSet = (state: AppState, id: number) =>
|
const shouldFetchVolunteerTeamWishesSet = (state: AppState, id: number) =>
|
||||||
|
@ -10,6 +10,15 @@ $color-grey-light: #ccc;
|
|||||||
$color-grey-lighter: #eee;
|
$color-grey-lighter: #eee;
|
||||||
$color-green: #080;
|
$color-green: #080;
|
||||||
|
|
||||||
|
$color-unknown: rgb(232, 223, 235);
|
||||||
|
$color-active-unknown: rgb(167, 74, 196);
|
||||||
|
$color-ok: rgb(215, 233, 217);
|
||||||
|
$color-active-ok: rgb(27, 105, 11);
|
||||||
|
$color-bof: rgb(231, 227, 214);
|
||||||
|
$color-active-bof: rgb(180, 124, 19);
|
||||||
|
$color-niet: rgb(233, 215, 217);
|
||||||
|
$color-active-niet: rgb(158, 17, 41);
|
||||||
|
|
||||||
$border-large: 4px solid $color-black;
|
$border-large: 4px solid $color-black;
|
||||||
$border-thin: 2px solid $color-black;
|
$border-thin: 2px solid $color-black;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user