From 1b93b412257bc926ee1fe737517219f3abc40a94 Mon Sep 17 00:00:00 2001 From: pikiou Date: Wed, 11 May 2022 02:23:08 +0200 Subject: [PATCH] =?UTF-8?q?Add=20Mes=20connaissances=C2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/img/poufpaf.png | Bin 0 -> 3901 bytes src/components/Knowledge/BoxItem.tsx | 97 +++++++++++++++ src/components/Knowledge/BoxList.tsx | 33 +++++ src/components/Knowledge/KnowledgeIntro.tsx | 20 +++ src/components/Knowledge/styles.module.scss | 117 ++++++++++++++++++ src/components/Navigation/MainMenu.tsx | 1 + src/components/RegisterForm/index.tsx | 2 + src/components/index.ts | 5 + src/pages/Knowledge/KnowledgesPage.tsx | 24 ++++ src/pages/Knowledge/index.tsx | 16 +++ src/pages/Knowledge/styles.module.scss | 9 ++ src/routes/index.ts | 6 + src/server/gsheets/accessors.ts | 22 ++-- src/server/gsheets/boxes.ts | 32 +++++ src/server/gsheets/games.ts | 6 +- src/server/gsheets/localDb.ts | 2 + src/server/gsheets/volunteers.ts | 46 +++++-- src/server/index.ts | 4 + src/services/boxes.ts | 48 +++++++ src/services/boxesAccessors.ts | 9 ++ src/services/gamesAccessors.ts | 6 +- src/services/volunteers.ts | 20 +++ src/services/volunteersAccessors.ts | 4 + src/store/boxList.ts | 70 +++++++++++ src/store/rootReducer.ts | 32 ++--- src/store/volunteerDayWishesSet.ts | 3 +- src/store/volunteerKnowledgeSet.ts | 86 +++++++++++++ src/store/volunteerParticipationDetailsSet.ts | 3 +- src/store/volunteerTeamAssignSet.ts | 3 +- src/store/volunteerTeamWishesSet.ts | 2 +- src/theme/variables.scss | 9 ++ 31 files changed, 691 insertions(+), 46 deletions(-) create mode 100755 src/app/img/poufpaf.png create mode 100644 src/components/Knowledge/BoxItem.tsx create mode 100644 src/components/Knowledge/BoxList.tsx create mode 100644 src/components/Knowledge/KnowledgeIntro.tsx create mode 100755 src/components/Knowledge/styles.module.scss create mode 100644 src/pages/Knowledge/KnowledgesPage.tsx create mode 100755 src/pages/Knowledge/index.tsx create mode 100755 src/pages/Knowledge/styles.module.scss create mode 100644 src/server/gsheets/boxes.ts create mode 100644 src/services/boxes.ts create mode 100644 src/services/boxesAccessors.ts create mode 100644 src/store/boxList.ts create mode 100644 src/store/volunteerKnowledgeSet.ts diff --git a/src/app/img/poufpaf.png b/src/app/img/poufpaf.png new file mode 100755 index 0000000000000000000000000000000000000000..673f13e2e74a93ea6a8654211856d8cb4aff4d04 GIT binary patch literal 3901 zcmV-D55n+?P)81Iv9}zl;r>b02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{01l-|L_t(|+U=ctkQCJwhrgK} z_Q4y0)hI^V2T>!5p%T#qgRhuUjK+Xyq8KnyOht_n4XIR3i!&w~;|mqbs1*=2qOl5m zk{GODsWGB~fZz*d)_|a_SrB&FW!ag@ANNeQoy_#(&h6P=;aAngFx%6&`<{EA_ngx) z_QD7(I~@Z~1%?B^1@;ekiPwQM5__l8bE0cXOzzf9D%o1}-C4i_pugr&qk*xBJ!Db= z5LRpbZTfaigHkkwm6h*N>I@DZ2 zK5&obgY^gI0DUYweSapE%xPRB4?F{8t0klO?*h*xgb*fHQ5|rxtjf!P zRu|6r0dRMPtE>UI4XCl~^t~ROQf_kGIP3$NEK?ko1?~mzbZ|tbSnS4SB2A$Nm;!v$ zp___z##G?Rc!&t+1{U0#MyTok2g3@l-F)B+CxCx@V5PX@kL^!*62 z%6b*zVP3niO6A{UfpMnH!y@bpoGT8R(+4>?LUSGS!?i&O@&WLn&vS=~ymaEbBC`D8 zBj6o5Hv9a|g+P6T7ym3-Bt{7GhM2YA2G;rXZ=3i0qq{Y z9U`kRs%Q`=_$A^f|#T@_7nykjMGkf$bhP`r|%y$!Wls zdkH~)DvtF3i4(sUs0F?RJnF-{|5!ZefFn2y$cno)>%Y3 zU6S4NXk_tNMWCY+Gh6a#5SY$`Xggi`4Mq(L&TUXN^pnfH4;AVWsryY+zLEwXz~&s z(xWrrBI}}bW{YuxyoK7^zC4pkwz;eTubZC+98u2PwCr>paHPjIIy4mRm{Q&{YJbIT z-{yLtIlws2$q@=cT2LYJ9|IFmxlQdJtK-*nZ|0XmKh&0KO&OnHB=94T`?{sXeLO_@ zErU4li!z*0D@QrB6(v^WJfuUX9L5Ne1)fA@|BXWh#^%btuxmD<*xIW-xFiOg0374w z68ZcE;7@pO`>rhxOroOHJ3YkUvCfS&;l@(}F4AUMbVg*Q7v2LdkO$V)5go!=!(Gm4 z1RevW}PQNG)@GYVb%b^{W!t<#`XW8l9)gZ`vv7Ogc z=;*Bz@!^$jxj(S9=(`#p*I!!3qROK7@n^Z1%=^hgSSX(-c}R}6a(*cA5?PysGfem> z>wJb&YKjgov;x@ z2rC1MjwVzc%895DxK~2N8>nKRyC}1FQS@vpft8k>9)QaJ@6ws0m}RH)F6?$#CY9W1 znnlDBt3y>x%|zv2HF(J2Pf**!OMun7mY*XnX^Z^bO{m|&}na1FMU+Gji%3*Q>NTUj8ri*EKpAh1&_|N4vL72$GveRQx zWfapz)W$*@YGO2Up+cR_qD4{Ml&Ioa4 z8>;bK2@j<;6ga*@!sSq#GdoO(Cb`E!s7(Cd;9lae*s{|P0?R7o`l>D5gu`6hSCrN7 z9E2JKe5Jx0%1emjKFdxo0k&mQ$(@?Ss6k~y-X+$J*L__i4&)TziQd5ZF(JlOR4G6$ zW{W_bDC{dgjEbzb1zEM?KU^mAyl=oovQWI{9U9FIMxX+e)hGsfGA?G=9pcc`h0Klh zzz=~Hzy)RYv&&(qSULSPICe5>??37~b3wtg>{rD>y%mXyS zG*w5u2gmLUoCoaVMHJcRKBB@EJ`PnXx0fI;I2pCYTI+G%5LA{+C8!04i7tvFI)Pn1 zO^_I>81y)ooU8)j`WP7R2EwV!_hV`1N*ud^2T^TCW+J^#dKyDe?eU&>A=*?QT;!Ka zs)a3dXQ%uV)rYYZq23nmAd^Z)rlZ_A@h>))5v!&+xdGKh?*5SDXPbeuf!CZI;%CgL z5M&H0XR8!JPRC!3yC2kvnSX!C&(jRNrn6NLC;F&2>>u?pq>mzJWo;7+X&x%++IWOm zBCTIHeAx+vTQicQyGNv!_pE~~Do(Um20lnEp(OBmgPhk_6uRk%L6{C=R;;F6wK?Ry zOe&d|Wb5})H6o)?Ta-Bi+<{nMrejDkQS4!fts3B5-MKZULxec3r~u&KH7+r#gwr;G zaDsYgg{Vj3s4!2gM+xG(iTmDg2p3~!bilomI_gh7zQQ6*?W zEtAXXPV+1j&V(5VJlZ8XU;@sL+@M+Wk65t)1xaHz)5i_3@jMOr|nP&&sC3Z+#u7a3Ca>}AbEs|v zI8>!RSd{NDnY?+Z%yJz(h00C`Rp2=xAR(SbWn9iDSd-@dL=3tFsX-NS-hnEF*$4dq zU@NLIWB~(F``w!Q+3XdCc!CY3ChyE=+ix3K5{ z)C*lcTRNly>%dxpbM1 zO;WCh?w&@F?PA`paeQ|ks(8PeIH!vKNCzG@jtPSI^)Mx>o&{DEoIyullc2A%h)V`! zJ^C9@amNe3!g$ua5aVj#RWE{E(gOr}0hMRvj0%rLrI`lBEL4Zj8dR;Kj-`1cs*u!V z#aOc#EOSs9HCa?wj9gfLh%VYtNd})3t;jmMOT~iVApCbsEob_UH2jPo3V~k*vO$u{ zt4)wk#LRruNenS}kL*F6)=x~}e;c~q7GQalvre+&NuLcoQi5&cB6@l%8Tt_1tAKk& z=)|u=TZ4K%-7J@P97ZAEHfBh%=R`+pWu3%BR_E0t7$3SW!1h{ zha|%t7!fD=P|Hql?8&-$9fIUhEtclWzd2bEx|xfOL?gggi)v0bo*sNtgAPG9p|T0L zR=SeJJ0DAQk}YC#NHW96 z;!(ehOGT1pr*B7Xdfles{m(?TApE$uaEK`k5vwK$wgazbQpuc0d-EJByszV&#T*95 z9z~Ei!LmMbs4Cj7kngL3Dz##Y3_&U=bzT*)O{AVwYruJ5 zL9h;$P#1KpHdNW#JltE0ccS{q=TVzWa~#o#7F0BPw%q$#R2z1uzUnbg-INdD=Q=r5 zdgyFaR(pHULF83LPoQ=#k9Ik=S%{WLWr^g9COeP!tHY~Kkmaa0+LfXWl?yi0 void +} + +const BoxItem: React.FC = ({ + 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 ( +
  • +
    + +
    + + +
    +
    +
      + {knowledgeChoices.map(({ name, value }) => ( +
    • + +
    • + ))} +
    +
  • + ) +} + +export default memo(BoxItem) diff --git a/src/components/Knowledge/BoxList.tsx b/src/components/Knowledge/BoxList.tsx new file mode 100644 index 0000000..80b418a --- /dev/null +++ b/src/components/Knowledge/BoxList.tsx @@ -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 ( +
      + {detailedBoxes.map((detailedBox: any) => ( + + ))} +
    + ) +} + +export default memo(BoxList) + +export const fetchFor = [fetchBoxListIfNeed, fetchVolunteerKnowledgeSetIfNeed] diff --git a/src/components/Knowledge/KnowledgeIntro.tsx b/src/components/Knowledge/KnowledgeIntro.tsx new file mode 100644 index 0000000..a68156c --- /dev/null +++ b/src/components/Knowledge/KnowledgeIntro.tsx @@ -0,0 +1,20 @@ +import React from "react" + +const KnowledgeIntro: React.FC = (): JSX.Element => ( +
    +

    Tes connaissances en jeux

    +

    + 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. +

    +

    + 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. +
    + Bof signifie que tu seras plus utile que la lecture des règles. +

    +
    +) + +export default KnowledgeIntro diff --git a/src/components/Knowledge/styles.module.scss b/src/components/Knowledge/styles.module.scss new file mode 100755 index 0000000..d9eb008 --- /dev/null +++ b/src/components/Knowledge/styles.module.scss @@ -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; + } +} diff --git a/src/components/Navigation/MainMenu.tsx b/src/components/Navigation/MainMenu.tsx index f0d36b2..dafb203 100644 --- a/src/components/Navigation/MainMenu.tsx +++ b/src/components/Navigation/MainMenu.tsx @@ -52,6 +52,7 @@ const MainMenu: FC = (): JSX.Element => { + { Ces rencontres ont lieu à 19h dans un bar/resto calme à Châtelet, le{" "} { , ou à une soirée festive à 2 pas du lieu du festival, aux{" "} = (): JSX.Element => ( +
    +
    + + + +
    +
    +) + +// Fetch server-side data here +export const loadData = (): AppThunk[] => [...fetchForKnowledge.map((f) => f())] + +export default memo(KnowledgesPage) diff --git a/src/pages/Knowledge/index.tsx b/src/pages/Knowledge/index.tsx new file mode 100755 index 0000000..5d41959 --- /dev/null +++ b/src/pages/Knowledge/index.tsx @@ -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: , +}) + +export default (props: Props): JSX.Element => ( + + + +) + +export { loadData } diff --git a/src/pages/Knowledge/styles.module.scss b/src/pages/Knowledge/styles.module.scss new file mode 100755 index 0000000..4c6d8ae --- /dev/null +++ b/src/pages/Knowledge/styles.module.scss @@ -0,0 +1,9 @@ +@import "../../theme/mixins"; + +.knowledgesPage { + @include page-wrapper-center; +} + +.knowledgesContent { + @include page-content-wrapper(700px); +} diff --git a/src/routes/index.ts b/src/routes/index.ts index 15a1fd8..630bdfa 100755 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -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, diff --git a/src/server/gsheets/accessors.ts b/src/server/gsheets/accessors.ts index 8b51ab2..8df7e77 100644 --- a/src/server/gsheets/accessors.ts +++ b/src/server/gsheets/accessors.ts @@ -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}`) diff --git a/src/server/gsheets/boxes.ts b/src/server/gsheets/boxes.ts new file mode 100644 index 0000000..2487df0 --- /dev/null +++ b/src/server/gsheets/boxes.ts @@ -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("Boxes", new Box(), translationBox) + +export const detailedBoxListGet = expressAccessor.get(async (list) => { + const gameSheet = await getSheet("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 + }) +}) diff --git a/src/server/gsheets/games.ts b/src/server/gsheets/games.ts index 492b71f..785379b 100644 --- a/src/server/gsheets/games.ts +++ b/src/server/gsheets/games.ts @@ -8,6 +8,6 @@ const expressAccessor = new ExpressAccessors( ) 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() diff --git a/src/server/gsheets/localDb.ts b/src/server/gsheets/localDb.ts index c544234..d01c1b7 100644 --- a/src/server/gsheets/localDb.ts +++ b/src/server/gsheets/localDb.ts @@ -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" diff --git a/src/server/gsheets/volunteers.ts b/src/server/gsheets/volunteers.ts index 3e763ff..21e44ec 100644 --- a/src/server/gsheets/volunteers.ts +++ b/src/server/gsheets/volunteers.ts @@ -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(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, + } +}) diff --git a/src/server/index.ts b/src/server/index.ts index 66ba78c..f9721a5 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -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, diff --git a/src/services/boxes.ts b/src/services/boxes.ts new file mode 100644 index 0000000..7dad63c --- /dev/null +++ b/src/services/boxes.ts @@ -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 + +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 diff --git a/src/services/boxesAccessors.ts b/src/services/boxesAccessors.ts new file mode 100644 index 0000000..866aea9 --- /dev/null +++ b/src/services/boxesAccessors.ts @@ -0,0 +1,9 @@ +import ServiceAccessors from "./accessors" +import { elementName, Box, BoxWithoutId, DetailedBox } from "./boxes" + +const serviceAccessors = new ServiceAccessors(elementName) + +export const detailedBoxListGet = serviceAccessors.customGet<[], DetailedBox>("DetailedListGet") +// export const boxGet = serviceAccessors.get() +// export const boxAdd = serviceAccessors.add() +// export const boxSet = serviceAccessors.set() diff --git a/src/services/gamesAccessors.ts b/src/services/gamesAccessors.ts index c0ebe27..09e25c2 100644 --- a/src/services/gamesAccessors.ts +++ b/src/services/gamesAccessors.ts @@ -4,6 +4,6 @@ import { elementName, Game, GameWithoutId } from "./games" const serviceAccessors = new ServiceAccessors(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() diff --git a/src/services/volunteers.ts b/src/services/volunteers.ts index ccc7075..b91065c 100644 --- a/src/services/volunteers.ts +++ b/src/services/volunteers.ts @@ -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 +export interface VolunteerKnowledge { + id: Volunteer["id"] + ok: Volunteer["ok"] + bof: Volunteer["bof"] + niet: Volunteer["niet"] +} diff --git a/src/services/volunteersAccessors.ts b/src/services/volunteersAccessors.ts index a245968..7f99008 100644 --- a/src/services/volunteersAccessors.ts +++ b/src/services/volunteersAccessors.ts @@ -9,6 +9,7 @@ import { VolunteerTeamAssign, VolunteerWithoutId, VolunteerDiscordId, + VolunteerKnowledge, } from "./volunteers" const serviceAccessors = new ServiceAccessors(elementName) @@ -42,3 +43,6 @@ export const volunteerParticipationDetailsSet = export const volunteerTeamAssignSet = serviceAccessors.securedCustomPost<[number, Partial]>("TeamAssignSet") + +export const volunteerKnowledgeSet = + serviceAccessors.securedCustomPost<[number, Partial]>("KnowledgeSet") diff --git a/src/store/boxList.ts b/src/store/boxList.ts new file mode 100644 index 0000000..cad79bd --- /dev/null +++ b/src/store/boxList.ts @@ -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() + +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) => { + state.readyStatus = "success" + boxAdapter.setAll(state, payload) + }, + getFailure: (state, { payload }: PayloadAction) => { + 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 => 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" + ) +) diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 1ff416a..b935c4c 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -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, diff --git a/src/store/volunteerDayWishesSet.ts b/src/store/volunteerDayWishesSet.ts index ca8991e..7561830 100644 --- a/src/store/volunteerDayWishesSet.ts +++ b/src/store/volunteerDayWishesSet.ts @@ -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) => diff --git a/src/store/volunteerKnowledgeSet.ts b/src/store/volunteerKnowledgeSet.ts new file mode 100644 index 0000000..8c7fff9 --- /dev/null +++ b/src/store/volunteerKnowledgeSet.ts @@ -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) => ({ + readyStatus: "success", + entity: payload, + }), + getFailure: (_, { payload }: PayloadAction) => ({ + 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 = {}): 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] +} diff --git a/src/store/volunteerParticipationDetailsSet.ts b/src/store/volunteerParticipationDetailsSet.ts index 50f36d3..f02b286 100644 --- a/src/store/volunteerParticipationDetailsSet.ts +++ b/src/store/volunteerParticipationDetailsSet.ts @@ -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) => diff --git a/src/store/volunteerTeamAssignSet.ts b/src/store/volunteerTeamAssignSet.ts index dbd9884..34aa021 100644 --- a/src/store/volunteerTeamAssignSet.ts +++ b/src/store/volunteerTeamAssignSet.ts @@ -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) => diff --git a/src/store/volunteerTeamWishesSet.ts b/src/store/volunteerTeamWishesSet.ts index 21d17e3..8865d6f 100644 --- a/src/store/volunteerTeamWishesSet.ts +++ b/src/store/volunteerTeamWishesSet.ts @@ -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) => diff --git a/src/theme/variables.scss b/src/theme/variables.scss index ae9cf56..2135300 100755 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -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;