Add Ask Discord

This commit is contained in:
pikiou
2022-04-19 02:33:05 +02:00
parent ebefcb247c
commit 74b5ef9e96
20 changed files with 198 additions and 223 deletions

View File

@@ -0,0 +1,67 @@
import { useCallback } from "react"
import { useSelector } from "react-redux"
import { fetchVolunteerAsksSet } from "../../store/volunteerAsksSet"
import styles from "./styles.module.scss"
import { useAskTools, addAsk } from "./utils"
import FormButton from "../Form/FormButton/FormButton"
import {
fetchVolunteerDiscordIdIfNeed,
selectVolunteerDiscordId,
} from "../../store/volunteerDiscordId"
export function AskDiscord(asks: JSX.Element[], id: number): void {
const { dispatch, jwtToken, volunteerAsks } = useAskTools()
const discordId: number | undefined = useSelector(selectVolunteerDiscordId)
const onSubmit = useCallback((): void => {
dispatch(
fetchVolunteerAsksSet(jwtToken, 0, {
hiddenAsks: [...(volunteerAsks?.hiddenAsks || []), id],
})
)
}, [dispatch, id, jwtToken, volunteerAsks?.hiddenAsks])
const needToShow = !discordId
addAsk(
asks,
id,
volunteerAsks,
true,
needToShow,
<div className={styles.formLine}>
<p>
Discord nous permet gratuitement et sans pub de s'écrire entre bénévoles via nos
navigateurs ou smartphones. Et donc de s'organiser super efficacement !<br />
C'est un peu déroutant au début, mais extrêmement pratique car à chaque sujet de
discussion correspond un salon différent que tu peux demander à suivre ou ignorer
totalement via la gestion des notifications.
<br />
Pour rejoindre le serveur PeL, voici le lien d'invitation à cliquer :{" "}
<a href="https://discord.gg/eXhjKxSBB4" onClick={onSubmit}>
https://discord.gg/eXhjKxSBB4
</a>{" "}
!
</p>
<p>
Prends le temps de le rejoindre maintenant, c'est via cet outil que la plupart des
équipes s'organisent !
</p>
<p>
Pour s'y retrouver tellement on est nombreux (plus de 120), il est nécessaire
d'avoir son prénom comme alias. Voir même d'avoir ensuite la première lettre de ton
nom de famille si un autre bénévole présent sur le serveur a le même prénom. Pour
changer ton alias uniquement sur le serveur PeL, il faut faire un clique droit sur
l'icône ronde du serveur en haut à gauche, et aller dans Modifier le profil du
serveur.
</p>
<div className={styles.formButtons}>
<FormButton onClick={onSubmit}>Ok, noté</FormButton>
</div>
</div>
)
}
// Fetch server-side data here
export const fetchFor = [fetchVolunteerDiscordIdIfNeed]

View File

