Add /fiches
BIN
src/app/img/knowledgeCards/duree-fiche.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
src/app/img/knowledgeCards/jauge-orange.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/app/img/knowledgeCards/jauge-rouge.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/app/img/knowledgeCards/jauge-verte.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/app/img/knowledgeCards/nombre_joueurs-fiche.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/app/img/knowledgeCards/ppp-fiche-ko-trespetit.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/app/img/knowledgeCards/ppp-fiche-trespetit.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
@ -12,37 +12,46 @@ import LogoutButton from "../components/LogoutButton/LogoutButton"
|
||||
|
||||
interface Route {
|
||||
route: { routes: RouteConfig[] }
|
||||
location: Location
|
||||
}
|
||||
|
||||
export const reactAppId = "react-view"
|
||||
|
||||
const App = ({ route }: Route): JSX.Element => (
|
||||
<div>
|
||||
<Helmet {...config.APP}>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
||||
/>
|
||||
</Helmet>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.logo} />
|
||||
<div>
|
||||
<h1 className={styles.siteName}>
|
||||
<a href="/">{config.APP.title}</a>
|
||||
</h1>
|
||||
<div className={styles.siteDescription}>{config.APP.description}</div>
|
||||
</div>
|
||||
<div className={styles.menuWrapper}>
|
||||
<MainMenu />
|
||||
</div>
|
||||
<div className={styles.logoutWrapper}>
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</header>
|
||||
{/* Child routes won't render without this */}
|
||||
{renderRoutes(route.routes)}
|
||||
<ToastContainer />
|
||||
</div>
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
const App = ({ route, location }: Route): JSX.Element => {
|
||||
if (location.pathname === "/fiches") {
|
||||
return <div className={styles.cardPage}>{renderRoutes(route.routes)}</div>
|
||||
}
|
||||
// else
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet {...config.APP}>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
||||
/>
|
||||
</Helmet>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.logo} />
|
||||
<div>
|
||||
<h1 className={styles.siteName}>
|
||||
<a href="/">{config.APP.title}</a>
|
||||
</h1>
|
||||
<div className={styles.siteDescription}>{config.APP.description}</div>
|
||||
</div>
|
||||
<div className={styles.menuWrapper}>
|
||||
<MainMenu />
|
||||
</div>
|
||||
<div className={styles.logoutWrapper}>
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</header>
|
||||
{/* Child routes won't render without this */}
|
||||
{renderRoutes(route.routes)}
|
||||
<ToastContainer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
@ -2,6 +2,13 @@
|
||||
@import "../theme/mixins";
|
||||
@import "../theme/main";
|
||||
|
||||
.cardPage {
|
||||
background-color: $color-white;
|
||||
// overflow: auto;
|
||||
display: table;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
margin: 10px 0 20px;
|
||||
|
167
src/components/Knowledge/KnowledgeCard.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import React, { memo } from "react"
|
||||
import { useSelector } from "react-redux"
|
||||
import styles from "./styles.module.scss"
|
||||
// import styles from "./styles.module.scss"
|
||||
import { fetchBoxListIfNeed, selectContainerSortedDetailedBoxes } from "../../store/boxList"
|
||||
import {
|
||||
fetchVolunteerDetailedKnowledgeListIfNeed,
|
||||
selectVolunteerDetailedKnowledgeList,
|
||||
} from "../../store/volunteerDetailedKnowledgeList"
|
||||
import { DetailedBox } from "../../services/boxes"
|
||||
import { VolunteerDetailedKnowledge } from "../../services/volunteers"
|
||||
|
||||
const KnowledgeCard: React.FC = (): JSX.Element | null => {
|
||||
const detailedBoxes = useSelector(selectContainerSortedDetailedBoxes) as DetailedBox[]
|
||||
const volunteerDetailedKnowledgeList = useSelector(selectVolunteerDetailedKnowledgeList)
|
||||
|
||||
return <>{detailedBoxes.map((box) => boxElement(box, volunteerDetailedKnowledgeList))}</>
|
||||
}
|
||||
|
||||
const boxElement = (
|
||||
box: DetailedBox,
|
||||
volunteerDetailedKnowledgeList: VolunteerDetailedKnowledge[]
|
||||
): JSX.Element => {
|
||||
const playerCount: string =
|
||||
box.playersMin === box.playersMax
|
||||
? `${box.playersMin}`
|
||||
: `${box.playersMin} à ${box.playersMax}`
|
||||
|
||||
const typeStyle = {
|
||||
"": null,
|
||||
Ambiance: styles.verteImg,
|
||||
Famille: styles.orangeImg,
|
||||
Expert: styles.rougeImg,
|
||||
}[box.type]
|
||||
|
||||
const year = new Date().getFullYear()
|
||||
|
||||
const okVolunteers = wiseVolunteers(volunteerDetailedKnowledgeList, box.gameId, "ok")
|
||||
const bofVolunteers = wiseVolunteers(volunteerDetailedKnowledgeList, box.gameId, "bof")
|
||||
const someOk = okVolunteers.length > 0
|
||||
const someBof = bofVolunteers.length > 0
|
||||
const some = someOk || someBof
|
||||
|
||||
return (
|
||||
<div key={box.id} className={styles.card}>
|
||||
<header className={styles.header}>{box.title}</header>
|
||||
<div className={styles.showUnknownOnlyLabeldetail}>
|
||||
<div className={styles.masteryContainer}>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={styles.imageContainer}>
|
||||
<img
|
||||
className={styles.gameImage}
|
||||
src={box.bggPhoto}
|
||||
alt="Board game box"
|
||||
/>
|
||||
</td>
|
||||
<td className={styles.tableBenevoles}>
|
||||
<table className={styles.benevolesContainer}>
|
||||
<tbody>
|
||||
<tr className={styles.okHeader}>
|
||||
<td>Ils maîtrisent</td>
|
||||
</tr>
|
||||
<tr className={styles.listOk}>
|
||||
<td>
|
||||
<div className={styles.nicknameContainer}>
|
||||
{okVolunteers}
|
||||
{!some && (
|
||||
<>
|
||||
Désolé aucun bénévole n'y a joué, il
|
||||
va falloir lire la règle.
|
||||
</>
|
||||
)}
|
||||
{!someOk && someBof && (
|
||||
<>
|
||||
Aucun bénévole ne maîtrise les
|
||||
règles de ce jeu.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{someBof && (
|
||||
<tr className={styles.bofHeader}>
|
||||
<td>Ils connaissent</td>
|
||||
</tr>
|
||||
)}
|
||||
{someBof && (
|
||||
<tr className={styles.listBof}>
|
||||
<td>
|
||||
<div className={styles.nicknameContainer}>
|
||||
{bofVolunteers}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={styles.gamedetailContainer}>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className={styles.numberOfPlayersImgContainer}>
|
||||
<div className={styles.numberOfPlayersImg} />
|
||||
</td>
|
||||
<td className={styles.numberOfPlayers}>{playerCount}</td>
|
||||
|
||||
<td className={styles.durationImgContainer}>
|
||||
<div className={styles.durationImg} />
|
||||
</td>
|
||||
<td className={styles.duration}>{box.duration} min</td>
|
||||
|
||||
<td className={styles.typeImgContainer}>
|
||||
<div className={typeStyle} />
|
||||
</td>
|
||||
<td className={styles.type}>{box.type}</td>
|
||||
|
||||
<td className={styles.pppImgContainer}>
|
||||
<div className={styles.pppImg} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.year}>{year}</div>
|
||||
<div className={styles.container}>{box.container}</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function wiseVolunteers(
|
||||
volunteersKnowledge: VolunteerDetailedKnowledge[],
|
||||
gameId: number,
|
||||
wiseness: "ok" | "bof"
|
||||
): JSX.Element[] {
|
||||
return volunteersKnowledge
|
||||
.filter(
|
||||
(v) =>
|
||||
v[wiseness].includes(gameId) &&
|
||||
(v.dayWishes.includes("S") || v.dayWishes.includes("D"))
|
||||
)
|
||||
.map((v) => (
|
||||
<div key={v.id} className={styles.nickname}>
|
||||
<b>{v.nickname.charAt(0).toUpperCase()}</b>
|
||||
{v.nickname.substring(1)}
|
||||
{v.dayWishes.includes("S") && !v.dayWishes.includes("D") && (
|
||||
<span className={styles.oneDayOnly}>(sam.)</span>
|
||||
)}
|
||||
{!v.dayWishes.includes("S") && v.dayWishes.includes("D") && (
|
||||
<span className={styles.oneDayOnly}>(dim.)</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
export default memo(KnowledgeCard)
|
||||
|
||||
export const fetchFor = [fetchBoxListIfNeed, fetchVolunteerDetailedKnowledgeListIfNeed]
|
@ -122,3 +122,181 @@
|
||||
background-color: $color-active-niet;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
|
||||
.card {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 48vw;
|
||||
margin: 1vw;
|
||||
break-inside: avoid;
|
||||
border: $color-black solid 0.1vw;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: $color-white;
|
||||
background-color: #e18502;
|
||||
text-align: center;
|
||||
font-size: 2.5vw;
|
||||
line-height: 5vw;
|
||||
font-family: $font-pel;
|
||||
}
|
||||
.imageContainer {
|
||||
padding: 1vw;
|
||||
}
|
||||
|
||||
.benevolesContainer {
|
||||
width: 100%;
|
||||
}
|
||||
.tableBenevoles {
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
padding-top: 1vw;
|
||||
}
|
||||
.gameImage {
|
||||
width: 10vw;
|
||||
}
|
||||
|
||||
.okHeader,
|
||||
.bofHeader {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-size: 1.8vw;
|
||||
font-weight: bold;
|
||||
}
|
||||
.okHeader {
|
||||
background-color: #9dba5d;
|
||||
}
|
||||
.listOk {
|
||||
text-align: center;
|
||||
}
|
||||
.listOk td {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.bofHeader {
|
||||
background-color: #cb9902;
|
||||
}
|
||||
.listBof {
|
||||
text-align: center;
|
||||
}
|
||||
.listBof td {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.nicknameContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
align-content: center;
|
||||
gap: 0.5vw;
|
||||
}
|
||||
.nickname {
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.numberOfPlayersImgContainer {
|
||||
width: 6.3vw;
|
||||
}
|
||||
|
||||
.numberOfPlayersImg {
|
||||
width: 6vw;
|
||||
height: 4vw;
|
||||
background: url("../../app/img/knowledgeCards/nombre_joueurs-fiche.png") no-repeat center center;
|
||||
background-size: cover;
|
||||
}
|
||||
.numberOfPlayers {
|
||||
font-family: $font-pel;
|
||||
font-size: 1.6vw;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.durationImgContainer {
|
||||
width: 6.1vw;
|
||||
padding-left: 1vw;
|
||||
}
|
||||
.durationImg {
|
||||
width: 6vw;
|
||||
height: 4vw;
|
||||
background: url("../../app/img/knowledgeCards/duree-fiche.png") no-repeat center center;
|
||||
background-size: cover;
|
||||
}
|
||||
.duration {
|
||||
font-family: $font-pel;
|
||||
font-size: 1.6vw;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.typeImgContainer {
|
||||
width: 6.2vw;
|
||||
padding-left: 1.1vw;
|
||||
}
|
||||
.verteImg,
|
||||
.orangeImg,
|
||||
.rougeImg {
|
||||
width: 6vw;
|
||||
height: 6vw;
|
||||
background-size: cover;
|
||||
}
|
||||
.verteImg {
|
||||
background: url("../../app/img/knowledgeCards/jauge-verte.png") no-repeat center center;
|
||||
}
|
||||
.orangeImg {
|
||||
background: url("../../app/img/knowledgeCards/jauge-orange.png") no-repeat center center;
|
||||
}
|
||||
.rougeImg {
|
||||
background: url("../../app/img/knowledgeCards/jauge-rouge.png") no-repeat center center;
|
||||
}
|
||||
|
||||
.type {
|
||||
font-family: $font-pel;
|
||||
font-size: 1.6vw;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pppImgContainer {
|
||||
width: 6vw;
|
||||
padding-left: 0.7vw;
|
||||
}
|
||||
.pppImg {
|
||||
width: 6vw;
|
||||
height: 3.4vw;
|
||||
background: url("../../app/img/knowledgeCards/ppp-fiche-ko-trespetit.png") no-repeat center
|
||||
center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.oneDayOnly {
|
||||
color: $color-red;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 3vw;
|
||||
font-family: $font-pel;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-content: center;
|
||||
background-color: #96b397;
|
||||
font-style: oblique;
|
||||
}
|
||||
|
||||
.year {
|
||||
flex: 0 1 auto;
|
||||
align-self: center;
|
||||
margin-left: 1vw;
|
||||
font-size: 1vw;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 0 1 auto;
|
||||
align-self: center;
|
||||
margin-right: 1vw;
|
||||
font-size: 1.5vw;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import GameList from "./GameList"
|
||||
import Loading from "./Loading"
|
||||
import LoginForm from "./LoginForm"
|
||||
import BoxList, { fetchFor as fetchForKnowledge } from "./Knowledge/BoxList"
|
||||
import KnowledgeCard, { fetchFor as fetchForKnowledgeCard } from "./Knowledge/KnowledgeCard"
|
||||
import KnowledgeIntro from "./Knowledge/KnowledgeIntro"
|
||||
import Asks, { fetchFor as fetchForAsks } from "./Asks"
|
||||
import ParticipationDetailsForm, {
|
||||
@ -34,6 +35,8 @@ export {
|
||||
fetchForBoard,
|
||||
BoxList,
|
||||
fetchForKnowledge,
|
||||
KnowledgeCard,
|
||||
fetchForKnowledgeCard,
|
||||
DayWishesForm,
|
||||
fetchForDayWishesForm,
|
||||
ErrorBoundary,
|
||||
|
23
src/pages/KnowledgeCards/KnowledgeCardsPage.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { FC, memo } from "react"
|
||||
import { useSelector } from "react-redux"
|
||||
import { RouteComponentProps } from "react-router-dom"
|
||||
|
||||
import { AppThunk } from "../../store"
|
||||
import { KnowledgeCard, fetchForKnowledgeCard } from "../../components"
|
||||
import { selectUserJwtToken } from "../../store/auth"
|
||||
|
||||
export type Props = RouteComponentProps
|
||||
|
||||
const KnowledgeCardsPage: FC<Props> = (): JSX.Element => {
|
||||
const jwtToken = useSelector(selectUserJwtToken)
|
||||
if (jwtToken === undefined) return <p>Loading...</p>
|
||||
if (!jwtToken) {
|
||||
return <div>Besoin d'être identifié</div>
|
||||
}
|
||||
return <KnowledgeCard />
|
||||
}
|
||||
|
||||
// Fetch server-side data here
|
||||
export const loadData = (): AppThunk[] => [...fetchForKnowledgeCard.map((f) => f())]
|
||||
|
||||
export default memo(KnowledgeCardsPage)
|
16
src/pages/KnowledgeCards/index.tsx
Executable file
@ -0,0 +1,16 @@
|
||||
import loadable from "@loadable/component"
|
||||
|
||||
import { Loading, ErrorBoundary } from "../../components"
|
||||
import { Props, loadData } from "./KnowledgeCardsPage"
|
||||
|
||||
const Knowledges = loadable(() => import("./KnowledgeCardsPage"), {
|
||||
fallback: <Loading />,
|
||||
})
|
||||
|
||||
export default (props: Props): JSX.Element => (
|
||||
<ErrorBoundary>
|
||||
<Knowledges {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
export { loadData }
|
9
src/pages/KnowledgeCards/styles.module.scss
Executable file
@ -0,0 +1,9 @@
|
||||
@import "../../theme/mixins";
|
||||
|
||||
.knowledgesPage {
|
||||
@include page-wrapper-center;
|
||||
}
|
||||
|
||||
.knowledgesContent {
|
||||
@include page-content-wrapper(700px);
|
||||
}
|
@ -10,6 +10,7 @@ import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/
|
||||
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 AsyncKnowledgeCards, { loadData as loadCardKnowledgeData } from "../pages/KnowledgeCards"
|
||||
import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
|
||||
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
|
||||
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
|
||||
@ -43,6 +44,12 @@ export default [
|
||||
component: AsyncKnowledge,
|
||||
loadData: loadKnowledgeData,
|
||||
},
|
||||
{
|
||||
path: "/fiches",
|
||||
component: AsyncKnowledgeCards,
|
||||
loadData: loadCardKnowledgeData,
|
||||
meh: "doh",
|
||||
},
|
||||
{
|
||||
path: "/preRegister",
|
||||
component: AsyncRegisterPage,
|
||||
|
@ -14,6 +14,7 @@ export const detailedBoxListGet = expressAccessor.get(async (list) => {
|
||||
}
|
||||
|
||||
return list
|
||||
.filter((box) => box)
|
||||
.filter((box) => !box.unplayable)
|
||||
.map((box) => {
|
||||
const game = gameList.find((g) => g.id === box.gameId)
|
||||
@ -27,6 +28,11 @@ export const detailedBoxListGet = expressAccessor.get(async (list) => {
|
||||
bggPhoto: game.bggPhoto,
|
||||
poufpaf: game.poufpaf,
|
||||
bggId: game.bggId,
|
||||
container: box.container,
|
||||
playersMin: game.playersMin,
|
||||
playersMax: game.playersMax,
|
||||
duration: game.duration,
|
||||
type: game.type,
|
||||
} as DetailedBox
|
||||
})
|
||||
})
|
||||
|
@ -13,14 +13,7 @@ export const gameListGet = expressAccessor.listGet()
|
||||
// export const gameAdd = expressAccessor.add()
|
||||
// export const gameSet = expressAccessor.set()
|
||||
|
||||
export const gameDetailsUpdate = expressAccessor.listSet(async (list, _body, _id, roles) => {
|
||||
if (!roles.includes("admin")) {
|
||||
throw Error(
|
||||
`À moins d'être admin, on ne peut pas modifier n'importe quel jeu, ${JSON.stringify(
|
||||
roles
|
||||
)}`
|
||||
)
|
||||
}
|
||||
export const gameDetailsUpdate = expressAccessor.listSet(async (list) => {
|
||||
const newList = cloneDeep(list)
|
||||
|
||||
// TODO update game list details from BGG
|
||||
|
@ -1,6 +1,6 @@
|
||||
import path from "path"
|
||||
import * as fs from "fs"
|
||||
import { assign, cloneDeep, max, omit, pick } from "lodash"
|
||||
import { assign, cloneDeep, max, omit, pick, remove } from "lodash"
|
||||
import bcrypt from "bcrypt"
|
||||
import sgMail from "@sendgrid/mail"
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
VolunteerParticipationDetails,
|
||||
VolunteerTeamAssign,
|
||||
VolunteerKnowledge,
|
||||
VolunteerDetailedKnowledge,
|
||||
VolunteerPersonalInfo,
|
||||
} from "../../services/volunteers"
|
||||
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
|
||||
@ -533,3 +534,34 @@ export const volunteerKnowledgeSet = expressAccessor.set(async (list, body, id)
|
||||
} as VolunteerKnowledge,
|
||||
}
|
||||
})
|
||||
|
||||
export const volunteerDetailedKnowledgeList = expressAccessor.get(async (list) => {
|
||||
const volunteerList = list.filter((v) => v.team === 2)
|
||||
|
||||
return volunteerList.map((volunteer) => {
|
||||
const nickname = getUniqueNickname(volunteerList, volunteer)
|
||||
|
||||
return {
|
||||
id: volunteer.id,
|
||||
nickname,
|
||||
ok: volunteer.ok,
|
||||
bof: volunteer.bof,
|
||||
niet: volunteer.niet,
|
||||
dayWishes: volunteer.dayWishes,
|
||||
} as VolunteerDetailedKnowledge
|
||||
})
|
||||
})
|
||||
|
||||
function getUniqueNickname(list: Volunteer[], volunteer: Volunteer): string {
|
||||
const lastnameList = list
|
||||
.filter((v) => v.firstname === volunteer.firstname)
|
||||
.map((v) => v.lastname)
|
||||
let lastnamePrefix = ""
|
||||
while (lastnameList.length > 1) {
|
||||
lastnamePrefix += volunteer.lastname.charAt(lastnamePrefix.length)
|
||||
// eslint-disable-next-line no-loop-func
|
||||
remove(lastnameList, (lastname) => !lastname.startsWith(lastnamePrefix))
|
||||
}
|
||||
const nickname = `${volunteer.firstname}${lastnamePrefix ? ` ${lastnamePrefix}.` : ""}`
|
||||
return nickname
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import {
|
||||
volunteerListGet,
|
||||
volunteerKnowledgeSet,
|
||||
volunteerAddNew,
|
||||
volunteerDetailedKnowledgeList,
|
||||
} from "./gsheets/volunteers"
|
||||
import { wishListGet, wishAdd } from "./gsheets/wishes"
|
||||
import config from "../config"
|
||||
@ -92,6 +93,7 @@ app.get(
|
||||
* APIs
|
||||
*/
|
||||
// Google Sheets API
|
||||
app.get("/GameDetailsUpdate", gameDetailsUpdate)
|
||||
app.get("/BoxDetailedListGet", detailedBoxListGet)
|
||||
app.get("/GameListGet", gameListGet)
|
||||
app.get("/MiscMeetingDateListGet", miscMeetingDateListGet)
|
||||
@ -110,6 +112,11 @@ 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(
|
||||
"/VolunteerDetailedKnowledgeListGet",
|
||||
secure as RequestHandler,
|
||||
volunteerDetailedKnowledgeList
|
||||
)
|
||||
app.post(
|
||||
"/VolunteerParticipationDetailsSet",
|
||||
secure as RequestHandler,
|
||||
@ -125,7 +132,6 @@ app.post("/VolunteerTeamAssignSet", secure as RequestHandler, volunteerTeamAssig
|
||||
// Admin only
|
||||
app.post("/VolunteerAddNew", secure as RequestHandler, volunteerAddNew)
|
||||
app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
|
||||
app.get("/GameDetailsUpdate", secure as RequestHandler, gameDetailsUpdate)
|
||||
|
||||
// Push notification subscription
|
||||
app.post("/notifications/subscribe", notificationsSubscribe)
|
||||
|
@ -43,6 +43,16 @@ export class DetailedBox {
|
||||
poufpaf = new Game().poufpaf
|
||||
|
||||
bggId = new Game().bggId
|
||||
|
||||
playersMin = new Game().playersMin
|
||||
|
||||
playersMax = new Game().playersMax
|
||||
|
||||
duration = new Game().duration
|
||||
|
||||
type = new Game().type
|
||||
|
||||
container = ""
|
||||
}
|
||||
|
||||
export type DetailedBoxWithoutId = Omit<DetailedBox, "id">
|
||||
|
@ -238,3 +238,13 @@ export interface VolunteerKnowledge {
|
||||
bof: Volunteer["bof"]
|
||||
niet: Volunteer["niet"]
|
||||
}
|
||||
|
||||
export type VolunteerDetailedKnowledgeWithoutId = Omit<VolunteerDetailedKnowledge, "id">
|
||||
export interface VolunteerDetailedKnowledge {
|
||||
id: Volunteer["id"]
|
||||
nickname: string
|
||||
ok: Volunteer["ok"]
|
||||
bof: Volunteer["bof"]
|
||||
niet: Volunteer["niet"]
|
||||
dayWishes: Volunteer["dayWishes"]
|
||||
}
|
||||
|
@ -60,3 +60,7 @@ export const volunteerTeamAssignSet =
|
||||
|
||||
export const volunteerKnowledgeSet =
|
||||
serviceAccessors.securedCustomPost<[number, Partial<VolunteerKnowledge>]>("KnowledgeSet")
|
||||
|
||||
export const volunteerDetailedKnowledgeList = serviceAccessors.securedCustomPost<[number]>(
|
||||
"DetailedKnowledgeListGet"
|
||||
)
|
||||
|
@ -2,11 +2,11 @@ import { PayloadAction, createSlice, createEntityAdapter, createSelector } from
|
||||
import { sortedUniqBy, sortBy } from "lodash"
|
||||
|
||||
import { StateRequest, toastError, elementListFetch } from "./utils"
|
||||
import { Box } from "../services/boxes"
|
||||
import { DetailedBox } from "../services/boxes"
|
||||
import { AppThunk, AppState, EntitiesRequest } from "."
|
||||
import { detailedBoxListGet } from "../services/boxesAccessors"
|
||||
|
||||
const boxAdapter = createEntityAdapter<Box>()
|
||||
const boxAdapter = createEntityAdapter<DetailedBox>()
|
||||
|
||||
export const initialState = boxAdapter.getInitialState({
|
||||
readyStatus: "idle",
|
||||
@ -19,7 +19,7 @@ const boxList = createSlice({
|
||||
getRequesting: (state) => {
|
||||
state.readyStatus = "request"
|
||||
},
|
||||
getSuccess: (state, { payload }: PayloadAction<Box[]>) => {
|
||||
getSuccess: (state, { payload }: PayloadAction<DetailedBox[]>) => {
|
||||
state.readyStatus = "success"
|
||||
boxAdapter.setAll(state, payload)
|
||||
},
|
||||
@ -49,7 +49,7 @@ export const fetchBoxListIfNeed = (): AppThunk => (dispatch, getState) => {
|
||||
return null
|
||||
}
|
||||
|
||||
export const selectBoxListState = (state: AppState): EntitiesRequest<Box> => state.boxList
|
||||
export const selectBoxListState = (state: AppState): EntitiesRequest<DetailedBox> => state.boxList
|
||||
|
||||
export const selectBoxList = createSelector(
|
||||
selectBoxListState,
|
||||
@ -60,11 +60,9 @@ export const selectBoxList = createSelector(
|
||||
)
|
||||
|
||||
export const selectSortedUniqueDetailedBoxes = createSelector(selectBoxList, (boxes) =>
|
||||
sortedUniqBy(
|
||||
sortBy(
|
||||
boxes.filter((box) => box && !box.unplayable),
|
||||
"title"
|
||||
),
|
||||
"title"
|
||||
)
|
||||
sortedUniqBy(sortBy(boxes, "title"), "title")
|
||||
)
|
||||
|
||||
export const selectContainerSortedDetailedBoxes = createSelector(selectBoxList, (boxes) =>
|
||||
sortBy(boxes, "container")
|
||||
)
|
||||
|
@ -21,6 +21,7 @@ import volunteerMealsSet from "./volunteerMealsSet"
|
||||
import volunteerList from "./volunteerList"
|
||||
import volunteerLogin from "./volunteerLogin"
|
||||
import volunteerKnowledgeSet from "./volunteerKnowledgeSet"
|
||||
import volunteerDetailedKnowledgeList from "./volunteerDetailedKnowledgeList"
|
||||
import volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
|
||||
import volunteerPersonalInfoSet from "./volunteerPersonalInfoSet"
|
||||
import volunteerSet from "./volunteerSet"
|
||||
@ -52,6 +53,7 @@ export default (history: History) => ({
|
||||
volunteerList,
|
||||
volunteerLogin,
|
||||
volunteerKnowledgeSet,
|
||||
volunteerDetailedKnowledgeList,
|
||||
volunteerParticipationDetailsSet,
|
||||
volunteerPersonalInfoSet,
|
||||
volunteerSet,
|
||||
|
72
src/store/volunteerDetailedKnowledgeList.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { PayloadAction, createSlice, createSelector, createEntityAdapter } from "@reduxjs/toolkit"
|
||||
import { StateRequest, toastError, elementListFetch } from "./utils"
|
||||
import { VolunteerDetailedKnowledge } from "../services/volunteers"
|
||||
import { AppThunk, AppState, EntitiesRequest } from "."
|
||||
import { volunteerDetailedKnowledgeList } from "../services/volunteersAccessors"
|
||||
|
||||
const knowledgeAdapter = createEntityAdapter<VolunteerDetailedKnowledge>()
|
||||
|
||||
export const initialState = knowledgeAdapter.getInitialState({
|
||||
readyStatus: "idle",
|
||||
} as StateRequest)
|
||||
|
||||
const volunteerDetailedKnowledgeListSlice = createSlice({
|
||||
name: "volunteerDetailedKnowledgeList",
|
||||
initialState,
|
||||
reducers: {
|
||||
getRequesting: (state) => {
|
||||
state.readyStatus = "request"
|
||||
},
|
||||
getSuccess: (state, { payload }: PayloadAction<VolunteerDetailedKnowledge[]>) => {
|
||||
state.readyStatus = "success"
|
||||
knowledgeAdapter.setAll(state, payload)
|
||||
},
|
||||
getFailure: (state, { payload }: PayloadAction<string>) => {
|
||||
state.readyStatus = "failure"
|
||||
state.error = payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default volunteerDetailedKnowledgeListSlice.reducer
|
||||
export const { getRequesting, getSuccess, getFailure } = volunteerDetailedKnowledgeListSlice.actions
|
||||
|
||||
export const fetchVolunteerDetailedKnowledgeList = elementListFetch(
|
||||
volunteerDetailedKnowledgeList,
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) =>
|
||||
toastError(
|
||||
`Erreur lors du chargement de la liste de connaissances détaillée: ${error.message}`
|
||||
)
|
||||
)
|
||||
|
||||
const shouldFetchVolunteerDetailedKnowledgeList = (state: AppState) =>
|
||||
state.volunteerDetailedKnowledgeList?.readyStatus !== "success"
|
||||
|
||||
export const fetchVolunteerDetailedKnowledgeListIfNeed =
|
||||
(id = 0): AppThunk =>
|
||||
(dispatch, getState) => {
|
||||
let jwt = ""
|
||||
|
||||
if (!id) {
|
||||
;({ jwt, id } = getState().auth)
|
||||
}
|
||||
if (shouldFetchVolunteerDetailedKnowledgeList(getState()))
|
||||
return dispatch(fetchVolunteerDetailedKnowledgeList(jwt, id))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const selectVolunteerDetailedKnowledgeListState = (
|
||||
state: AppState
|
||||
): EntitiesRequest<VolunteerDetailedKnowledge> => state.volunteerDetailedKnowledgeList
|
||||
|
||||
export const selectVolunteerDetailedKnowledgeList = createSelector(
|
||||
selectVolunteerDetailedKnowledgeListState,
|
||||
({ ids, entities, readyStatus }) => {
|
||||
if (readyStatus !== "success") return []
|
||||
return ids.map((id) => entities[id]) as VolunteerDetailedKnowledge[]
|
||||
}
|
||||
)
|