Add /emprunts and better BGG games parsing

This commit is contained in:
pikiou 2022-09-24 03:49:23 +02:00
parent 72a633ae4f
commit 2a1d2ef49b
72 changed files with 884 additions and 34 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

12
src/app/img/giftable.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.638 0 0 1.638 -14.93 -26.05)" fill-rule="evenodd" stroke-width="1.084">
<path d="m257.5 107.7h-62.31l22.29-17.88-12.77-16.98-19.95 14.64 9.175-29.63-19.95-6.169-10.38 31.43-10.35-31.43-19.99 6.169 9.174 29.63-20.69-14.91-12.77 16.98 23.03 18.15h-60.01v182.9h185.5v-182.9z"/>
<g fill="#FB8">
<path d="m240.1 129.4h-66.02v29.01h66.02v-29.01z"/>
<path d="m152.2 129.4h-60.63v29.01h60.63v-29.01z"/>
<path d="m240.1 179.8h-66.02v89.41h66.02v-89.41z"/>
<path d="m152.2 179.8h-60.63v89.41h60.63v-89.41z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 675 B

View File

@ -0,0 +1,7 @@
<svg width="147" height="147" version="1.1" viewBox="0 0 1581 1581" xmlns="http://www.w3.org/2000/svg">
<path d="m320.7 462.6c14.74 7.684 29.66 15.05 44.46 22.62-68.16 285.7-63 591.2 73.48 842.6-9.222-82.36-22.51-23.56 7.917-126.5 80.94 15.62 53.63 4.685 109.3 38.32-101.5-216.1-124.1-468.1-59.85-688.1 14.94 7.422 29.65 15.29 44.69 22.46 4.471-97.97 19.95-192.1 33.81-314-105.1 81.69-178.1 123.4-253.8 202.5z"/>
<path d="m1339 1112c-14.74-7.684-29.66-15.05-44.46-22.62 68.16-285.7 63-591.2-73.48-842.6 9.222 82.36 22.51 23.56-7.917 126.5-80.94-15.62-53.63-4.685-109.3-38.32 101.5 216.1 124.1 468.1 59.85 688.1-14.94-7.422-29.65-15.29-44.69-22.46-4.471 97.97-19.95 192.1-33.81 314 105.1-81.69 178.1-123.4 253.8-202.5z"/>
<g transform="matrix(1.637,0,0,1.492,1191,889)">
<path d="m-362.9 294.4-222.1 1.919-0.7906 106 222.1-1.919z" fill="#fff" fill-rule="evenodd" stroke-width="1.084"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 897 B

4
src/app/img/loanable.svg Normal file
View File

@ -0,0 +1,4 @@
<svg version="1.1" viewBox="0 0 1647 1647" xmlns="http://www.w3.org/2000/svg">
<path d="m320.7 462.6c14.74 7.684 29.66 15.05 44.46 22.62-68.16 285.7-63 591.2 73.48 842.6-9.222-82.36-22.51-23.56 7.917-126.5 80.94 15.62 53.63 4.685 109.3 38.32-101.5-216.1-124.1-468.1-59.85-688.1 14.94 7.422 29.65 15.29 44.69 22.46 4.471-97.97 19.95-192.1 33.81-314-105.1 81.69-178.1 123.4-253.8 202.5z"/>
<path d="m1339 1112c-14.74-7.684-29.66-15.05-44.46-22.62 68.16-285.7 63-591.2-73.48-842.6 9.222 82.36 22.51 23.56-7.917 126.5-80.94-15.62-53.63-4.685-109.3-38.32 101.5 216.1 124.1 468.1 59.85 688.1-14.94-7.422-29.65-15.29-44.69-22.46-4.471 97.97-19.95 192.1-33.81 314 105.1-81.69 178.1-123.4 253.8-202.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 703 B

View File

@ -0,0 +1,4 @@
<svg width="20.98mm" height="18mm" version="1.1" viewBox="0 0 20.98 18" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="10.64" cy="4.959" rx="1.664" ry="1.643" fill-opacity=".9853" stroke-width=".999"/>
<path class="UnoptimicedTransforms" transform="matrix(1.713 0 0 1.713 -7.382 .08839)" d="m9.777 3.951c-0.2838-0.07088-0.5598-0.04619-0.6816 0.2852l-0.6074 1.928-0.373-1.035 0.1504-0.5781c-0.5512 0.6313-1.322 0.4093-1.711 0.793l1.229-0.01562 0.6211 1.76 0.8535-1.68-0.1484 3.264 1.396-0.1914 1.396 0.1914-0.1484-3.264 0.8535 1.68 0.6211-1.76 1.229 0.01562c-0.3885-0.3837-1.16-0.1617-1.711-0.793l0.1504 0.5781-0.373 1.035-0.6074-1.928c-0.2437-0.6627-1.104-0.09524-1.41-0.001953-0.1532-0.04664-0.4448-0.2123-0.7285-0.2832z" fill-opacity=".9853"/>
</svg>

After

Width:  |  Height:  |  Size: 755 B

13
src/app/img/playable.svg Normal file
View File