@@ -191,7 +191,7 @@ export function AskPushNotif(asks: JSX.Element[], id: number): void {
volunteerAsks,
true,
needToShow,
<div className={styles.formLine} key="line-participation">
<div className={styles.formLine}>
<label>
Acceptes-tu de recevoir une alerte dans ton navigateur quand on en aura
d&apos;autres à t'afficher ici ?<br />

View File

@@ -3,19 +3,21 @@ import React, { memo } from "react"
import styles from "./styles.module.scss"
import { useAskTools } from "./utils"
import { AskWelcome } from "./AskWelcome"
import { AskPushNotif } from "./AskPushNotif"
import { AskDiscord, fetchFor as fetchForDiscord } from "./AskDiscord"
import { AskDayWishes, fetchFor as fetchForDayWishes } from "./AskDayWishes"
import { AskTeamWishes, fetchFor as fetchForTeamWishes } from "./AskTeamWishes"
import {
AskParticipationDetails,
fetchFor as fetchForParticipationDetails,
} from "./AskParticipationDetails"
import { AskPushNotif } from "./AskPushNotif"
const Asks = (): JSX.Element | null => {
const { volunteerAsks } = useAskTools()
const asks: JSX.Element[] = []
AskWelcome(asks, 1)
AskDiscord(asks, 3)
AskDayWishes(asks, 10)
AskTeamWishes(asks, 11)
@@ -28,7 +30,7 @@ const Asks = (): JSX.Element | null => {
<div key="pushNotifs">
<div className={styles.notificationsPage}>
<div className={styles.notificationsContent}>
<div className={styles.formLine} key="line-participation">
<div className={styles.formLine}>
<label>
Tu as fait le tour des dernières infos ou questions importantes,
merci ! :)
@@ -54,6 +56,7 @@ export default memo(Asks)
// Fetch server-side data here
export const fetchFor = [
...fetchForDiscord,
...fetchForDayWishes,
...fetchForTeamWishes,
...fetchForParticipationDetails,

View File

@@ -34,7 +34,7 @@ export function addAsk(
volunteerAsks: VolunteerAsks | undefined,
isNarrow: boolean,
needToShow: boolean,
children: JSX.Element
children: JSX.Element | undefined
): void {
const hidden = volunteerAsks?.hiddenAsks || []
if (_.includes(hidden, id) || !_.isEmpty(asks) || !needToShow) {

View File

@@ -778,7 +778,7 @@ const RegisterForm = ({ dispatch }: Props): JSX.Element => {
{meeting}
{helpBefore}
{pelMemberQuestion}
{pelMember && (
{(potentialVolunteer || pelMember) && (
<>
{nameMobileEmail}
{howToContact !== "Aucun" && submitButton}

View File

@@ -1,53 +0,0 @@
import { useEffect, memo } from "react"
import { RouteComponentProps } from "react-router-dom"
import { useDispatch, useSelector, shallowEqual } from "react-redux"
import { Helmet } from "react-helmet"
import { AppState, AppThunk } from "../../store"
import { fetchVolunteerIfNeed } from "../../store/volunteer"
import { VolunteerInfo, VolunteerSet } from "../../components"
import styles from "./styles.module.scss"
export type Props = RouteComponentProps<{ id: string }>
const VolunteerPage = ({ match }: Props): JSX.Element => {
const { id: rawId } = match.params
const id = +rawId
const dispatch = useDispatch()
const volunteer = useSelector((state: AppState) => state.volunteer, shallowEqual)
useEffect(() => {
dispatch(fetchVolunteerIfNeed(id))
}, [dispatch, id])
const renderInfo = () => {
const volunteerInfo = volunteer
if (!volunteerInfo || volunteerInfo.readyStatus === "request") return <p>Loading...</p>
if (volunteerInfo.readyStatus === "failure" || !volunteerInfo.entity)
return <p>Oops! Failed to load data.</p>
return (
<div>
<VolunteerInfo item={volunteerInfo.entity} />
<VolunteerSet dispatch={dispatch} volunteer={volunteerInfo.entity} />
</div>
)
}
return (
<div className={styles.VolunteerPage}>
<Helmet title="User Info" />
{renderInfo()}
</div>
)
}
interface LoadDataArgs {
params: { id: number }
}
export const loadData = ({ params }: LoadDataArgs): AppThunk[] => [fetchVolunteerIfNeed(params.id)]
export default memo(VolunteerPage)

View File

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

View File

@@ -1,3 +0,0 @@
.VolunteerPage {
padding: 0 15px;
}

View File

@@ -8,7 +8,6 @@ import AsyncTeams, { loadData as loadTeamsData } from "../pages/Teams"
import AsyncBoard, { loadData as loadBoardData } from "../pages/Board"
import AsyncVolunteers, { loadData as loadVolunteersData } from "../pages/Volunteers"
import AsyncWish, { loadData as loadWishData } from "../pages/Wish"
import AsyncVolunteerPage, { loadData as loadVolunteerPageData } from "../pages/VolunteerPage"
import Login from "../pages/Login"
import Forgot from "../pages/Forgot"
import NotFound from "../pages/NotFound"
@@ -33,11 +32,6 @@ export default [
component: AsyncRegisterPage,
loadData: loadRegisterPage,
},
{
path: "/VolunteerPage/:id",
component: AsyncVolunteerPage,
loadData: loadVolunteerPageData,
},
{
path: "/login",
component: Login,

View File

@@ -23,9 +23,20 @@ const expressAccessor = new ExpressAccessors<VolunteerWithoutId, Volunteer>(
)
export const volunteerListGet = expressAccessor.listGet()
// export const volunteerAdd = expressAccessor.add()
export const volunteerSet = expressAccessor.set()
export const volunteerDiscordId = expressAccessor.get(async (list, body, id) => {
const requestedId = +body[0] || id
if (requestedId !== id && requestedId !== 0) {
throw Error(`On ne peut acceder qu'à ses propres envies de jours`)
}
const volunteer = list.find((v) => v.id === requestedId)
if (!volunteer) {
throw Error(`Il n'y a aucun bénévole avec cet identifiant ${requestedId}`)
}
return _.pick(volunteer, "id", "discordId")
})
export const volunteerPartialAdd = expressAccessor.add(async (list, body) => {
const params = body[0]
const volunteer = getByEmail(list, params.email)

View File

@@ -22,10 +22,11 @@ import { gameListGet } from "./gsheets/games"
import { postulantAdd } from "./gsheets/postulants"
import { teamListGet } from "./gsheets/teams"
import {
volunteerAsksSet,
volunteerDayWishesSet,
volunteerForgot,
volunteerDiscordId,
volunteerLogin,
volunteerAsksSet,
volunteerPartialAdd,
volunteerParticipationDetailsSet,
volunteerSet,
@@ -92,7 +93,7 @@ app.post("/VolunteerForgot", volunteerForgot)
app.get("/AnnouncementListGet", secure as RequestHandler, announcementListGet)
app.post("/VolunteerSet", secure as RequestHandler, volunteerSet)
app.get("/TeamListGet", teamListGet)
// UNSAFE app.post("/VolunteerGet", secure as RequestHandler, volunteerGet)
app.get("/VolunteerDiscordId", secure as RequestHandler, volunteerDiscordId)
app.post("/VolunteerAsksSet", secure as RequestHandler, volunteerAsksSet)
app.post(
"/VolunteerParticipationDetailsSet",

View File

@@ -64,7 +64,7 @@ export default class ServiceAccessors<
}
}
secureListGet(): (jwt: string) => Promise<{
securedListGet(): (jwt: string) => Promise<{
data?: Element[]
error?: Error
}> {
@@ -192,6 +192,37 @@ export default class ServiceAccessors<
}
}
securedCustomGet<InputElements extends Array<any>>(
apiName: string
): (
jwt: string,
...params: InputElements
) => Promise<{
data?: any
error?: Error
}> {
interface ElementGetResponse {
data?: any
error?: Error
}
return async (jwt: string, ...params: InputElements): Promise<ElementGetResponse> => {
try {
const auth = { headers: { Authorization: `Bearer ${jwt}` } }
const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig)
const { data } = await axios.get(
`${config.API_URL}/${this.elementName}${apiName}`,
{ ...fullAxiosConfig, params }
)
if (data.error) {
throw Error(data.error)
}
return { data }
} catch (error) {
return { error: error as Error }
}
}
}
securedCustomPost<InputElements extends Array<any>>(
apiName: string
): (

View File

@@ -7,4 +7,4 @@ const serviceAccessors = new ServiceAccessors<AnnouncementWithoutId, Announcemen
// export const announcementAdd = serviceAccessors.add()
// export const announcementSet = serviceAccessors.set()
export const announcementListGet = serviceAccessors.secureListGet()
export const announcementListGet = serviceAccessors.securedListGet()

View File

@@ -139,6 +139,11 @@ export interface VolunteerForgot {
message: string
}
export interface VolunteerDiscordId {
id: Volunteer["id"]
discordId: Volunteer["discordId"]
}
export interface VolunteerAsks {
id: Volunteer["id"]
firstname: Volunteer["firstname"]

View File

@@ -12,7 +12,7 @@ import {
const serviceAccessors = new ServiceAccessors<VolunteerWithoutId, Volunteer>(elementName)
export const volunteerListGet = serviceAccessors.listGet()
export const volunteerGet = serviceAccessors.get()
export const volunteerDiscordIdGet = serviceAccessors.securedCustomGet<[number]>("DiscordId")
export const volunteerPartialAdd = serviceAccessors.customPost<[Partial<Volunteer>]>("PartialAdd")
export const volunteerSet = serviceAccessors.set()

View File

@@ -1,78 +0,0 @@
import axios from "axios"
import mockStore from "../../utils/mockStore"
import volunteer, {
getRequesting,
getSuccess,
getFailure,
fetchVolunteer,
initialState,
} from "../volunteer"
import { Volunteer, volunteerExample } from "../../services/volunteers"
jest.mock("axios")
const mockData: Volunteer = volunteerExample
const { id } = mockData
const mockError = "Oops! Something went wrong."
describe("volunteer reducer", () => {
it("should handle initial state correctly", () => {
// @ts-expect-error
expect(volunteer(undefined, {})).toEqual(initialState)
})
it("should handle requesting correctly", () => {
expect(volunteer(undefined, { type: getRequesting.type, payload: id })).toEqual({
readyStatus: "request",
})
})
it("should handle success correctly", () => {
expect(
volunteer(undefined, {
type: getSuccess.type,
payload: mockData,
})
).toEqual({ readyStatus: "success", entity: mockData })
})
it("should handle failure correctly", () => {
expect(
volunteer(undefined, {
type: getFailure.type,
payload: mockError,
})
).toEqual({ readyStatus: "failure", error: mockError })
})
})
describe("volunteer action", () => {
it("fetches volunteer data successful", async () => {
const { dispatch, getActions } = mockStore()
const expectedActions = [
{ type: getRequesting.type, payload: undefined },
{ type: getSuccess.type, payload: mockData },
]
// @ts-expect-error
axios.get.mockResolvedValue({ data: mockData })
await dispatch(fetchVolunteer(id))
expect(getActions()).toEqual(expectedActions)
})
it("fetches volunteer data failed", async () => {
const { dispatch, getActions } = mockStore()
const expectedActions = [
{ type: getRequesting.type },
{ type: getFailure.type, payload: mockError },
]
// @ts-expect-error
axios.get.mockRejectedValue({ message: mockError })
await dispatch(fetchVolunteer(id))
expect(getActions()).toEqual(expectedActions)
})
})

View File

@@ -35,9 +35,9 @@ export const auth = createSlice({
export const { setCurrentUser, logoutUser } = auth.actions
export const selectAuthData = (state: AppState): AuthState => state.auth
const selectAuthData = (state: AppState): AuthState => state.auth
export const selectRouter = (state: AppState): AppState["router"] => state.router
const selectRouter = (state: AppState): AppState["router"] => state.router
export const selectUserJwtToken = createSelector(selectAuthData, (authData) => authData.jwt)

View File

@@ -7,8 +7,8 @@ import announcementList from "./announcementList"
import postulantAdd from "./postulantAdd"
import teamList from "./teamList"
import ui from "./ui"
import volunteer from "./volunteer"
import volunteerAdd from "./volunteerPartialAdd"
import volunteerDiscordId from "./volunteerDiscordId"
import volunteerList from "./volunteerList"
import volunteerSet from "./volunteerSet"
import volunteerLogin from "./volunteerLogin"
@@ -29,8 +29,8 @@ export default (history: History) => ({
postulantAdd,
teamList,
ui,
volunteer,
volunteerAdd,
volunteerDiscordId,
volunteerList,
volunteerSet,
volunteerLogin,

View File

@@ -1,53 +0,0 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { StateRequest, toastError, elementFetch } from "./utils"
import { Volunteer } from "../services/volunteers"
import { AppThunk, AppState } from "."
import { volunteerGet } from "../services/volunteersAccessors"
type StateVolunteer = { entity?: Volunteer } & StateRequest
export const initialState: StateVolunteer = {
readyStatus: "idle",
}
const volunteer = createSlice({
name: "volunteer",
initialState,
reducers: {
getRequesting: (_) => ({
readyStatus: "request",
}),
getSuccess: (_, { payload }: PayloadAction<Volunteer>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
readyStatus: "failure",
error: payload,
}),
},
})
export default volunteer.reducer
export const { getRequesting, getSuccess, getFailure } = volunteer.actions
export const fetchVolunteer = elementFetch(
volunteerGet,
getRequesting,
getSuccess,
getFailure,
(error: Error) => toastError(`Erreur lors du chargement d'un bénévole: ${error.message}`)
)
const shouldFetchVolunteer = (state: AppState, id: number) =>
state.volunteer.readyStatus !== "success" ||
(state.volunteer.entity && state.volunteer.entity.id !== id)
export const fetchVolunteerIfNeed =
(id: number): AppThunk =>
(dispatch, getState) => {
if (shouldFetchVolunteer(getState(), id)) return dispatch(fetchVolunteer(id))
return null
}

View File

@@ -0,0 +1,65 @@
import { PayloadAction, createSlice, createSelector } from "@reduxjs/toolkit"
import { StateRequest, toastError, elementFetch } from "./utils"
import { VolunteerDiscordId } from "../services/volunteers"
import { AppThunk, AppState } from "."
import { volunteerDiscordIdGet } from "../services/volunteersAccessors"
type StateVolunteerDiscordId = { entity?: VolunteerDiscordId } & StateRequest
export const initialState: StateVolunteerDiscordId = {
readyStatus: "idle",
}
const volunteerDiscordId = createSlice({
name: "volunteerDiscordId",
initialState,
reducers: {
getRequesting: (_) => ({
readyStatus: "request",
}),
getSuccess: (_, { payload }: PayloadAction<VolunteerDiscordId>) => ({
readyStatus: "success",
entity: payload,
}),
getFailure: (_, { payload }: PayloadAction<string>) => ({
readyStatus: "failure",
error: payload,
}),
},
})
export default volunteerDiscordId.reducer
export const { getRequesting, getSuccess, getFailure } = volunteerDiscordId.actions
export const fetchVolunteerDiscordId = elementFetch(
volunteerDiscordIdGet,
getRequesting,
getSuccess,
getFailure,
(error: Error) =>
toastError(`Erreur lors du chargement du discordId d'un bénévole: ${error.message}`)
)
const shouldFetchVolunteerDiscordId = (state: AppState, id: number) =>
state.volunteerDiscordId.readyStatus !== "success" ||
(state.volunteerDiscordId.entity && state.volunteerDiscordId.entity.id !== id)
export const fetchVolunteerDiscordIdIfNeed =
(id = 0): AppThunk =>
(dispatch, getState) => {
let jwt = ""
if (!id) {
;({ jwt, id } = getState().auth)
}
if (shouldFetchVolunteerDiscordId(getState(), id))
return dispatch(fetchVolunteerDiscordId(jwt, id))
return null
}
export const selectVolunteerDiscordId = createSelector(
(state: AppState) => state,
(state): number | undefined => state.volunteerDiscordId?.entity?.id
)