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 {
|
interface Route {
|
||||||
route: { routes: RouteConfig[] }
|
route: { routes: RouteConfig[] }
|
||||||
|
location: Location
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reactAppId = "react-view"
|
export const reactAppId = "react-view"
|
||||||
|
|
||||||
const App = ({ route }: Route): JSX.Element => (
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
<div>
|
const App = ({ route, location }: Route): JSX.Element => {
|
||||||
<Helmet {...config.APP}>
|
if (location.pathname === "/fiches") {
|
||||||
<meta
|
return <div className={styles.cardPage}>{renderRoutes(route.routes)}</div>
|
||||||
name="viewport"
|
}
|
||||||
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
// else
|
||||||
/>
|
|
||||||
</Helmet>
|
return (
|
||||||
<header className={styles.header}>
|
<div>
|
||||||
<div className={styles.logo} />
|
<Helmet {...config.APP}>
|
||||||
<div>
|
<meta
|
||||||
<h1 className={styles.siteName}>
|
name="viewport"
|
||||||
<a href="/">{config.APP.title}</a>
|
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
||||||
</h1>
|
/>
|
||||||
<div className={styles.siteDescription}>{config.APP.description}</div>
|
</Helmet>
|
||||||
</div>
|
<header className={styles.header}>
|
||||||
<div className={styles.menuWrapper}>
|
<div className={styles.logo} />
|
||||||
<MainMenu />
|
<div>
|
||||||
</div>
|
<h1 className={styles.siteName}>
|
||||||
<div className={styles.logoutWrapper}>
|
<a href="/">{config.APP.title}</a>
|
||||||
<LogoutButton />
|
</h1>
|
||||||
</div>
|
<div className={styles.siteDescription}>{config.APP.description}</div>
|
||||||
</header>
|
</div>
|
||||||
{/* Child routes won't render without this */}
|
<div className={styles.menuWrapper}>
|
||||||
{renderRoutes(route.routes)}
|
<MainMenu />
|
||||||
<ToastContainer />
|
</div>
|
||||||
</div>
|
<div className={styles.logoutWrapper}>
|
||||||
)
|
<LogoutButton />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{/* Child routes won't render without this */}
|
||||||
|
{renderRoutes(route.routes)}
|
||||||
|
<ToastContainer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
@ -2,6 +2,13 @@
|
|||||||
@import "../theme/mixins";
|
@import "../theme/mixins";
|
||||||
@import "../theme/main";
|
@import "../theme/main";
|
||||||
|
|
||||||
|
.cardPage {
|
||||||
|
background-color: $color-white;
|
||||||
|
// overflow: auto;
|
||||||
|
display: table;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 10px 0 20px;
|
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;
|
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 Loading from "./Loading"
|
||||||
import LoginForm from "./LoginForm"
|
import LoginForm from "./LoginForm"
|
||||||
import BoxList, { fetchFor as fetchForKnowledge } from "./Knowledge/BoxList"
|
import BoxList, { fetchFor as fetchForKnowledge } from "./Knowledge/BoxList"
|
||||||
|
import KnowledgeCard, { fetchFor as fetchForKnowledgeCard } from "./Knowledge/KnowledgeCard"
|
||||||
import KnowledgeIntro from "./Knowledge/KnowledgeIntro"
|
import KnowledgeIntro from "./Knowledge/KnowledgeIntro"
|
||||||
import Asks, { fetchFor as fetchForAsks } from "./Asks"
|
import Asks, { fetchFor as fetchForAsks } from "./Asks"
|
||||||
import ParticipationDetailsForm, {
|
import ParticipationDetailsForm, {
|
||||||
@ -34,6 +35,8 @@ export {
|
|||||||
fetchForBoard,
|
fetchForBoard,
|
||||||
BoxList,
|
BoxList,
|
||||||
fetchForKnowledge,
|
fetchForKnowledge,
|
||||||
|
KnowledgeCard,
|
||||||
|
fetchForKnowledgeCard,
|
||||||
DayWishesForm,
|
DayWishesForm,
|
||||||
fetchForDayWishesForm,
|
fetchForDayWishesForm,
|
||||||
ErrorBoundary,
|
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 AsyncTeamAssignment, { loadData as loadTeamAssignmentData } from "../pages/TeamAssignment"
|
||||||
import AsyncRegisterPage, { loadData as loadRegisterPage } from "../pages/Register"
|
import AsyncRegisterPage, { loadData as loadRegisterPage } from "../pages/Register"
|
||||||
import AsyncKnowledge, { loadData as loadKnowledgeData } from "../pages/Knowledge"
|
import AsyncKnowledge, { loadData as loadKnowledgeData } from "../pages/Knowledge"
|
||||||
|
import AsyncKnowledgeCards, { loadData as loadCardKnowledgeData } from "../pages/KnowledgeCards"
|
||||||
import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
|
import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
|
||||||
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
|
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
|
||||||
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
|
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
|
||||||
@ -43,6 +44,12 @@ export default [
|
|||||||
component: AsyncKnowledge,
|
component: AsyncKnowledge,
|
||||||
loadData: loadKnowledgeData,
|
loadData: loadKnowledgeData,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/fiches",
|
||||||
|
component: AsyncKnowledgeCards,
|
||||||
|
loadData: loadCardKnowledgeData,
|
||||||
|
meh: "doh",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/preRegister",
|
path: "/preRegister",
|
||||||
component: AsyncRegisterPage,
|
component: AsyncRegisterPage,
|
||||||
|
@ -14,6 +14,7 @@ export const detailedBoxListGet = expressAccessor.get(async (list) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return list
|
return list
|
||||||
|
.filter((box) => box)
|
||||||
.filter((box) => !box.unplayable)
|
.filter((box) => !box.unplayable)
|
||||||
.map((box) => {
|
.map((box) => {
|
||||||
const game = gameList.find((g) => g.id === box.gameId)
|
const game = gameList.find((g) => g.id === box.gameId)
|
||||||
@ -27,6 +28,11 @@ export const detailedBoxListGet = expressAccessor.get(async (list) => {
|
|||||||
bggPhoto: game.bggPhoto,
|
bggPhoto: game.bggPhoto,
|
||||||
poufpaf: game.poufpaf,
|
poufpaf: game.poufpaf,
|
||||||
bggId: game.bggId,
|
bggId: game.bggId,
|
||||||
|
container: box.container,
|
||||||
|
playersMin: game.playersMin,
|
||||||
|
playersMax: game.playersMax,
|
||||||
|
duration: game.duration,
|
||||||
|
type: game.type,
|
||||||
} as DetailedBox
|
} as DetailedBox
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -13,14 +13,7 @@ export const gameListGet = expressAccessor.listGet()
|
|||||||
// export const gameAdd = expressAccessor.add()
|
// export const gameAdd = expressAccessor.add()
|
||||||
// export const gameSet = expressAccessor.set()
|
// export const gameSet = expressAccessor.set()
|
||||||
|
|
||||||
export const gameDetailsUpdate = expressAccessor.listSet(async (list, _body, _id, roles) => {
|
export const gameDetailsUpdate = expressAccessor.listSet(async (list) => {
|
||||||
if (!roles.includes("admin")) {
|
|
||||||
throw Error(
|
|
||||||
`À moins d'être admin, on ne peut pas modifier n'importe quel jeu, ${JSON.stringify(
|
|
||||||
roles
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const newList = cloneDeep(list)
|
const newList = cloneDeep(list)
|
||||||
|
|
||||||
// TODO update game list details from BGG
|
// TODO update game list details from BGG
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import * as fs from "fs"
|
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 bcrypt from "bcrypt"
|
||||||
import sgMail from "@sendgrid/mail"
|
import sgMail from "@sendgrid/mail"
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ import {
|
|||||||
VolunteerParticipationDetails,
|
VolunteerParticipationDetails,
|
||||||
VolunteerTeamAssign,
|
VolunteerTeamAssign,
|
||||||
VolunteerKnowledge,
|
VolunteerKnowledge,
|
||||||
|
VolunteerDetailedKnowledge,
|
||||||
VolunteerPersonalInfo,
|
VolunteerPersonalInfo,
|
||||||
} from "../../services/volunteers"
|
} from "../../services/volunteers"
|
||||||
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
|
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
|
||||||
@ -533,3 +534,34 @@ export const volunteerKnowledgeSet = expressAccessor.set(async (list, body, id)
|
|||||||
} as VolunteerKnowledge,
|
} 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,
|
volunteerListGet,
|
||||||
volunteerKnowledgeSet,
|
volunteerKnowledgeSet,
|
||||||
volunteerAddNew,
|
volunteerAddNew,
|
||||||
|
volunteerDetailedKnowledgeList,
|
||||||
} from "./gsheets/volunteers"
|
} from "./gsheets/volunteers"
|
||||||
import { wishListGet, wishAdd } from "./gsheets/wishes"
|
import { wishListGet, wishAdd } from "./gsheets/wishes"
|
||||||
import config from "../config"
|
import config from "../config"
|
||||||
@ -92,6 +93,7 @@ app.get(
|
|||||||
* APIs
|
* APIs
|
||||||
*/
|
*/
|
||||||
// Google Sheets API
|
// Google Sheets API
|
||||||
|
app.get("/GameDetailsUpdate", gameDetailsUpdate)
|
||||||
app.get("/BoxDetailedListGet", detailedBoxListGet)
|
app.get("/BoxDetailedListGet", detailedBoxListGet)
|
||||||
app.get("/GameListGet", gameListGet)
|
app.get("/GameListGet", gameListGet)
|
||||||
app.get("/MiscMeetingDateListGet", miscMeetingDateListGet)
|
app.get("/MiscMeetingDateListGet", miscMeetingDateListGet)
|
||||||
@ -110,6 +112,11 @@ app.get("/TeamListGet", teamListGet)
|
|||||||
app.get("/VolunteerDiscordId", secure as RequestHandler, volunteerDiscordId)
|
app.get("/VolunteerDiscordId", secure as RequestHandler, volunteerDiscordId)
|
||||||
app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet)
|
app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet)
|
||||||
app.post("/VolunteerKnowledgeSet", secure as RequestHandler, volunteerKnowledgeSet)
|
app.post("/VolunteerKnowledgeSet", secure as RequestHandler, volunteerKnowledgeSet)
|
||||||
|
app.post(
|
||||||
|
"/VolunteerDetailedKnowledgeListGet",
|
||||||
|
secure as RequestHandler,
|
||||||
|
volunteerDetailedKnowledgeList
|
||||||
|
)
|
||||||
app.post(
|
app.post(
|
||||||
"/VolunteerParticipationDetailsSet",
|
"/VolunteerParticipationDetailsSet",
|
||||||
secure as RequestHandler,
|
secure as RequestHandler,
|
||||||
@ -125,7 +132,6 @@ app.post("/VolunteerTeamAssignSet", secure as RequestHandler, volunteerTeamAssig
|
|||||||
// Admin only
|
// Admin only
|
||||||
app.post("/VolunteerAddNew", secure as RequestHandler, volunteerAddNew)
|
app.post("/VolunteerAddNew", secure as RequestHandler, volunteerAddNew)
|
||||||
app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
|
app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
|
||||||
app.get("/GameDetailsUpdate", secure as RequestHandler, gameDetailsUpdate)
|
|
||||||
|
|
||||||
// Push notification subscription
|
// Push notification subscription
|
||||||
app.post("/notifications/subscribe", notificationsSubscribe)
|
app.post("/notifications/subscribe", notificationsSubscribe)
|
||||||
|
@ -43,6 +43,16 @@ export class DetailedBox {
|
|||||||
poufpaf = new Game().poufpaf
|
poufpaf = new Game().poufpaf
|
||||||
|
|
||||||
bggId = new Game().bggId
|
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">
|
export type DetailedBoxWithoutId = Omit<DetailedBox, "id">
|
||||||
|
@ -238,3 +238,13 @@ export interface VolunteerKnowledge {
|
|||||||
bof: Volunteer["bof"]
|
bof: Volunteer["bof"]
|
||||||
niet: Volunteer["niet"]
|
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 =
|
export const volunteerKnowledgeSet =
|
||||||
serviceAccessors.securedCustomPost<[number, Partial<VolunteerKnowledge>]>("KnowledgeSet")
|
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 { sortedUniqBy, sortBy } from "lodash"
|
||||||
|
|
||||||
import { StateRequest, toastError, elementListFetch } from "./utils"
|
import { StateRequest, toastError, elementListFetch } from "./utils"
|
||||||
import { Box } from "../services/boxes"
|
import { DetailedBox } from "../services/boxes"
|
||||||
import { AppThunk, AppState, EntitiesRequest } from "."
|
import { AppThunk, AppState, EntitiesRequest } from "."
|
||||||
import { detailedBoxListGet } from "../services/boxesAccessors"
|
import { detailedBoxListGet } from "../services/boxesAccessors"
|
||||||
|
|
||||||
const boxAdapter = createEntityAdapter<Box>()
|
const boxAdapter = createEntityAdapter<DetailedBox>()
|
||||||
|
|
||||||
export const initialState = boxAdapter.getInitialState({
|
export const initialState = boxAdapter.getInitialState({
|
||||||
readyStatus: "idle",
|
readyStatus: "idle",
|
||||||
@ -19,7 +19,7 @@ const boxList = createSlice({
|
|||||||
getRequesting: (state) => {
|
getRequesting: (state) => {
|
||||||
state.readyStatus = "request"
|
state.readyStatus = "request"
|
||||||
},
|
},
|
||||||
getSuccess: (state, { payload }: PayloadAction<Box[]>) => {
|
getSuccess: (state, { payload }: PayloadAction<DetailedBox[]>) => {
|
||||||
state.readyStatus = "success"
|
state.readyStatus = "success"
|
||||||
boxAdapter.setAll(state, payload)
|
boxAdapter.setAll(state, payload)
|
||||||
},
|
},
|
||||||
@ -49,7 +49,7 @@ export const fetchBoxListIfNeed = (): AppThunk => (dispatch, getState) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectBoxListState = (state: AppState): EntitiesRequest<Box> => state.boxList
|
export const selectBoxListState = (state: AppState): EntitiesRequest<DetailedBox> => state.boxList
|
||||||
|
|
||||||
export const selectBoxList = createSelector(
|
export const selectBoxList = createSelector(
|
||||||
selectBoxListState,
|
selectBoxListState,
|
||||||
@ -60,11 +60,9 @@ export const selectBoxList = createSelector(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const selectSortedUniqueDetailedBoxes = createSelector(selectBoxList, (boxes) =>
|
export const selectSortedUniqueDetailedBoxes = createSelector(selectBoxList, (boxes) =>
|
||||||
sortedUniqBy(
|
sortedUniqBy(sortBy(boxes, "title"), "title")
|
||||||
sortBy(
|
)
|
||||||
boxes.filter((box) => box && !box.unplayable),
|
|
||||||
"title"
|
export const selectContainerSortedDetailedBoxes = createSelector(selectBoxList, (boxes) =>
|
||||||
),
|
sortBy(boxes, "container")
|
||||||
"title"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
@ -21,6 +21,7 @@ import volunteerMealsSet from "./volunteerMealsSet"
|
|||||||
import volunteerList from "./volunteerList"
|
import volunteerList from "./volunteerList"
|
||||||
import volunteerLogin from "./volunteerLogin"
|
import volunteerLogin from "./volunteerLogin"
|
||||||
import volunteerKnowledgeSet from "./volunteerKnowledgeSet"
|
import volunteerKnowledgeSet from "./volunteerKnowledgeSet"
|
||||||
|
import volunteerDetailedKnowledgeList from "./volunteerDetailedKnowledgeList"
|
||||||
import volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
|
import volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
|
||||||
import volunteerPersonalInfoSet from "./volunteerPersonalInfoSet"
|
import volunteerPersonalInfoSet from "./volunteerPersonalInfoSet"
|
||||||
import volunteerSet from "./volunteerSet"
|
import volunteerSet from "./volunteerSet"
|
||||||
@ -52,6 +53,7 @@ export default (history: History) => ({
|
|||||||
volunteerList,
|
volunteerList,
|
||||||
volunteerLogin,
|
volunteerLogin,
|
||||||
volunteerKnowledgeSet,
|
volunteerKnowledgeSet,
|
||||||
|
volunteerDetailedKnowledgeList,
|
||||||
volunteerParticipationDetailsSet,
|
volunteerParticipationDetailsSet,
|
||||||
volunteerPersonalInfoSet,
|
volunteerPersonalInfoSet,
|
||||||
volunteerSet,
|
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[]
|
||||||
|
}
|
||||||
|
)
|