@ -0,0 +1,13 @@
<svg width="20.98mm" height="18mm" version="1.1" viewBox="0 0 20.98 18" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-22.63 -20.84)" fill-opacity=".9853">
<g>
<ellipse cx="25.95" cy="24.37" rx="1.666" ry="1.645"/>
<path d="m26.21 26.12c-0.9445 0.02935-1.688 0.3129-1.682 1.02 0.0094 1.2-0.6131 5.638 0.1249 6.621 0.6255 0.8335 2.379 0.6755 2.899 0.6098 0.03165 0.0086 0.06436 0.01458 0.09883 0.01458h1.666v2.54c0 0.2538 0.1488 0.4582 0.3334 0.4582h1.249c0.1845 0 0.3334-0.2044 0.3334-0.4582v-4.081c0-0.2076-0.1672-0.3748-0.3748-0.3748h-3.156c0.01777-0.8546 0.04491-1.887 0.04281-2.862l2.096 0.6786c0.1253 0.04057 0.2764-0.08329 0.3393-0.2774l0.1835-0.567c0.06285-0.1941 0.01278-0.383-0.1125-0.4235l-2.539-0.8221c-0.03974-0.7396-0.1192-1.342-0.2692-1.639-0.1618-0.3206-0.6662-0.4548-1.233-0.4372z"/>
<path d="m40.8 26.12c0.9445 0.02936 1.812 0.4794 1.807 1.186-0.0094 1.2 0.4881 5.471-0.2498 6.454-0.6255 0.8335-2.379 0.6755-2.899 0.6098-0.03166 0.0086-0.06437 0.01458-0.09883 0.01458h-1.666v2.54c0 0.2538-0.1488 0.4582-0.3334 0.4582h-1.249c-0.1845 0-0.3334-0.2044-0.3334-0.4582v-4.081c0-0.2076 0.1672-0.3748 0.3748-0.3748h3.156c-0.01776-0.8546-0.04491-1.887-0.04282-2.862l-2.096 0.6786c-0.1253 0.04057-0.2764-0.08329-0.3393-0.2774l-0.1835-0.567c-0.06286-0.1941-0.01278-0.383 0.1125-0.4235l2.539-0.8221c0.03974-0.7396 0.1192-1.342 0.2692-1.639 0.1618-0.3206 0.6662-0.4548 1.233-0.4372z"/>
<ellipse cx="41.05" cy="24.37" rx="1.666" ry="1.645"/>
</g>
<ellipse cx="33.17" cy="23.21" rx="1.666" ry="1.645" fill="#909090"/>
<path d="m33.14 24.98c-0.8121 0-1.479 0.6306-1.602 1.459h-0.02187v3.466h3.581v-3.466h-0.02232c-0.1225-0.8287-0.7896-1.459-1.602-1.459z" fill="#909090"/>
<path d="m29.17 30.39c-0.3345 0-0.6039 0.2694-0.6039 0.6039 0 0.3345 0.2694 0.6034 0.6039 0.6034h2.731c-0.02857 0.1133-0.04555 0.2314-0.04555 0.3539v4.04c0 0.7957 0.6407 1.436 1.436 1.436h0.2505c0.7957 0 1.436-0.6403 1.436-1.436v-4.04c0-0.1224-0.01653-0.2405-0.04509-0.3539h2.606c0.3345 0 0.6039-0.269 0.6039-0.6034 0-0.3345-0.2694-0.6039-0.6039-0.6039z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -8,7 +8,7 @@ import {
useVolunteerKnowledge,
} from "../../store/volunteerKnowledgeSet"
const BoxList: React.FC = (): JSX.Element | null => {
const KnowledgeBoxList: React.FC = (): JSX.Element | null => {
const detailedBoxes = useSelector(selectSortedUniqueDetailedBoxes)
const [volunteerKnowledge, saveVolunteerKnowledge] = useVolunteerKnowledge()
const [showUnknownOnly, setShowUnknownOnly] = useState(false)
@ -53,6 +53,6 @@ const BoxList: React.FC = (): JSX.Element | null => {
)
}
export default memo(BoxList)
export default memo(KnowledgeBoxList)
export const fetchFor = [fetchBoxListIfNeed, fetchVolunteerKnowledgeSetIfNeed]

View File

@ -0,0 +1,156 @@
import { cloneDeep, pull, sortBy, uniq } from "lodash"
import React, { memo, useCallback, useEffect, useState } from "react"
import classnames from "classnames"
import styles from "./styles.module.scss"
import { DetailedBox } from "../../services/boxes"
import { VolunteerLoan, VolunteerLoanWithoutId } from "../../services/volunteers"
interface Props {
detailedBox: DetailedBox
volunteerLoan: VolunteerLoan | undefined
saveVolunteerLoan: (newVolunteerLoan: VolunteerLoan) => void
}
const BoxItem: React.FC<Props> = ({
detailedBox,
volunteerLoan,
saveVolunteerLoan,
}): JSX.Element => {
type ChoiceValue = keyof VolunteerLoanWithoutId
const [loanable, setLoanable] = useState(false)
const [playable, setPlayable] = useState(false)
const [giftable, setGiftable] = useState(false)
const [noOpinion, setNoOpinion] = useState(false)
const { gameId, bggPhoto, title, bggId, poufpaf, bggIdAlternative } = detailedBox
const isBggPhoto = !/^[0-9]+\.[a-zA-Z]+$/.test(bggPhoto)
const loanChoices: { name: string; value: ChoiceValue }[] = [
{ name: "Empruntable", value: "loanable" },
{ name: "Jouable", value: "playable" },
{ name: "Offrable", value: "giftable" },
{ name: "SansAvis", value: "noOpinion" },
]
const loanValues = {
loanable,
playable,
giftable,
noOpinion,
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const loanSetters = {
loanable: setLoanable,
playable: setPlayable,
giftable: setGiftable,
noOpinion: setNoOpinion,
}
useEffect(() => {
if (!volunteerLoan) return
setLoanable(volunteerLoan.loanable.includes(gameId))
setPlayable(volunteerLoan.playable.includes(gameId))
setGiftable(volunteerLoan.giftable.includes(gameId))
setNoOpinion(volunteerLoan.noOpinion.includes(gameId))
}, [gameId, volunteerLoan])
const onChoiceClick = useCallback(
(value: ChoiceValue) => {
if (!volunteerLoan) return
const adding = !volunteerLoan[value].includes(gameId)
const newVolunteerLoan: VolunteerLoan = cloneDeep(volunteerLoan)
if (adding) {
newVolunteerLoan[value] = sortBy(uniq([...newVolunteerLoan[value], gameId]))
} else {
pull(newVolunteerLoan[value], gameId)
}
if (adding && value === "noOpinion") {
pull(newVolunteerLoan.loanable, gameId)
pull(newVolunteerLoan.playable, gameId)
pull(newVolunteerLoan.giftable, gameId)
setLoanable(volunteerLoan.loanable.includes(gameId))
setPlayable(volunteerLoan.playable.includes(gameId))
setGiftable(volunteerLoan.giftable.includes(gameId))
setNoOpinion(true)
} else {
pull(newVolunteerLoan.noOpinion, gameId)
setNoOpinion(false)
loanSetters[value](adding)
}
saveVolunteerLoan(newVolunteerLoan)
},
[volunteerLoan, gameId, saveVolunteerLoan, loanSetters]
)
const onCheckboxChange = useCallback(() => {
// Do nothing
}, [])
return (
<li className={styles.boxItem}>
<div className={styles.photoContainer}>
{isBggPhoto && <img className={styles.photo} src={bggPhoto} alt="" />}
{!isBggPhoto && (
<div
className={classnames(
styles.alternateBox,
styles[`a${bggPhoto.replace(/\..*/, "")}`]
)}
>
{" "}
</div>
)}
</div>
<div className={classnames(styles.titleContainer, poufpaf && styles.shorterTitle)}>
<a
href={
bggId > 0
? `https://boardgamegeek.com/boardgame/${bggId}`
: bggIdAlternative
}
target="_blank"
rel="noreferrer"
className={styles.title}
>
{title}
</a>
</div>
<a
href={`https://sites.google.com/site/poufpafpasteque/fiches_alpha/${poufpaf}`}
target="_blank"
rel="noreferrer"
>
<div className={poufpaf ? styles.poufpaf : styles.noPoufpaf}> </div>
</a>
<ul className={styles.loanList}>
{loanChoices.map(({ name, value }) => (
<li key={value} className={styles.loanItem}>
<button
type="button"
onClick={() => onChoiceClick(value)}
className={classnames(
styles.loanButton,
styles[value],
loanValues[value] && styles.active
)}
data-message={name}
>
<input
type="checkbox"
className={styles.loanCheckbox}
checked={loanValues[value]}
onChange={() => onCheckboxChange()}
/>
<div
className={classnames(
styles.loanCheckboxImg,
styles[`${value}Checkbox`]
)}
/>
</button>
</li>
))}
</ul>
</li>
)
}
export default memo(BoxItem)

View File

@ -0,0 +1,56 @@
import React, { memo, useState } from "react"
import { useSelector } from "react-redux"
import styles from "./styles.module.scss"
import BoxItem from "./BoxItem"
import { fetchBoxListIfNeed, selectSortedUniqueDetailedBoxes } from "../../store/boxList"
import { fetchVolunteerLoanSetIfNeed, useVolunteerLoan } from "../../store/volunteerLoanSet"
const LoanBoxList: React.FC = (): JSX.Element | null => {
const detailedBoxes = useSelector(selectSortedUniqueDetailedBoxes)
const [volunteerLoan, saveVolunteerLoan] = useVolunteerLoan()
const [showUnknownOnly, setShowUnknownOnly] = useState(false)
const onShowUnknownOnly = (e: React.ChangeEvent<HTMLInputElement>) =>
setShowUnknownOnly(e.target.checked)
if (!detailedBoxes || detailedBoxes.length === 0) return null
const boxesToShow = detailedBoxes.filter(
(box) =>
!box ||
!showUnknownOnly ||
!volunteerLoan ||
(!volunteerLoan.loanable.includes(box.gameId) &&
!volunteerLoan.playable.includes(box.gameId) &&
!volunteerLoan.giftable.includes(box.gameId) &&
!volunteerLoan.noOpinion.includes(box.gameId))
)
return (
<div className={styles.loanThings}>
<label className={styles.showUnknownOnlyLabel}>
<input
type="checkbox"
name="showUnknownOnly"
onChange={onShowUnknownOnly}
checked={showUnknownOnly}
/>{" "}
Uniquement les non-renseignés
</label>
<ul className={styles.boxList}>
{boxesToShow.map((detailedBox: any) => (
<BoxItem
detailedBox={detailedBox}
volunteerLoan={volunteerLoan}
saveVolunteerLoan={saveVolunteerLoan}
key={detailedBox.id}
/>
))}
</ul>
</div>
)
}
export default memo(LoanBoxList)
export const fetchFor = [fetchBoxListIfNeed, fetchVolunteerLoanSetIfNeed]

View File

@ -0,0 +1,57 @@
import classnames from "classnames"
import React from "react"
import styles from "./styles.module.scss"
const LoanIntro: React.FC = (): JSX.Element => (
<div className={styles.loanThings}>
<h1>Emprunt et tri des jeux</h1>
<p>
Lors du brunch des bénévoles ayant contribué au dernier festival, des boîtes de jeux en
trop dans la ludothèque seront données, et des boîtes qui passe l'année dans la cave
entre deux éditions seront prêtées pour 4 mois.
</p>
<p>
Pour chaque jeu, active le bouton :<br />
<button
type="button"
className={classnames(styles.loanButton, styles.loanable, styles.loanableCheckbox)}
>
&nbsp;
</button>{" "}
si tu veux l'emprunter lors du brunch ou d'un rdv bénévoles mensuel
<br />
<button
type="button"
className={classnames(styles.loanButton, styles.playable, styles.playableCheckbox)}
>
&nbsp;
</button>{" "}
si tu penses que les visiteurs du festival (ou même les bénévoles) doivent pouvoir y
jouer
<br />
<button
type="button"
className={classnames(styles.loanButton, styles.giftable, styles.giftableCheckbox)}
>
&nbsp;
</button>{" "}
si tu aimerais le récupérer comme cadeau lors du brunch (ou rdv bénévole si tu ne peux
pas venir)
<br />
<button
type="button"
className={classnames(
styles.loanButton,
styles.noOpinion,
styles.noOpinionCheckbox
)}
>
&nbsp;
</button>{" "}
si tu n'as pas d'avis sur ce jeu
<br />
</p>
</div>
)
export default LoanIntro

View File

@ -0,0 +1,292 @@
@import "../../theme/variables";
@import "../../theme/mixins";
.showUnknownOnlyLabel {
text-align: left;
display: inline-block;
margin-bottom: 10px;
width: 280px;
}
.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;
}
.a371 {
background: url("../../app/img/gameImages/371.png") no-repeat;
}
.a888 {
background: url("../../app/img/gameImages/888.jpg") no-repeat;
}
.a918 {
background: url("../../app/img/gameImages/918.jpg") no-repeat;
}
.a992 {
background: url("../../app/img/gameImages/992.jpg") no-repeat;
}
.a993 {
background: url("../../app/img/gameImages/993.jpg") no-repeat;
}
.a994 {
background: url("../../app/img/gameImages/994.png") no-repeat;
}
.a995 {
background: url("../../app/img/gameImages/995.jpg") no-repeat;
}
.a996 {
background: url("../../app/img/gameImages/996.png") no-repeat;
}
.a997 {
background: url("../../app/img/gameImages/997.jpg") no-repeat;
}
.a998 {
background: url("../../app/img/gameImages/998.jpg") no-repeat;
}
.a999 {
background: url("../../app/img/gameImages/999.png") no-repeat;
}
.a1000 {
background: url("../../app/img/gameImages/1000.jpg") no-repeat;
}
.a1001 {
background: url("../../app/img/gameImages/1001.png") no-repeat;
}
.a1002 {
background: url("../../app/img/gameImages/1002.jpg") no-repeat;
}
.a1003 {
background: url("../../app/img/gameImages/1003.jpg") no-repeat;
}
.a1004 {
background: url("../../app/img/gameImages/1004.png") no-repeat;
}
.a1005 {
background: url("../../app/img/gameImages/1005.png") no-repeat;
}
.a1007 {
background: url("../../app/img/gameImages/1007.png") no-repeat;
}
.a1008 {
background: url("../../app/img/gameImages/1008.png") no-repeat;
}
.a1009 {
background: url("../../app/img/gameImages/1009.png") no-repeat;
}
.a1010 {
background: url("../../app/img/gameImages/1010.jpg") no-repeat;
}
.a1011 {
background: url("../../app/img/gameImages/1011.jpg") no-repeat;
}
.a1012 {
background: url("../../app/img/gameImages/1012.jpg") no-repeat;
}
.a1013 {
background: url("../../app/img/gameImages/1013.jpg") no-repeat;
}
.a1014 {
background: url("../../app/img/gameImages/1014.jpg") no-repeat;
}
.a1015 {
background: url("../../app/img/gameImages/1015.jpg") no-repeat;
}
.a1016 {
background: url("../../app/img/gameImages/1016.jpg") no-repeat;
}
.a1017 {
background: url("../../app/img/gameImages/1017.jpg") no-repeat;
}
.a1018 {
background: url("../../app/img/gameImages/1018.jpg") no-repeat;
}
.a1019 {
background: url("../../app/img/gameImages/1019.jpg") no-repeat;
}
.a1020 {
background: url("../../app/img/gameImages/1020.jpg") no-repeat;
}
.a1021 {
background: url("../../app/img/gameImages/1021.jpg") no-repeat;
}
.a1022 {
background: url("../../app/img/gameImages/1022.jpg") no-repeat;
}
.a1023 {
background: url("../../app/img/gameImages/1023.jpg") no-repeat;
}
.a1024 {
background: url("../../app/img/gameImages/1024.jpg") no-repeat;
}
.a1025 {
background: url("../../app/img/gameImages/1025.jpg") no-repeat;
}
.a1026 {
background: url("../../app/img/gameImages/1026.jpg") no-repeat;
}
.a1027 {
background: url("../../app/img/gameImages/1027.jpg") no-repeat;
}
.a1028 {
background: url("../../app/img/gameImages/1028.jpg") no-repeat;
}
.a1029 {
background: url("../../app/img/gameImages/1029.jpeg") no-repeat;
}
.a1030 {
background: url("../../app/img/gameImages/1030.jpeg") no-repeat;
}
.a1031 {
background: url("../../app/img/gameImages/1031.jpg") no-repeat;
}
.a1032 {
background: url("../../app/img/gameImages/1032.png") no-repeat;
}
.alternateBox {
vertical-align: middle;
display: inline-block;
width: inherit;
height: inherit;
background-size: contain;
}
.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;
}
.loanList {
@include clear-ul-style;
width: 266px;
display: inline-block;
text-align: center;
vertical-align: middle;
}
.loanItem {
display: inline-block;
margin: 3px;
}
.loanButton {
margin: 0;
padding: 4px 0 3px;
border: 0;
border-radius: 0;
width: 60px;
text-align: center;
color: $color-grey-medium;
cursor: pointer;
&.active {
color: $color-yellow;
}
}
.loanCheckbox {
vertical-align: middle;
}
.loanCheckboxImg {
vertical-align: middle;
display: inline-block;
width: 2em;
height: 1.4em;
}
.loanThings {
position: relative;
}
.loanThings .loanable {
background-color: $color-loanable;
&.active {
background-color: $color-active-loanable;
}
}
.loanableCheckbox {
background: url("../../app/img/loanable.svg") no-repeat center center;
background-size: contain;
}
.loanThings .playable {
background-color: $color-playable;
&.active {
background-color: $color-active-playable;
}
}
.playableCheckbox {
background: url("../../app/img/playable.svg") no-repeat center center;
background-size: contain;
}
.loanThings .giftable {
background-color: $color-giftable;
&.active {
background-color: $color-active-giftable;
}
}
.giftableCheckbox {
background: url("../../app/img/giftable.svg") no-repeat center center;
background-size: contain;
}
.loanThings .noOpinion {
background-color: $color-no-opinion;
&.active {
background-color: $color-active-no-opinion;
}
}
.noOpinionCheckbox {
background: url("../../app/img/noOpinion.svg") no-repeat center center;
background-size: contain;
}

