Add Mes connaissances

This commit is contained in:
pikiou 2022-05-11 02:23:08 +02:00
parent 0d69bc93db
commit 1b93b41225
31 changed files with 691 additions and 46 deletions

BIN
src/app/img/poufpaf.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

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

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

View 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

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

View File

@ -52,6 +52,7 @@ const MainMenu: FC = (): JSX.Element => {
<MenuItem name="Questions" pathname="/" />
<MenuItem name="Annonces" pathname="/annonces" />
<MenuItem name="Mon profil" pathname="/profil" />
<MenuItem name="Mes connaissances" pathname="/connaissances" />
<RestrictMenuItem
role={ROLES.ASSIGNER}
name="Gestion équipes"

View File

@ -462,6 +462,7 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
Ces rencontres ont lieu à 19h dans un bar/resto calme à Châtelet, le{" "}
<a
href="https://goo.gl/maps/N5NYWDF66vNQDFMh8"
id="sfmMap"
key="sfmMap"
target="_blank"
rel="noreferrer"
@ -471,6 +472,7 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
, ou à une soirée festive à 2 pas du lieu du festival, aux{" "}
<a
href="https://www.captainturtle.fr/aperos-petanque-paris/"
id="petanque"
key="petanque"
target="_blank"
rel="noreferrer"

View File

@ -7,6 +7,8 @@ import ErrorBoundary from "./ErrorBoundary"
import GameList from "./GameList"
import Loading from "./Loading"
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 ParticipationDetailsForm, {
fetchFor as fetchForParticipationDetailsForm,
@ -25,10 +27,13 @@ export {
AnnouncementLink,
Board,
fetchForBoard,
BoxList,
fetchForKnowledge,
DayWishesForm,
fetchForDayWishesForm,
ErrorBoundary,
GameList,
KnowledgeIntro,
Loading,
LoginForm,
Asks,

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

View File

@ -0,0 +1,9 @@
@import "../../theme/mixins";
.knowledgesPage {
@include page-wrapper-center;
}
.knowledgesContent {
@include page-content-wrapper(700px);
}

View File

@ -5,6 +5,7 @@ import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements"
import AsyncTeamAssignment, { loadData as loadTeamAssignmentData } from "../pages/TeamAssignment"
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 AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
@ -23,6 +24,11 @@ export default [
component: AsyncHome,
loadData: loadHomeData,
},
{
path: "/connaissances",
component: AsyncKnowledge,
loadData: loadKnowledgeData,
},
{
path: "/preRegister",
component: AsyncRegisterPage,

View File

@ -406,8 +406,7 @@ export class Sheet<
}
break
default:
// eslint-disable-next-line no-case-declarations
default: {
const matchArrayType = type.match(
/^(number|string|boolean|date)\[([^\]]+)\]$/
)
@ -438,11 +437,9 @@ export class Sheet<
)
break
case "date":
// eslint-disable-next-line no-case-declarations
case "date": {
const rawDates = rawProp.split(delimiter)
element[prop] = []
// eslint-disable-next-line no-case-declarations
rawDates.forEach((rawDate) => {
try {
element[prop].push(parseDate(rawDate))
@ -453,12 +450,14 @@ export class Sheet<
}
})
break
}
default:
throw new Error(
`Unknown array type ${arrayType} in sheet ${this.name} at prop ${prop}`
)
}
}
}
}
return element
},
@ -492,19 +491,20 @@ export class Sheet<
stringifiedElement[prop as keyof Element] = stringifiedDate(value)
break
default:
// eslint-disable-next-line no-case-declarations
default: {
const matchArrayType = type.match(
/^(number|string|boolean|date)\[([^\]]+)\]$/
)
if (!matchArrayType || !_.isArray(value)) {
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]
// eslint-disable-next-line no-case-declarations
const delimiter = matchArrayType[2]
switch (arrayType) {
@ -554,6 +554,7 @@ export class Sheet<
default:
throw new Error(`Unknown array type ${arrayType}`)
}
}
}
return stringifiedElement
@ -586,7 +587,6 @@ function stringifiedDate(value: unknown): string {
}
function parseDate(value: string): Date {
// eslint-disable-next-line no-case-declarations
const matchDate = value.match(/^([0-9]+)\/([0-9]+)\/([0-9]+)$/)
if (!matchDate) {
throw new Error(`Unable to read date from val ${value}`)

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

View File

@ -8,6 +8,6 @@ const expressAccessor = new ExpressAccessors<GameWithoutId, Game>(
)
export const gameListGet = expressAccessor.listGet()
export const gameGet = expressAccessor.get()
export const gameAdd = expressAccessor.add()
export const gameSet = expressAccessor.set()
// export const gameGet = expressAccessor.get()
// export const gameAdd = expressAccessor.add()
// export const gameSet = expressAccessor.set()

View File

@ -12,6 +12,8 @@ const ANONYMIZED_DB_PATH = path.resolve(process.cwd(), "access/dbAnonymized.json
export class SheetNames {
Announcements = "Annonces"
Boxes = "Boîtes"
Games = "Jeux"
Miscs = "Divers"

View File

@ -1,4 +1,4 @@
import _ from "lodash"
import { assign, cloneDeep, keys, omit, pick } from "lodash"
import bcrypt from "bcrypt"
import sgMail from "@sendgrid/mail"
@ -13,6 +13,7 @@ import {
VolunteerDayWishes,
VolunteerParticipationDetails,
VolunteerTeamAssign,
VolunteerKnowledge,
} from "../../services/volunteers"
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
import { getJwt } from "../secure"
@ -40,7 +41,7 @@ export const volunteerDiscordId = expressAccessor.get(async (list, body, id) =>
if (!volunteer) {
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) => {
@ -58,9 +59,9 @@ export const volunteerPartialAdd = expressAccessor.add(async (list, body) => {
const password = generatePassword()
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),
firstname: trim(params.firstname),
email: trim(params.email),
@ -135,7 +136,7 @@ export const volunteerForgot = expressAccessor.set(async (list, bodyArray) => {
if (!volunteer) {
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 timeSinceLastSent = now - lastForgot[volunteer.id]
@ -196,9 +197,9 @@ export const volunteerAsksSet = expressAccessor.set(async (list, body, id) => {
if (!volunteer) {
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 {
toDatabase: newVolunteer,
@ -226,7 +227,7 @@ export const volunteerTeamWishesSet = expressAccessor.set(async (list, body, id,
if (!volunteer) {
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) {
newVolunteer.teamWishes = wishes.teamWishes
@ -255,7 +256,7 @@ export const volunteerDayWishesSet = expressAccessor.set(async (list, body, id)
if (!volunteer) {
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) {
newVolunteer.active = wishes.active
@ -290,7 +291,7 @@ export const volunteerParticipationDetailsSet = expressAccessor.set(async (list,
if (!volunteer) {
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) {
newVolunteer.tshirtSize = wishes.tshirtSize
@ -329,7 +330,7 @@ export const volunteerTeamAssignSet = expressAccessor.set(async (list, body, id)
if (!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
return {
@ -345,3 +346,26 @@ function getByEmail<T extends { email: string }>(list: T[], rawEmail: string): T
const email = canonicalEmail(rawEmail || "")
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,
}
})

View File

@ -18,6 +18,7 @@ import ssr from "./ssr"
import certbotRouter from "../routes/certbot"
import { hasSecret, secure } from "./secure"
import { announcementListGet } from "./gsheets/announcements"
import { detailedBoxListGet } from "./gsheets/boxes"
import { gameListGet } from "./gsheets/games"
import { postulantAdd } from "./gsheets/postulants"
import { teamListGet } from "./gsheets/teams"
@ -33,6 +34,7 @@ import {
volunteerTeamWishesSet,
volunteerTeamAssignSet,
volunteerListGet,
volunteerKnowledgeSet,
} from "./gsheets/volunteers"
import { wishListGet, wishAdd } from "./gsheets/wishes"
import config from "../config"
@ -84,6 +86,7 @@ app.get(
* APIs
*/
// Google Sheets API
app.get("/BoxDetailedListGet", detailedBoxListGet)
app.get("/GameListGet", gameListGet)
app.get("/MiscMeetingDateListGet", miscMeetingDateListGet)
app.get("/WishListGet", wishListGet)
@ -101,6 +104,7 @@ app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
app.get("/TeamListGet", teamListGet)
app.get("/VolunteerDiscordId", secure as RequestHandler, volunteerDiscordId)
app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet)
app.post("/VolunteerKnowledgeSet", secure as RequestHandler, volunteerKnowledgeSet)
app.post(
"/VolunteerParticipationDetailsSet",
secure as RequestHandler,

48
src/services/boxes.ts Normal file
View 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">

View 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()

View File

@ -4,6 +4,6 @@ import { elementName, Game, GameWithoutId } from "./games"
const serviceAccessors = new ServiceAccessors<GameWithoutId, Game>(elementName)
export const gameListGet = serviceAccessors.listGet()
export const gameGet = serviceAccessors.get()
export const gameAdd = serviceAccessors.add()
export const gameSet = serviceAccessors.set()
// export const gameGet = serviceAccessors.get()
// export const gameAdd = serviceAccessors.add()
// export const gameSet = serviceAccessors.set()

View File

@ -53,6 +53,12 @@ export class Volunteer implements VolunteerPartial {
pushNotifSubscription = ""
acceptsNotifs = ""
ok: number[] = []
bof: number[] = []
niet: number[] = []
}
export const translationVolunteer: { [k in keyof Volunteer]: string } = {
@ -83,6 +89,9 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
password2: "passe2",
pushNotifSubscription: "pushNotifSubscription",
acceptsNotifs: "accepteLesNotifs",
ok: "OK",
bof: "Bof",
niet: "Niet",
}
export class VolunteerPartial {
@ -125,6 +134,9 @@ export const volunteerExample: Volunteer = {
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
pushNotifSubscription: "",
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 =
@ -184,3 +196,11 @@ export interface VolunteerTeamAssign {
volunteer: number
team: Volunteer["team"]
}
export type VolunteerKnowledgeWithoutId = Omit<VolunteerKnowledge, "id">
export interface VolunteerKnowledge {
id: Volunteer["id"]
ok: Volunteer["ok"]
bof: Volunteer["bof"]
niet: Volunteer["niet"]
}

View File

@ -9,6 +9,7 @@ import {
VolunteerTeamAssign,
VolunteerWithoutId,
VolunteerDiscordId,
VolunteerKnowledge,
} from "./volunteers"
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
@ -42,3 +43,6 @@ export const volunteerParticipationDetailsSet =
export const volunteerTeamAssignSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerTeamAssign>]>("TeamAssignSet")
export const volunteerKnowledgeSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerKnowledge>]>("KnowledgeSet")

70
src/store/boxList.ts Normal file
View 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"
)
)

View File

@ -3,6 +3,7 @@ import { connectRouter } from "connected-react-router"
import announcementList from "./announcementList"
import auth from "./auth"
import boxList from "./boxList"
import gameList from "./gameList"
import miscDiscordInvitation from "./miscDiscordInvitation"
import miscMeetingDateList from "./miscMeetingDateList"
@ -10,16 +11,17 @@ import postulantAdd from "./postulantAdd"
import teamList from "./teamList"
import ui from "./ui"
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 volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
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 volunteerTeamWishesSet from "./volunteerTeamWishesSet"
import wishAdd from "./wishAdd"
import wishList from "./wishList"
@ -28,6 +30,7 @@ import wishList from "./wishList"
export default (history: History) => ({
announcementList,
auth,
boxList,
gameList,
miscDiscordInvitation,
miscMeetingDateList,
@ -35,16 +38,17 @@ export default (history: History) => ({
teamList,
ui,
volunteerAdd,
volunteerDiscordId,
volunteerList,
volunteerSet,
volunteerLogin,
volunteerForgot,
volunteerAsksSet,
volunteerParticipationDetailsSet,
volunteerDayWishesSet,
volunteerTeamWishesSet,
volunteerDiscordId,
volunteerForgot,
volunteerList,
volunteerLogin,
volunteerKnowledgeSet,
volunteerParticipationDetailsSet,
volunteerSet,
volunteerTeamAssignSet,
volunteerTeamWishesSet,
wishAdd,
wishList,
router: connectRouter(history) as any,

View File

@ -37,7 +37,8 @@ export const fetchVolunteerDayWishesSet = elementFetch(
getRequesting,
getSuccess,
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) =>

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

View File

@ -40,7 +40,8 @@ export const fetchVolunteerParticipationDetailsSet = elementFetch(
getRequesting,
getSuccess,
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) =>

View File

@ -37,7 +37,8 @@ export const fetchVolunteerTeamAssignSet = elementFetch(
getRequesting,
getSuccess,
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) =>

View File

@ -37,7 +37,7 @@ export const fetchVolunteerTeamWishesSet = elementFetch(
getRequesting,
getSuccess,
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) =>

View File

@ -10,6 +10,15 @@ $color-grey-light: #ccc;
$color-grey-lighter: #eee;
$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-thin: 2px solid $color-black;