mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-08 08:34:20 +02:00
Add Mes connaissances
This commit is contained in:
parent
0d69bc93db
commit
1b93b41225
BIN
src/app/img/poufpaf.png
Executable file
BIN
src/app/img/poufpaf.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
97
src/components/Knowledge/BoxItem.tsx
Normal file
97
src/components/Knowledge/BoxItem.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { cloneDeep, pull, sortBy } from "lodash"
|
||||
import React, { memo, useCallback, useEffect, useState } from "react"
|
||||
import classnames from "classnames"
|
||||
import styles from "./styles.module.scss"
|
||||
import { DetailedBox } from "../../services/boxes"
|
||||
import { VolunteerKnowledge, VolunteerKnowledgeWithoutId } from "../../services/volunteers"
|
||||
|
||||
interface Props {
|
||||
detailedBox: DetailedBox
|
||||
volunteerKnowledge: VolunteerKnowledge | undefined
|
||||
saveVolunteerKnowledge: (newVolunteerKnowledge: VolunteerKnowledge) => void
|
||||
}
|
||||
|
||||
const BoxItem: React.FC<Props> = ({
|
||||
detailedBox,
|
||||
volunteerKnowledge,
|
||||
saveVolunteerKnowledge,
|
||||
}): JSX.Element => {
|
||||
type ChoiceValue = keyof VolunteerKnowledgeWithoutId | "unknown"
|
||||
const [choice, setChoice] = useState("unknown")
|
||||
// const discordInvitation = useSelector(selectMiscDiscordInvitation)
|
||||
const { gameId, bggPhoto, title, bggId, poufpaf } = detailedBox
|
||||
const knowledgeChoices: { name: string; value: ChoiceValue }[] = [
|
||||
{ name: "?", value: "unknown" },
|
||||
{ name: "OK", value: "ok" },
|
||||
{ name: "Bof", value: "bof" },
|
||||
{ name: "Niet", value: "niet" },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
if (!volunteerKnowledge) return
|
||||
let providedchoice = "unknown"
|
||||
if (volunteerKnowledge.ok.includes(gameId)) providedchoice = "ok"
|
||||
if (volunteerKnowledge.bof.includes(gameId)) providedchoice = "bof"
|
||||
if (volunteerKnowledge.niet.includes(gameId)) providedchoice = "niet"
|
||||
setChoice(providedchoice)
|
||||
}, [gameId, setChoice, volunteerKnowledge])
|
||||
|
||||
const onChoiceClick = useCallback(
|
||||
(value: ChoiceValue) => {
|
||||
if (!volunteerKnowledge) return
|
||||
const newVolunteerKnowledge: VolunteerKnowledge = cloneDeep(volunteerKnowledge)
|
||||
pull(newVolunteerKnowledge.ok, gameId)
|
||||
pull(newVolunteerKnowledge.bof, gameId)
|
||||
pull(newVolunteerKnowledge.niet, gameId)
|
||||
if (value !== "unknown") {
|
||||
newVolunteerKnowledge[value] = sortBy([...newVolunteerKnowledge[value], gameId])
|
||||
}
|
||||
saveVolunteerKnowledge(newVolunteerKnowledge)
|
||||
},
|
||||
[volunteerKnowledge, gameId, saveVolunteerKnowledge]
|
||||
)
|
||||
|
||||
return (
|
||||
<li className={styles.boxItem}>
|
||||
<div className={styles.photoContainer}>
|
||||
<img className={styles.photo} src={bggPhoto} alt="" />
|
||||
</div>
|
||||
<div className={classnames(styles.titleContainer, poufpaf && styles.shorterTitle)}>
|
||||
<a
|
||||
href={`https://boardgamegeek.com/boardgame/${bggId}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.title}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href={`https://sites.google.com/site/poufpafpasteque/fiches_alpha/${poufpaf}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className={poufpaf ? styles.poufpaf : styles.noPoufpaf}> </div>
|
||||
</a>
|
||||
<ul className={styles.knowledgeList}>
|
||||
{knowledgeChoices.map(({ name, value }) => (
|
||||
<li key={value} className={styles.knowledgeItem}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChoiceClick(value)}
|
||||
className={classnames(
|
||||
styles.knowledgeButton,
|
||||
styles[value],
|
||||
choice === value && styles.active
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(BoxItem)
|
33
src/components/Knowledge/BoxList.tsx
Normal file
33
src/components/Knowledge/BoxList.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { memo } from "react"
|
||||
import { useSelector } from "react-redux"
|
||||
import styles from "./styles.module.scss"
|
||||
import BoxItem from "./BoxItem"
|
||||
import { fetchBoxListIfNeed, selectSortedUniqueDetailedBoxes } from "../../store/boxList"
|
||||
import {
|
||||
fetchVolunteerKnowledgeSetIfNeed,
|
||||
useVolunteerKnowledge,
|
||||
} from "../../store/volunteerKnowledgeSet"
|
||||
|
||||
const BoxList: React.FC = (): JSX.Element | null => {
|
||||
const detailedBoxes = useSelector(selectSortedUniqueDetailedBoxes)
|
||||
const [volunteerKnowledge, saveVolunteerKnowledge] = useVolunteerKnowledge()
|
||||
|
||||
if (!detailedBoxes || detailedBoxes.length === 0) return null
|
||||
|
||||
return (
|
||||
<ul className={styles.boxList}>
|
||||
{detailedBoxes.map((detailedBox: any) => (
|
||||
<BoxItem
|
||||
detailedBox={detailedBox}
|
||||
volunteerKnowledge={volunteerKnowledge}
|
||||
saveVolunteerKnowledge={saveVolunteerKnowledge}
|
||||
key={detailedBox.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(BoxList)
|
||||
|
||||
export const fetchFor = [fetchBoxListIfNeed, fetchVolunteerKnowledgeSetIfNeed]
|
20
src/components/Knowledge/KnowledgeIntro.tsx
Normal file
20
src/components/Knowledge/KnowledgeIntro.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react"
|
||||
|
||||
const KnowledgeIntro: React.FC = (): JSX.Element => (
|
||||
<div>
|
||||
<h1>Tes connaissances en jeux</h1>
|
||||
<p>
|
||||
Lors du festival, si tu es aux Jeux à Volonté, il sera très utile de savoir qui peut
|
||||
expliquer quoi. Ce sera même indiqué dans chaque boîte de jeu ! Mais pour ça il faut que
|
||||
tu nous dises à quel point tu peux les expliquer.
|
||||
</p>
|
||||
<p>
|
||||
OK signifie que tu peux expliquer le jeu, avec au maximum un coup d'oeil aux règles sur
|
||||
un nombre de cartes à distribuer, ou un nombre de PV déclancheur de fin de partie.
|
||||
<br />
|
||||
Bof signifie que tu seras plus utile que la lecture des règles.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default KnowledgeIntro
|
117
src/components/Knowledge/styles.module.scss
Executable file
117
src/components/Knowledge/styles.module.scss
Executable file
@ -0,0 +1,117 @@
|
||||
@import "../../theme/variables";
|
||||
@import "../../theme/mixins";
|
||||
|
||||
.boxList {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.boxItem {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.photoContainer {
|
||||
height: 50px;
|
||||
width: 73px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.photo {
|
||||
height: 50px;
|
||||
max-width: 73px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
height: 50px;
|
||||
width: 198px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.shorterTitle {
|
||||
width: 168px;
|
||||
}
|
||||
.title {
|
||||
height: 50px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.poufpaf {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background: url("../../app/img/poufpaf.png");
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.noPoufpaf {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.knowledgeList {
|
||||
@include clear-ul-style;
|
||||
|
||||
width: 266px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.knowledgeItem {
|
||||
display: inline-block;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.knowledgeButton {
|
||||
margin: 0;
|
||||
padding: 7px 2px 6px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
color: $color-grey-medium;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
color: $color-yellow;
|
||||
}
|
||||
}
|
||||
|
||||
.unknown {
|
||||
background-color: $color-unknown;
|
||||
|
||||
&.active {
|
||||
background-color: $color-active-unknown;
|
||||
}
|
||||
}
|
||||
|
||||
.ok {
|
||||
background-color: $color-ok;
|
||||
|
||||
&.active {
|
||||
background-color: $color-active-ok;
|
||||
}
|
||||
}
|
||||
|
||||
.bof {
|
||||
background-color: $color-bof;
|
||||
|
||||
&.active {
|
||||
background-color: $color-active-bof;
|
||||
}
|
||||
}
|
||||
|
||||
.niet {
|
||||
background-color: $color-niet;
|
||||
|
||||
&.active {
|
||||
background-color: $color-active-niet;
|
||||
}
|
||||
}
|
@ -52,6 +52,7 @@ const MainMenu: FC = (): JSX.Element => {
|
||||
<MenuItem name="Questions" pathname="/" />
|
||||
<MenuItem name="Annonces" pathname="/annonces" />
|
||||
<MenuItem name="Mon profil" pathname="/profil" />
|
||||
<MenuItem name="Mes connaissances" pathname="/connaissances" />
|
||||
<RestrictMenuItem
|
||||
role={ROLES.ASSIGNER}
|
||||
name="Gestion équipes"
|
||||
|
@ -462,6 +462,7 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
|
||||
Ces rencontres ont lieu à 19h dans un bar/resto calme à Châtelet, le{" "}
|
||||
<a
|
||||
href="https://goo.gl/maps/N5NYWDF66vNQDFMh8"
|
||||
id="sfmMap"
|
||||
key="sfmMap"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@ -471,6 +472,7 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
|
||||
, ou à une soirée festive à 2 pas du lieu du festival, aux{" "}
|
||||
<a
|
||||
href="https://www.captainturtle.fr/aperos-petanque-paris/"
|
||||
id="petanque"
|
||||
key="petanque"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
@ -7,6 +7,8 @@ import ErrorBoundary from "./ErrorBoundary"
|
||||
import GameList from "./GameList"
|
||||
import Loading from "./Loading"
|
||||
import LoginForm from "./LoginForm"
|
||||
import BoxList, { fetchFor as fetchForKnowledge } from "./Knowledge/BoxList"
|
||||
import KnowledgeIntro from "./Knowledge/KnowledgeIntro"
|
||||
import Asks, { fetchFor as fetchForAsks } from "./Asks"
|
||||
import ParticipationDetailsForm, {
|
||||
fetchFor as fetchForParticipationDetailsForm,
|
||||
@ -25,10 +27,13 @@ export {
|
||||
AnnouncementLink,
|
||||
Board,
|
||||
fetchForBoard,
|
||||
BoxList,
|
||||
fetchForKnowledge,
|
||||
DayWishesForm,
|
||||
fetchForDayWishesForm,
|
||||
ErrorBoundary,
|
||||
GameList,
|
||||
KnowledgeIntro,
|
||||
Loading,
|
||||
LoginForm,
|
||||
Asks,
|
||||
|
24
src/pages/Knowledge/KnowledgesPage.tsx
Normal file
24
src/pages/Knowledge/KnowledgesPage.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { FC, memo } from "react"
|
||||
import { RouteComponentProps } from "react-router-dom"
|
||||
import { Helmet } from "react-helmet"
|
||||
|
||||
import { AppThunk } from "../../store"
|
||||
import styles from "./styles.module.scss"
|
||||
import { BoxList, KnowledgeIntro, fetchForKnowledge } from "../../components"
|
||||
|
||||
export type Props = RouteComponentProps
|
||||
|
||||
const KnowledgesPage: FC<Props> = (): JSX.Element => (
|
||||
<div className={styles.knowledgesPage}>
|
||||
<div className={styles.knowledgesContent}>
|
||||
<Helmet title="KnowledgesPage" />
|
||||
<KnowledgeIntro />
|
||||
<BoxList />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Fetch server-side data here
|
||||
export const loadData = (): AppThunk[] => [...fetchForKnowledge.map((f) => f())]
|
||||
|
||||
export default memo(KnowledgesPage)
|
16
src/pages/Knowledge/index.tsx
Executable file
16
src/pages/Knowledge/index.tsx
Executable file
@ -0,0 +1,16 @@
|
||||
import loadable from "@loadable/component"
|
||||
|
||||
import { Loading, ErrorBoundary } from "../../components"
|
||||
import { Props, loadData } from "./KnowledgesPage"
|
||||
|
||||
const Knowledges = loadable(() => import("./KnowledgesPage"), {
|
||||
fallback: <Loading />,
|
||||
})
|
||||
|
||||
export default (props: Props): JSX.Element => (
|
||||
<ErrorBoundary>
|
||||
<Knowledges {...props} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
export { loadData }
|
9
src/pages/Knowledge/styles.module.scss
Executable file
9
src/pages/Knowledge/styles.module.scss
Executable file
@ -0,0 +1,9 @@
|
||||
@import "../../theme/mixins";
|
||||
|
||||
.knowledgesPage {
|
||||
@include page-wrapper-center;
|
||||
}
|
||||
|
||||
.knowledgesContent {
|
||||
@include page-content-wrapper(700px);
|
||||
}
|
@ -5,6 +5,7 @@ import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
|
||||
import AsyncAnnouncements, { loadData as loadAnnouncementsData } from "../pages/Announcements"
|
||||
import AsyncTeamAssignment, { loadData as loadTeamAssignmentData } from "../pages/TeamAssignment"
|
||||
import AsyncRegisterPage, { loadData as loadRegisterPage } from "../pages/Register"
|
||||
import AsyncKnowledge, { loadData as loadKnowledgeData } from "../pages/Knowledge"
|
||||
import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
|
||||
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
|
||||
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
|
||||
@ -23,6 +24,11 @@ export default [
|
||||
component: AsyncHome,
|
||||
loadData: loadHomeData,
|
||||
},
|
||||
{
|
||||
path: "/connaissances",
|
||||
component: AsyncKnowledge,
|
||||
loadData: loadKnowledgeData,
|
||||
},
|
||||
{
|
||||
path: "/preRegister",
|
||||
component: AsyncRegisterPage,
|
||||
|
@ -406,8 +406,7 @@ export class Sheet<
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
default: {
|
||||
const matchArrayType = type.match(
|
||||
/^(number|string|boolean|date)\[([^\]]+)\]$/
|
||||
)
|
||||
@ -438,11 +437,9 @@ export class Sheet<
|
||||
)
|
||||
break
|
||||
|
||||
case "date":
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
case "date": {
|
||||
const rawDates = rawProp.split(delimiter)
|
||||
element[prop] = []
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
rawDates.forEach((rawDate) => {
|
||||
try {
|
||||
element[prop].push(parseDate(rawDate))
|
||||
@ -453,12 +450,14 @@ export class Sheet<
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown array type ${arrayType} in sheet ${this.name} at prop ${prop}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return element
|
||||
},
|
||||
@ -492,19 +491,20 @@ export class Sheet<
|
||||
stringifiedElement[prop as keyof Element] = stringifiedDate(value)
|
||||
break
|
||||
|
||||
default:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
default: {
|
||||
const matchArrayType = type.match(
|
||||
/^(number|string|boolean|date)\[([^\]]+)\]$/
|
||||
)
|
||||
if (!matchArrayType || !_.isArray(value)) {
|
||||
throw new Error(
|
||||
"Unknown matchArrayType or not an array in stringifyElement"
|
||||
`Unknown matchArrayType ${JSON.stringify(
|
||||
matchArrayType
|
||||
)} or not an array in stringifyElement for prop ${prop} and value ${JSON.stringify(
|
||||
value
|
||||
)}, ${JSON.stringify(element)}`
|
||||
)
|
||||
}
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const arrayType = matchArrayType[1]
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const delimiter = matchArrayType[2]
|
||||
|
||||
switch (arrayType) {
|
||||
@ -554,6 +554,7 @@ export class Sheet<
|
||||
default:
|
||||
throw new Error(`Unknown array type ${arrayType}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stringifiedElement
|
||||
@ -586,7 +587,6 @@ function stringifiedDate(value: unknown): string {
|
||||
}
|
||||
|
||||
function parseDate(value: string): Date {
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const matchDate = value.match(/^([0-9]+)\/([0-9]+)\/([0-9]+)$/)
|
||||
if (!matchDate) {
|
||||
throw new Error(`Unable to read date from val ${value}`)
|
||||
|
32
src/server/gsheets/boxes.ts
Normal file
32
src/server/gsheets/boxes.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import ExpressAccessors from "./expressAccessors"
|
||||
import { Box, BoxWithoutId, translationBox, DetailedBox } from "../../services/boxes"
|
||||
import { Game, GameWithoutId, translationGame } from "../../services/games"
|
||||
import { getSheet } from "./accessors"
|
||||
|
||||
const expressAccessor = new ExpressAccessors<BoxWithoutId, Box>("Boxes", new Box(), translationBox)
|
||||
|
||||
export const detailedBoxListGet = expressAccessor.get(async (list) => {
|
||||
const gameSheet = await getSheet<GameWithoutId, Game>("Games", new Game(), translationGame)
|
||||
|
||||
const gameList = await gameSheet.getList()
|
||||
if (!gameList) {
|
||||
throw Error("Unable to load gameList")
|
||||
}
|
||||
|
||||
return list
|
||||
.filter((box) => !box.unplayable)
|
||||
.map((box) => {
|
||||
const game = gameList.find((g) => g.id === box.gameId)
|
||||
if (!game) {
|
||||
throw Error(`Unable to find game #${box.gameId}`)
|
||||
}
|
||||
return {
|
||||
id: box.id,
|
||||
gameId: box.gameId,
|
||||
title: game.title,
|
||||
bggPhoto: game.bggPhoto,
|
||||
poufpaf: game.poufpaf,
|
||||
bggId: game.bggId,
|
||||
} as DetailedBox
|
||||
})
|
||||
})
|
@ -8,6 +8,6 @@ const expressAccessor = new ExpressAccessors<GameWithoutId, Game>(
|
||||
)
|
||||
|
||||
export const gameListGet = expressAccessor.listGet()
|
||||
export const gameGet = expressAccessor.get()
|
||||
export const gameAdd = expressAccessor.add()
|
||||
export const gameSet = expressAccessor.set()
|
||||
// export const gameGet = expressAccessor.get()
|
||||
// export const gameAdd = expressAccessor.add()
|
||||
// export const gameSet = expressAccessor.set()
|
||||
|
@ -12,6 +12,8 @@ const ANONYMIZED_DB_PATH = path.resolve(process.cwd(), "access/dbAnonymized.json
|
||||
export class SheetNames {
|
||||
Announcements = "Annonces"
|
||||
|
||||
Boxes = "Boîtes"
|
||||
|
||||
Games = "Jeux"
|
||||
|
||||
Miscs = "Divers"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import _ from "lodash"
|
||||
import { assign, cloneDeep, keys, omit, pick } from "lodash"
|
||||
import bcrypt from "bcrypt"
|
||||
import sgMail from "@sendgrid/mail"
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
VolunteerDayWishes,
|
||||
VolunteerParticipationDetails,
|
||||
VolunteerTeamAssign,
|
||||
VolunteerKnowledge,
|
||||
} from "../../services/volunteers"
|
||||
import { canonicalEmail, canonicalMobile, trim, validMobile } from "../../utils/standardization"
|
||||
import { getJwt } from "../secure"
|
||||
@ -40,7 +41,7 @@ export const volunteerDiscordId = expressAccessor.get(async (list, body, id) =>
|
||||
if (!volunteer) {
|
||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
||||
}
|
||||
return _.pick(volunteer, "id", "discordId")
|
||||
return pick(volunteer, "id", "discordId")
|
||||
})
|
||||
|
||||
export const volunteerPartialAdd = expressAccessor.add(async (list, body) => {
|
||||
@ -58,9 +59,9 @@ export const volunteerPartialAdd = expressAccessor.add(async (list, body) => {
|
||||
const password = generatePassword()
|
||||
const passwordHash = await bcrypt.hash(password, 10)
|
||||
|
||||
const newVolunteer = _.omit(new Volunteer(), "id")
|
||||
const newVolunteer = omit(new Volunteer(), "id")
|
||||
|
||||
_.assign(newVolunteer, {
|
||||
assign(newVolunteer, {
|
||||
lastname: trim(params.lastname),
|
||||
firstname: trim(params.firstname),
|
||||
email: trim(params.email),
|
||||
@ -135,7 +136,7 @@ export const volunteerForgot = expressAccessor.set(async (list, bodyArray) => {
|
||||
if (!volunteer) {
|
||||
throw Error("Il n'y a aucun bénévole avec cet email")
|
||||
}
|
||||
const newVolunteer = _.cloneDeep(volunteer)
|
||||
const newVolunteer = cloneDeep(volunteer)
|
||||
|
||||
const now = +new Date()
|
||||
const timeSinceLastSent = now - lastForgot[volunteer.id]
|
||||
@ -196,9 +197,9 @@ export const volunteerAsksSet = expressAccessor.set(async (list, body, id) => {
|
||||
if (!volunteer) {
|
||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
||||
}
|
||||
const newVolunteer = _.cloneDeep(volunteer)
|
||||
const newVolunteer = cloneDeep(volunteer)
|
||||
|
||||
_.assign(newVolunteer, _.pick(notifChanges, _.keys(newVolunteer)))
|
||||
assign(newVolunteer, pick(notifChanges, keys(newVolunteer)))
|
||||
|
||||
return {
|
||||
toDatabase: newVolunteer,
|
||||
@ -226,7 +227,7 @@ export const volunteerTeamWishesSet = expressAccessor.set(async (list, body, id,
|
||||
if (!volunteer) {
|
||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
||||
}
|
||||
const newVolunteer = _.cloneDeep(volunteer)
|
||||
const newVolunteer = cloneDeep(volunteer)
|
||||
|
||||
if (wishes.teamWishes !== undefined) {
|
||||
newVolunteer.teamWishes = wishes.teamWishes
|
||||
@ -255,7 +256,7 @@ export const volunteerDayWishesSet = expressAccessor.set(async (list, body, id)
|
||||
if (!volunteer) {
|
||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
||||
}
|
||||
const newVolunteer = _.cloneDeep(volunteer)
|
||||
const newVolunteer = cloneDeep(volunteer)
|
||||
|
||||
if (wishes.active !== undefined) {
|
||||
newVolunteer.active = wishes.active
|
||||
@ -290,7 +291,7 @@ export const volunteerParticipationDetailsSet = expressAccessor.set(async (list,
|
||||
if (!volunteer) {
|
||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
||||
}
|
||||
const newVolunteer = _.cloneDeep(volunteer)
|
||||
const newVolunteer = cloneDeep(volunteer)
|
||||
|
||||
if (wishes.tshirtSize !== undefined) {
|
||||
newVolunteer.tshirtSize = wishes.tshirtSize
|
||||
@ -329,7 +330,7 @@ export const volunteerTeamAssignSet = expressAccessor.set(async (list, body, id)
|
||||
if (!volunteer) {
|
||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${teamAssign.volunteer}`)
|
||||
}
|
||||
const newVolunteer = _.cloneDeep(volunteer)
|
||||
const newVolunteer = cloneDeep(volunteer)
|
||||
newVolunteer.team = teamAssign.team
|
||||
|
||||
return {
|
||||
@ -345,3 +346,26 @@ function getByEmail<T extends { email: string }>(list: T[], rawEmail: string): T
|
||||
const email = canonicalEmail(rawEmail || "")
|
||||
return list.find((v) => canonicalEmail(v.email) === email)
|
||||
}
|
||||
|
||||
export const volunteerKnowledgeSet = expressAccessor.set(async (list, body, id) => {
|
||||
const requestedId = +body[0] || id
|
||||
const volunteer = list.find((v) => v.id === requestedId)
|
||||
if (!volunteer) {
|
||||
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
|
||||
}
|
||||
const knowledge = body[1] as VolunteerKnowledge
|
||||
const newVolunteer = cloneDeep(volunteer)
|
||||
if (knowledge?.ok !== undefined) newVolunteer.ok = knowledge.ok
|
||||
if (knowledge?.bof !== undefined) newVolunteer.bof = knowledge.bof
|
||||
if (knowledge?.niet !== undefined) newVolunteer.niet = knowledge.niet
|
||||
|
||||
return {
|
||||
toDatabase: newVolunteer,
|
||||
toCaller: {
|
||||
id: newVolunteer.id,
|
||||
ok: newVolunteer.ok,
|
||||
bof: newVolunteer.bof,
|
||||
niet: newVolunteer.niet,
|
||||
} as VolunteerKnowledge,
|
||||
}
|
||||
})
|
||||
|
@ -18,6 +18,7 @@ import ssr from "./ssr"
|
||||
import certbotRouter from "../routes/certbot"
|
||||
import { hasSecret, secure } from "./secure"
|
||||
import { announcementListGet } from "./gsheets/announcements"
|
||||
import { detailedBoxListGet } from "./gsheets/boxes"
|
||||
import { gameListGet } from "./gsheets/games"
|
||||
import { postulantAdd } from "./gsheets/postulants"
|
||||
import { teamListGet } from "./gsheets/teams"
|
||||
@ -33,6 +34,7 @@ import {
|
||||
volunteerTeamWishesSet,
|
||||
volunteerTeamAssignSet,
|
||||
volunteerListGet,
|
||||
volunteerKnowledgeSet,
|
||||
} from "./gsheets/volunteers"
|
||||
import { wishListGet, wishAdd } from "./gsheets/wishes"
|
||||
import config from "../config"
|
||||
@ -84,6 +86,7 @@ app.get(
|
||||
* APIs
|
||||
*/
|
||||
// Google Sheets API
|
||||
app.get("/BoxDetailedListGet", detailedBoxListGet)
|
||||
app.get("/GameListGet", gameListGet)
|
||||
app.get("/MiscMeetingDateListGet", miscMeetingDateListGet)
|
||||
app.get("/WishListGet", wishListGet)
|
||||
@ -101,6 +104,7 @@ app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
|
||||
app.get("/TeamListGet", teamListGet)
|
||||
app.get("/VolunteerDiscordId", secure as RequestHandler, volunteerDiscordId)
|
||||
app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet)
|
||||
app.post("/VolunteerKnowledgeSet", secure as RequestHandler, volunteerKnowledgeSet)
|
||||
app.post(
|
||||
"/VolunteerParticipationDetailsSet",
|
||||
secure as RequestHandler,
|
||||
|
48
src/services/boxes.ts
Normal file
48
src/services/boxes.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { Game } from "./games"
|
||||
|
||||
export class Box {
|
||||
id = 0
|
||||
|
||||
gameId = 0
|
||||
|
||||
container = ""
|
||||
|
||||
unplayable = false
|
||||
|
||||
specificEan = 0
|
||||
|
||||
missingParts = ""
|
||||
|
||||
verified = new Date()
|
||||
}
|
||||
|
||||
export const translationBox: { [k in keyof Box]: string } = {
|
||||
id: "id",
|
||||
gameId: "jeuId",
|
||||
container: "caisse",
|
||||
unplayable: "injouable",
|
||||
specificEan: "eanSpécifique",
|
||||
missingParts: "partiesManquantes",
|
||||
verified: "verifié",
|
||||
}
|
||||
|
||||
export const elementName = "Box"
|
||||
|
||||
export type BoxWithoutId = Omit<Box, "id">
|
||||
|
||||
export class DetailedBox {
|
||||
id = 0
|
||||
|
||||
gameId = 0
|
||||
|
||||
title = new Game().title
|
||||
|
||||
bggPhoto = new Game().bggPhoto
|
||||
|
||||
poufpaf = new Game().poufpaf
|
||||
|
||||
bggId = new Game().bggId
|
||||
}
|
||||
|
||||
export type DetailedBoxWithoutId = Omit<DetailedBox, "id">
|
9
src/services/boxesAccessors.ts
Normal file
9
src/services/boxesAccessors.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import ServiceAccessors from "./accessors"
|
||||
import { elementName, Box, BoxWithoutId, DetailedBox } from "./boxes"
|
||||
|
||||
const serviceAccessors = new ServiceAccessors<BoxWithoutId, Box>(elementName)
|
||||
|
||||
export const detailedBoxListGet = serviceAccessors.customGet<[], DetailedBox>("DetailedListGet")
|
||||
// export const boxGet = serviceAccessors.get()
|
||||
// export const boxAdd = serviceAccessors.add()
|
||||
// export const boxSet = serviceAccessors.set()
|
@ -4,6 +4,6 @@ import { elementName, Game, GameWithoutId } from "./games"
|
||||
const serviceAccessors = new ServiceAccessors<GameWithoutId, Game>(elementName)
|
||||
|
||||
export const gameListGet = serviceAccessors.listGet()
|
||||
export const gameGet = serviceAccessors.get()
|
||||
export const gameAdd = serviceAccessors.add()
|
||||
export const gameSet = serviceAccessors.set()
|
||||
// export const gameGet = serviceAccessors.get()
|
||||
// export const gameAdd = serviceAccessors.add()
|
||||
// export const gameSet = serviceAccessors.set()
|
||||
|
@ -53,6 +53,12 @@ export class Volunteer implements VolunteerPartial {
|
||||
pushNotifSubscription = ""
|
||||
|
||||
acceptsNotifs = ""
|
||||
|
||||
ok: number[] = []
|
||||
|
||||
bof: number[] = []
|
||||
|
||||
niet: number[] = []
|
||||
}
|
||||
|
||||
export const translationVolunteer: { [k in keyof Volunteer]: string } = {
|
||||
@ -83,6 +89,9 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = {
|
||||
password2: "passe2",
|
||||
pushNotifSubscription: "pushNotifSubscription",
|
||||
acceptsNotifs: "accepteLesNotifs",
|
||||
ok: "OK",
|
||||
bof: "Bof",
|
||||
niet: "Niet",
|
||||
}
|
||||
|
||||
export class VolunteerPartial {
|
||||
@ -125,6 +134,9 @@ export const volunteerExample: Volunteer = {
|
||||
password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O",
|
||||
pushNotifSubscription: "",
|
||||
acceptsNotifs: "",
|
||||
ok: [5, 7, 24, 26, 31, 38, 50, 52, 54, 58],
|
||||
bof: [9, 12, 16, 27, 34, 35, 36],
|
||||
niet: [13, 18, 19, 23, 47, 53, 59, 67],
|
||||
}
|
||||
|
||||
export const emailRegexp =
|
||||
@ -184,3 +196,11 @@ export interface VolunteerTeamAssign {
|
||||
volunteer: number
|
||||
team: Volunteer["team"]
|
||||
}
|
||||
|
||||
export type VolunteerKnowledgeWithoutId = Omit<VolunteerKnowledge, "id">
|
||||
export interface VolunteerKnowledge {
|
||||
id: Volunteer["id"]
|
||||
ok: Volunteer["ok"]
|
||||
bof: Volunteer["bof"]
|
||||
niet: Volunteer["niet"]
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
VolunteerTeamAssign,
|
||||
VolunteerWithoutId,
|
||||
VolunteerDiscordId,
|
||||
VolunteerKnowledge,
|
||||
} from "./volunteers"
|
||||
|
||||
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
|
||||
@ -42,3 +43,6 @@ export const volunteerParticipationDetailsSet =
|
||||
|
||||
export const volunteerTeamAssignSet =
|
||||
serviceAccessors.securedCustomPost<[number, Partial<VolunteerTeamAssign>]>("TeamAssignSet")
|
||||
|
||||
export const volunteerKnowledgeSet =
|
||||
serviceAccessors.securedCustomPost<[number, Partial<VolunteerKnowledge>]>("KnowledgeSet")
|
||||
|
70
src/store/boxList.ts
Normal file
70
src/store/boxList.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { PayloadAction, createSlice, createEntityAdapter, createSelector } from "@reduxjs/toolkit"
|
||||
import { sortedUniqBy, sortBy } from "lodash"
|
||||
|
||||
import { StateRequest, toastError, elementListFetch } from "./utils"
|
||||
import { Box } from "../services/boxes"
|
||||
import { AppThunk, AppState, EntitiesRequest } from "."
|
||||
import { detailedBoxListGet } from "../services/boxesAccessors"
|
||||
|
||||
const boxAdapter = createEntityAdapter<Box>()
|
||||
|
||||
export const initialState = boxAdapter.getInitialState({
|
||||
readyStatus: "idle",
|
||||
} as StateRequest)
|
||||
|
||||
const boxList = createSlice({
|
||||
name: "boxList",
|
||||
initialState,
|
||||
reducers: {
|
||||
getRequesting: (state) => {
|
||||
state.readyStatus = "request"
|
||||
},
|
||||
getSuccess: (state, { payload }: PayloadAction<Box[]>) => {
|
||||
state.readyStatus = "success"
|
||||
boxAdapter.setAll(state, payload)
|
||||
},
|
||||
getFailure: (state, { payload }: PayloadAction<string>) => {
|
||||
state.readyStatus = "failure"
|
||||
state.error = payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default boxList.reducer
|
||||
export const { getRequesting, getSuccess, getFailure } = boxList.actions
|
||||
|
||||
export const fetchBoxList = elementListFetch(
|
||||
detailedBoxListGet,
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement des jeux JAV: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchBoxList = (state: AppState) => state.boxList.readyStatus !== "success"
|
||||
|
||||
export const fetchBoxListIfNeed = (): AppThunk => (dispatch, getState) => {
|
||||
if (shouldFetchBoxList(getState())) return dispatch(fetchBoxList())
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const selectBoxListState = (state: AppState): EntitiesRequest<Box> => state.boxList
|
||||
|
||||
export const selectBoxList = createSelector(
|
||||
selectBoxListState,
|
||||
({ ids, entities, readyStatus }) => {
|
||||
if (readyStatus !== "success") return []
|
||||
return ids.map((id) => entities[id])
|
||||
}
|
||||
)
|
||||
|
||||
export const selectSortedUniqueDetailedBoxes = createSelector(selectBoxList, (boxes) =>
|
||||
sortedUniqBy(
|
||||
sortBy(
|
||||
boxes.filter((box) => box && !box.unplayable),
|
||||
"title"
|
||||
),
|
||||
"title"
|
||||
)
|
||||
)
|
@ -3,6 +3,7 @@ import { connectRouter } from "connected-react-router"
|
||||
|
||||
import announcementList from "./announcementList"
|
||||
import auth from "./auth"
|
||||
import boxList from "./boxList"
|
||||
import gameList from "./gameList"
|
||||
import miscDiscordInvitation from "./miscDiscordInvitation"
|
||||
import miscMeetingDateList from "./miscMeetingDateList"
|
||||
@ -10,16 +11,17 @@ import postulantAdd from "./postulantAdd"
|
||||
import teamList from "./teamList"
|
||||
import ui from "./ui"
|
||||
import volunteerAdd from "./volunteerPartialAdd"
|
||||
import volunteerDiscordId from "./volunteerDiscordId"
|
||||
import volunteerList from "./volunteerList"
|
||||
import volunteerSet from "./volunteerSet"
|
||||
import volunteerLogin from "./volunteerLogin"
|
||||
import volunteerForgot from "./volunteerForgot"
|
||||
import volunteerAsksSet from "./volunteerAsksSet"
|
||||
import volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
|
||||
import volunteerDayWishesSet from "./volunteerDayWishesSet"
|
||||
import volunteerTeamWishesSet from "./volunteerTeamWishesSet"
|
||||
import volunteerDiscordId from "./volunteerDiscordId"
|
||||
import volunteerForgot from "./volunteerForgot"
|
||||
import volunteerList from "./volunteerList"
|
||||
import volunteerLogin from "./volunteerLogin"
|
||||
import volunteerKnowledgeSet from "./volunteerKnowledgeSet"
|
||||
import volunteerParticipationDetailsSet from "./volunteerParticipationDetailsSet"
|
||||
import volunteerSet from "./volunteerSet"
|
||||
import volunteerTeamAssignSet from "./volunteerTeamAssignSet"
|
||||
import volunteerTeamWishesSet from "./volunteerTeamWishesSet"
|
||||
import wishAdd from "./wishAdd"
|
||||
import wishList from "./wishList"
|
||||
|
||||
@ -28,6 +30,7 @@ import wishList from "./wishList"
|
||||
export default (history: History) => ({
|
||||
announcementList,
|
||||
auth,
|
||||
boxList,
|
||||
gameList,
|
||||
miscDiscordInvitation,
|
||||
miscMeetingDateList,
|
||||
@ -35,16 +38,17 @@ export default (history: History) => ({
|
||||
teamList,
|
||||
ui,
|
||||
volunteerAdd,
|
||||
volunteerDiscordId,
|
||||
volunteerList,
|
||||
volunteerSet,
|
||||
volunteerLogin,
|
||||
volunteerForgot,
|
||||
volunteerAsksSet,
|
||||
volunteerParticipationDetailsSet,
|
||||
volunteerDayWishesSet,
|
||||
volunteerTeamWishesSet,
|
||||
volunteerDiscordId,
|
||||
volunteerForgot,
|
||||
volunteerList,
|
||||
volunteerLogin,
|
||||
volunteerKnowledgeSet,
|
||||
volunteerParticipationDetailsSet,
|
||||
volunteerSet,
|
||||
volunteerTeamAssignSet,
|
||||
volunteerTeamWishesSet,
|
||||
wishAdd,
|
||||
wishList,
|
||||
router: connectRouter(history) as any,
|
||||
|
@ -37,7 +37,8 @@ export const fetchVolunteerDayWishesSet = elementFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
|
||||
(error: Error) =>
|
||||
toastError(`Erreur lors du chargement des choix de jours de présence: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchVolunteerDayWishesSet = (state: AppState, id: number) =>
|
||||
|
86
src/store/volunteerKnowledgeSet.ts
Normal file
86
src/store/volunteerKnowledgeSet.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
import { shallowEqual, useSelector } from "react-redux"
|
||||
import { useCallback } from "react"
|
||||
import { StateRequest, toastError, elementFetch } from "./utils"
|
||||
import { VolunteerKnowledge } from "../services/volunteers"
|
||||
import { AppThunk, AppState } from "."
|
||||
import { volunteerKnowledgeSet } from "../services/volunteersAccessors"
|
||||
import useAction from "../utils/useAction"
|
||||
import { selectUserJwtToken } from "./auth"
|
||||
|
||||
type StateVolunteerKnowledgeSet = {
|
||||
entity?: VolunteerKnowledge
|
||||
} & StateRequest
|
||||
|
||||
export const initialState: StateVolunteerKnowledgeSet = {
|
||||
readyStatus: "idle",
|
||||
}
|
||||
|
||||
const volunteerKnowledgeSetSlice = createSlice({
|
||||
name: "volunteerKnowledgeSet",
|
||||
initialState,
|
||||
reducers: {
|
||||
getRequesting: (_) => ({
|
||||
readyStatus: "request",
|
||||
}),
|
||||
getSuccess: (_, { payload }: PayloadAction<VolunteerKnowledge>) => ({
|
||||
readyStatus: "success",
|
||||
entity: payload,
|
||||
}),
|
||||
getFailure: (_, { payload }: PayloadAction<string>) => ({
|
||||
readyStatus: "failure",
|
||||
error: payload,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
export default volunteerKnowledgeSetSlice.reducer
|
||||
export const { getRequesting, getSuccess, getFailure } = volunteerKnowledgeSetSlice.actions
|
||||
|
||||
export const fetchVolunteerKnowledgeSet = elementFetch(
|
||||
volunteerKnowledgeSet,
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement des connaissances: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchVolunteerKnowledgeSet = (state: AppState, id: number) =>
|
||||
state.volunteerKnowledgeSet?.readyStatus !== "success" ||
|
||||
(state.volunteerKnowledgeSet?.entity && state.volunteerKnowledgeSet?.entity?.id !== id)
|
||||
|
||||
export const fetchVolunteerKnowledgeSetIfNeed =
|
||||
(id = 0, knowledge: Partial<VolunteerKnowledge> = {}): AppThunk =>
|
||||
(dispatch, getState) => {
|
||||
let jwt = ""
|
||||
|
||||
if (!id) {
|
||||
;({ jwt, id } = getState().auth)
|
||||
}
|
||||
|
||||
if (shouldFetchVolunteerKnowledgeSet(getState(), id))
|
||||
return dispatch(fetchVolunteerKnowledgeSet(jwt, id, knowledge))
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const useVolunteerKnowledge = (): [
|
||||
VolunteerKnowledge | undefined,
|
||||
(newVolunteerKnowledge: VolunteerKnowledge) => void
|
||||
] => {
|
||||
const save = useAction(fetchVolunteerKnowledgeSet)
|
||||
const jwtToken = useSelector(selectUserJwtToken)
|
||||
const volunteerKnowledge = useSelector(
|
||||
(state: AppState) => state.volunteerKnowledgeSet?.entity,
|
||||
shallowEqual
|
||||
)
|
||||
|
||||
const saveVolunteerKnowledge = useCallback(
|
||||
(newVolunteerKnowledge: VolunteerKnowledge) => {
|
||||
save(jwtToken, 0, newVolunteerKnowledge)
|
||||
},
|
||||
[save, jwtToken]
|
||||
)
|
||||
|
||||
return [volunteerKnowledge, saveVolunteerKnowledge]
|
||||
}
|
@ -40,7 +40,8 @@ export const fetchVolunteerParticipationDetailsSet = elementFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
|
||||
(error: Error) =>
|
||||
toastError(`Erreur lors du chargement des détails de participation: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchVolunteerParticipationDetailsSet = (state: AppState, id: number) =>
|
||||
|
@ -37,7 +37,8 @@ export const fetchVolunteerTeamAssignSet = elementFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
|
||||
(error: Error) =>
|
||||
toastError(`Erreur lors du chargement des assignation d'équipe: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchVolunteerTeamAssignSet = (state: AppState, id: number) =>
|
||||
|
@ -37,7 +37,7 @@ export const fetchVolunteerTeamWishesSet = elementFetch(
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
(error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`)
|
||||
(error: Error) => toastError(`Erreur lors du chargement des choix d'équipe: ${error.message}`)
|
||||
)
|
||||
|
||||
const shouldFetchVolunteerTeamWishesSet = (state: AppState, id: number) =>
|
||||
|
@ -10,6 +10,15 @@ $color-grey-light: #ccc;
|
||||
$color-grey-lighter: #eee;
|
||||
$color-green: #080;
|
||||
|
||||
$color-unknown: rgb(232, 223, 235);
|
||||
$color-active-unknown: rgb(167, 74, 196);
|
||||
$color-ok: rgb(215, 233, 217);
|
||||
$color-active-ok: rgb(27, 105, 11);
|
||||
$color-bof: rgb(231, 227, 214);
|
||||
$color-active-bof: rgb(180, 124, 19);
|
||||
$color-niet: rgb(233, 215, 217);
|
||||
$color-active-niet: rgb(158, 17, 41);
|
||||
|
||||
$border-large: 4px solid $color-black;
|
||||
$border-thin: 2px solid $color-black;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user