View File

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

View File

@ -8,8 +8,10 @@ import DayWishesForm, {
import ErrorBoundary from "./ErrorBoundary"
import GameList from "./GameList"
import Loading from "./Loading"
import LoanBoxList, { fetchFor as fetchForLoan } from "./Loan/LoanBoxList"
import LoanIntro from "./Loan/LoanIntro"
import LoginForm from "./LoginForm"
import BoxList, { fetchFor as fetchForKnowledge } from "./Knowledge/BoxList"
import KnowledgeBoxList, { fetchFor as fetchForKnowledge } from "./Knowledge/KnowledgeBoxList"
import KnowledgeCard, { fetchFor as fetchForKnowledgeCard } from "./Knowledge/KnowledgeCard"
import KnowledgeIntro from "./Knowledge/KnowledgeIntro"
import Asks, { fetchFor as fetchForAsks } from "./Asks"
@ -33,16 +35,19 @@ export {
fetchForGameDetailsUpdate,
Board,
fetchForBoard,
BoxList,
fetchForKnowledge,
KnowledgeCard,
fetchForKnowledgeCard,
DayWishesForm,
fetchForDayWishesForm,
ErrorBoundary,
GameList,
KnowledgeBoxList,
fetchForKnowledge,
KnowledgeIntro,
Loading,
LoanBoxList,
fetchForLoan,
LoanIntro,
LoginForm,
Asks,
fetchForAsks,

View File

@ -5,7 +5,7 @@ import { Helmet } from "react-helmet"
import { AppThunk } from "../../store"
import styles from "./styles.module.scss"
import { BoxList, KnowledgeIntro, fetchForKnowledge } from "../../components"
import { KnowledgeBoxList, KnowledgeIntro, fetchForKnowledge } from "../../components"
import { selectUserJwtToken } from "../../store/auth"
export type Props = RouteComponentProps
@ -21,7 +21,7 @@ const KnowledgesPage: FC<Props> = (): JSX.Element => {
<div className={styles.knowledgesContent}>
<Helmet title="KnowledgesPage" />
<KnowledgeIntro />
<BoxList />
<KnowledgeBoxList />
</div>
</div>
)

View File

@ -0,0 +1,33 @@
import { FC, memo } from "react"
import { useSelector } from "react-redux"
import { RouteComponentProps } from "react-router-dom"
import { Helmet } from "react-helmet"
import { AppThunk } from "../../store"
import styles from "./styles.module.scss"
import { LoanBoxList, LoanIntro, fetchForLoan } from "../../components"
import { selectUserJwtToken } from "../../store/auth"
export type Props = RouteComponentProps
const LoanPage: 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 (
<div className={styles.loanPage}>
<div className={styles.loanContent}>
<Helmet title="LoanPage" />
<LoanIntro />
<LoanBoxList />
</div>
</div>
)
}
// Fetch server-side data here
export const loadData = (): AppThunk[] => [...fetchForLoan.map((f) => f())]
export default memo(LoanPage)

16
src/pages/Loan/index.tsx Executable file
View File

@ -0,0 +1,16 @@
import loadable from "@loadable/component"
import { Loading, ErrorBoundary } from "../../components"
import { Props, loadData } from "./LoanPage"
const Loan = loadable(() => import("./LoanPage"), {
fallback: <Loading />,
})
export default (props: Props): JSX.Element => (
<ErrorBoundary>
<Loan {...props} />
</ErrorBoundary>
)
export { loadData }

View File

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

View File

@ -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 AsyncLoan, { loadData as loadLoanData } from "../pages/Loan"
import AsyncKnowledgeCards, { loadData as loadCardKnowledgeData } from "../pages/KnowledgeCards"
import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
@ -44,6 +45,11 @@ export default [
component: AsyncKnowledge,
loadData: loadKnowledgeData,
},
{
path: "/emprunts",
component: AsyncLoan,
loadData: loadLoanData,
},
{
path: "/fiches",
component: AsyncKnowledgeCards,

View File

@ -24,9 +24,10 @@ export const detailedBoxListGet = expressAccessor.get(async (list) => {
id: box?.id || 10000 + game.id,
gameId: game.id,
title: game.title,
bggId: game.bggId,
bggIdAlternative: game.bggIdAlternative,
bggPhoto: game.bggPhoto,
poufpaf: game.poufpaf,
bggId: game.bggId,
container: box?.container || "Non stocké",
playersMin: game.playersMin,
playersMax: game.playersMax,

View File

@ -1,6 +1,6 @@
import axios from "axios"
import { Parser } from "xml2js"
import { assign, cloneDeep, find, maxBy, some } from "lodash"
import { assign, cloneDeep, find } from "lodash"
import ExpressAccessors from "./expressAccessors"
import { Game, GameWithoutId, translationGame } from "../../services/games"
@ -26,28 +26,41 @@ export const gameDetailsUpdate = expressAccessor.listSet(
newList.forEach((game, index, arr) => {
const box = find(parsed.items.item, (item: any) => game.bggId === +item.$.objectid)
if (box && game.bggPhoto === "") {
if (box) {
if (game.bggPhoto === "" || game.duration === 0) {
assign(arr[index], xmlToGame(game.id, box), {
ean: arr[index].ean,
title: arr[index].title,
ean: arr[index].ean || 0,
type: arr[index].type,
})
}
} else {
assign(arr[index], {
bggId: 0,
playersMin: 0,
playersMax: 0,
duration: 0,
ean: 0,
toBeKnown: true,
})
}
})
const newGames = parsed.items.item.filter(
(item: any) =>
(item.status[0]?.$?.own === "1" ||
item.status[0]?.$?.want === "1" ||
item.status[0]?.$?.wishlist === "1" ||
item.status[0]?.$?.preordered === "1") &&
!some(list, (i) => +i.bggId === +item.$.objectid)
)
// // Add to DB game that were only present on BGG
// const newGames = parsed.items.item.filter(
// (item: any) =>
// (item.status[0]?.$?.own === "1" ||
// item.status[0]?.$?.want === "1" ||
// item.status[0]?.$?.wishlist === "1" ||
// item.status[0]?.$?.preordered === "1") &&
// !some(list, (i) => +i.bggId === +item.$.objectid)
// )
let id = maxBy(newList, "id")?.id || 0
newGames.forEach((item: any) => {
id += 1
newList.push(xmlToGame(id, item))
})
// let id = maxBy(newList, "id")?.id || 0
// newGames.forEach((item: any) => {
// id += 1
// newList.push(xmlToGame(id, item))
// })
return {
toDatabase: newList,
@ -59,13 +72,15 @@ export const gameDetailsUpdate = expressAccessor.listSet(
function xmlToGame(id: number, item: any): Game {
return {
id,
title: item.name?.[0]._ || "",
title: "",
bggId: +item.$.objectid || 0,
bggIdAlternative: "",
bggTitle: item.name?.[0]._ || "",
playersMin: item.stats?.[0]?.$?.minplayers || 0,
playersMax: item.stats?.[0]?.$?.maxplayers || 0,
duration: item.stats?.[0]?.$?.playingtime || 0,
type: "Famille",
poufpaf: "",
bggId: +item.$.objectid || 0,
ean: "",
bggPhoto: item.thumbnail?.[0] || "",
toBeKnown: true,

View File

@ -20,6 +20,7 @@ import {
VolunteerKnowledge,
VolunteerDetailedKnowledge,
VolunteerPersonalInfo,
VolunteerLoan,
} from "../../services/volunteers"
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
import { getJwt } from "../secure"
@ -552,6 +553,31 @@ export const volunteerDetailedKnowledgeList = expressAccessor.get(async (list) =
})
})
export const volunteerLoanSet = expressAccessor.set(async (list, body, id) => {
const requestedId = +body[0] || id
const volunteer: Volunteer | undefined = list.find((v) => v.id === requestedId)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
const loan = body[1] as VolunteerLoan
const newVolunteer: Volunteer = cloneDeep(volunteer)
if (loan?.loanable !== undefined) newVolunteer.loanable = loan.loanable
if (loan?.playable !== undefined) newVolunteer.playable = loan.playable
if (loan?.giftable !== undefined) newVolunteer.giftable = loan.giftable
if (loan?.noOpinion !== undefined) newVolunteer.noOpinion = loan.noOpinion
return {
toDatabase: newVolunteer,
toCaller: {
id: newVolunteer.id,
loanable: newVolunteer.loanable,
playable: newVolunteer.playable,
giftable: newVolunteer.giftable,
noOpinion: newVolunteer.noOpinion,
} as VolunteerLoan,
}
})
function getUniqueNickname(list: Volunteer[], volunteer: Volunteer): string {
const lastnameList = list
.filter((v) => v.firstname === volunteer.firstname)

View File

@ -39,6 +39,7 @@ import {
volunteerKnowledgeSet,
volunteerAddNew,
volunteerDetailedKnowledgeList,
volunteerLoanSet,
} from "./gsheets/volunteers"
import { wishListGet, wishAdd } from "./gsheets/wishes"
import config from "../config"
@ -119,6 +120,7 @@ app.post(
secure as RequestHandler,
volunteerDetailedKnowledgeList
)
app.post("/VolunteerLoanSet", secure as RequestHandler, volunteerLoanSet)
app.post(
"/VolunteerParticipationDetailsSet",
secure as RequestHandler,

View File

@ -38,12 +38,14 @@ export class DetailedBox {
title = new Game().title
bggId = new Game().bggId
bggIdAlternative = new Game().bggIdAlternative
bggPhoto = new Game().bggPhoto
poufpaf = new Game().poufpaf
bggId = new Game().bggId
playersMin = new Game().playersMin
playersMax = new Game().playersMax

View File

@ -3,6 +3,12 @@ export class Game {
title = ""
bggId = 0
bggIdAlternative = ""
bggTitle = ""
playersMin = 0
playersMax = 0
@ -13,8 +19,6 @@ export class Game {
poufpaf = ""
bggId = 0
ean = ""
bggPhoto = ""
@ -25,12 +29,14 @@ export class Game {
export const translationGame: { [k in keyof Game]: string } = {
id: "id",
title: "titre",
bggId: "bggId",
bggIdAlternative: "bggIdAlternative",
bggTitle: "titreBgg",
playersMin: "minJoueurs",
playersMax: "maxJoueurs",
duration: "duree",
type: "type",
poufpaf: "poufpaf",
bggId: "bggId",
ean: "ean",
bggPhoto: "bggPhoto",
toBeKnown: "àConnaitre",

View File

@ -58,6 +58,14 @@ export class Volunteer implements VolunteerPartial {
niet: number[] = []
loanable: number[] = []
playable: number[] = []
giftable: number[] = []
noOpinion: number[] = []
needsHosting = false
canHostCount = 0
@ -99,6 +107,10 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
ok: "OK",
bof: "Bof",
niet: "Niet",
loanable: "empruntable",
playable: "jouable",
giftable: "offrable",
noOpinion: "sansAvis",
needsHosting: "besoinHébergement",
canHostCount: "nombreHébergés",
distanceToFestival: "distanceAuFestival",
@ -148,6 +160,10 @@ export const volunteerExample: Volunteer = {
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],
loanable: [5, 7],
playable: [34, 35, 36],
giftable: [13, 67],
noOpinion: [3, 4],
needsHosting: false,
canHostCount: 0,
distanceToFestival: 0,
@ -247,3 +263,12 @@ export interface VolunteerDetailedKnowledge {
niet: Volunteer["niet"]
dayWishes: Volunteer["dayWishes"]
}
export type VolunteerLoanWithoutId = Omit<VolunteerLoan, "id">
export interface VolunteerLoan {
id: Volunteer["id"]
loanable: Volunteer["loanable"]
playable: Volunteer["playable"]
giftable: Volunteer["giftable"]
noOpinion: Volunteer["noOpinion"]
}

View File

@ -13,6 +13,7 @@ import {
VolunteerKnowledge,
VolunteerMeals,
VolunteerPersonalInfo,
VolunteerLoan,
} from "./volunteers"
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
@ -64,3 +65,6 @@ export const volunteerKnowledgeSet =
export const volunteerDetailedKnowledgeList = serviceAccessors.securedCustomPost<[number]>(
"DetailedKnowledgeListGet"
)
export const volunteerLoanSet =
serviceAccessors.securedCustomPost<[number, Partial<VolunteerLoan>]>("LoanSet")

View File

@ -17,6 +17,9 @@ const mockData: Game[] = [
{
id: 5,
title: "6 qui prend!",
bggId: 432,
bggIdAlternative: "",
bggTitle: "6 nimmt!",
playersMin: 2,
playersMax: 10,
duration: 45,
@ -24,7 +27,6 @@ const mockData: Game[] = [
poufpaf: "0-9-2/6-qui-prend-6-nimmt",
bggPhoto:
"https://cf.geekdo-images.com/thumb/img/lzczxR5cw7an7tRWeHdOrRtLyes=/fit-in/200x150/pic772547.jpg",
bggId: 432,
ean: "3421272101313",
toBeKnown: false,
},

View File

@ -20,6 +20,7 @@ import volunteerForgot from "./volunteerForgot"
import volunteerHostingSet from "./volunteerHostingSet"
import volunteerMealsSet from "./volunteerMealsSet"
import volunteerList from "./volunteerList"
import volunteerLoanSet from "./volunteerLoanSet"
import volunteerLogin from "./volunteerLogin"
import volunteerKnowledgeSet from "./volunteerKnowledgeSet"
import volunteerDetailedKnowledgeList from "./volunteerDetailedKnowledgeList"
@ -53,6 +54,7 @@ export default (history: History) => ({
volunteerHostingSet,
volunteerMealsSet,
volunteerList,
volunteerLoanSet,
volunteerLogin,
volunteerKnowledgeSet,
volunteerDetailedKnowledgeList,

View File

@ -0,0 +1,85 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { shallowEqual, useSelector } from "react-redux"
import { useCallback } from "react"
import { StateRequest, toastError, elementFetch } from "./utils"
import { VolunteerLoan } from "../services/volunteers"
import { AppThunk, AppState } from "."
import { volunteerLoanSet } from "../services/volunteersAccessors"
import useAction from "../utils/useAction"
import { selectUserJwtToken } from "./auth"
type StateVolunteerLoanSet = {
entity?: VolunteerLoan
} & StateRequest
export const initialState: StateVolunteerLoanSet = {
readyStatus: "idle",
}
const volunteerLoanSetSlice = createSlice({
name: "volunteerLoanSet",
initialState,
reducers: {
getRequesting: (_) => ({
readyStatus: "request",
}),
getSuccess: (_, { payload }: PayloadAction<VolunteerLoan>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
readyStatus: "failure",
error: payload,
}),
},
})
export default volunteerLoanSetSlice.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerLoanSetSlice.actions
export const fetchVolunteerLoanSet = elementFetch(
volunteerLoanSet,
getRequesting,
getSuccess,
getFailure,
(error: Error) => toastError(`Erreur lors du chargement des emprunts: ${error.message}`)
)
const shouldFetchVolunteerLoanSet = (state: AppState, id: number) =>
state.volunteerLoanSet?.readyStatus !== "success" ||
(state.volunteerLoanSet?.entity && state.volunteerLoanSet?.entity?.id !== id)
export const fetchVolunteerLoanSetIfNeed =
(id = 0, loan: Partial<VolunteerLoan> = {}): AppThunk =>
(dispatch, getState) => {
let jwt = ""
if (!id) {
;({ jwt, id } = getState().auth)
}
if (shouldFetchVolunteerLoanSet(getState(), id))
return dispatch(fetchVolunteerLoanSet(jwt, id, loan))
return null
}
type SetFunction = (newVolunteerLoan: VolunteerLoan) => void
export const useVolunteerLoan = (): [VolunteerLoan | undefined, SetFunction] => {
const save = useAction(fetchVolunteerLoanSet)
const jwtToken = useSelector(selectUserJwtToken)
const volunteerLoan = useSelector(
(state: AppState) => state.volunteerLoanSet?.entity,
shallowEqual
)
const saveVolunteerLoan: SetFunction = useCallback(
(newVolunteerLoan) => {
save(jwtToken, 0, newVolunteerLoan)
},
[save, jwtToken]
)
return [volunteerLoan, saveVolunteerLoan]
}

View File

@ -19,6 +19,15 @@ $color-active-bof: rgb(180, 124, 19);
$color-niet: rgb(233, 215, 217);
$color-active-niet: rgb(158, 17, 41);
$color-no-opinion: rgb(232, 223, 235);
$color-active-no-opinion: rgb(170, 87, 196);
$color-loanable: rgb(215, 233, 217);
$color-active-loanable: rgb(50, 107, 38);
$color-playable: rgb(231, 227, 214);
$color-active-playable: rgb(175, 139, 73);
$color-giftable: rgb(233, 215, 217);
$color-active-giftable: rgb(160, 70, 85);
$border-large: 4px solid $color-black;
$border-thin: 2px solid $color-black;