diff --git a/gsheetsTest.ts b/gsheetsTest.ts index a6c1362..abdca2e 100644 --- a/gsheetsTest.ts +++ b/gsheetsTest.ts @@ -330,20 +330,6 @@ class Test { tictactoe: boolean[] = [] } -// class Membre { -// membreId = 0 - -// nom = "" - -// prenom = "" - -// mail = "" - -// telephone = "" - -// photo = "" -// } - // Can't run it on every test, it requires private access to a google sheet async function testGSheetAPi(): Promise { const dataset: Test[] = [ diff --git a/src/components/AddEnvie/index.tsx b/src/components/AddEnvie/index.tsx index 68ceabe..27251ad 100644 --- a/src/components/AddEnvie/index.tsx +++ b/src/components/AddEnvie/index.tsx @@ -3,7 +3,7 @@ import { toast } from "react-toastify" import { AppDispatch } from "../../store" -import { postEnvie } from "../../store/envieAdd" +import { sendAddEnvie } from "../../store/envieAdd" import styles from "./styles.module.scss" interface Props { @@ -29,7 +29,7 @@ const AddEnvie = ({ dispatch }: Props) => { const onSavePostClicked = () => { if (domaine && envies) { dispatch( - postEnvie({ + sendAddEnvie({ domaine, envies, precisions, diff --git a/src/components/MembreSet/__tests__/MembreSet.tsx b/src/components/MembreSet/__tests__/MembreSet.tsx new file mode 100644 index 0000000..69b8c29 --- /dev/null +++ b/src/components/MembreSet/__tests__/MembreSet.tsx @@ -0,0 +1,37 @@ +/** + * @jest-environment jsdom + */ +import { render } from "@testing-library/react" +import { MemoryRouter } from "react-router-dom" + +import MembreSet from "../index" + +describe("", () => { + it("renders", () => { + const dispatch = jest.fn() + const tree = render( + + + + ).container.firstChild + + expect(tree).toMatchSnapshot() + }) +}) diff --git a/src/components/MembreSet/__tests__/__snapshots__/MembreSet.tsx.snap b/src/components/MembreSet/__tests__/__snapshots__/MembreSet.tsx.snap new file mode 100644 index 0000000..9cb2c96 --- /dev/null +++ b/src/components/MembreSet/__tests__/__snapshots__/MembreSet.tsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders 1`] = ` +
+

+ Modifier un membre +

+
+ + + + +
+
+`; diff --git a/src/components/MembreSet/index.tsx b/src/components/MembreSet/index.tsx new file mode 100644 index 0000000..3cb6aa2 --- /dev/null +++ b/src/components/MembreSet/index.tsx @@ -0,0 +1,87 @@ +import React, { useState, memo } from "react" +import { toast } from "react-toastify" + +import { AppDispatch } from "../../store" + +import { sendMembreSet } from "../../store/membreSet" +import { Membre } from "../../services/membres" +import styles from "./styles.module.scss" + +interface Props { + dispatch: AppDispatch + membre: Membre +} + +const MembreSet = ({ dispatch, membre }: Props) => { + const [prenom, setPrenom] = useState(membre.prenom) + const [nom, setNom] = useState(membre.nom) + const [majeur, setMajeur] = useState(membre.majeur) + + const onPrenomChanged = (e: React.ChangeEvent) => setPrenom(e.target.value) + const onNomChanged = (e: React.ChangeEvent) => setNom(e.target.value) + const onMajeurChanged = (e: React.ChangeEvent) => setMajeur(+e.target.value) + + const onSavePostClicked = () => { + if (prenom && nom) { + dispatch( + sendMembreSet({ + ...membre, + prenom, + nom, + majeur, + }) + ) + } else { + toast.warning("Il faut au moins préciser un prenom et un nom", { + position: "top-center", + autoClose: 6000, + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }) + } + } + return ( +
+

Modifier un membre

+
+ + + + +
+
+ ) +} +export default memo(MembreSet) diff --git a/src/components/MembreSet/styles.module.scss b/src/components/MembreSet/styles.module.scss new file mode 100644 index 0000000..ee7f04a --- /dev/null +++ b/src/components/MembreSet/styles.module.scss @@ -0,0 +1,17 @@ +@import "../../theme/variables"; + +.jav-game-list { + color: $color-white; + + ul { + padding-left: 17px; + + li { + margin-bottom: 0.5em; + } + } + + a { + color: $color-white; + } +} diff --git a/src/components/index.ts b/src/components/index.ts index 5696608..74c8c03 100755 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,8 +1,9 @@ import MembreList from "./MembreList" import JeuJavList from "./JeuJavList" import MembreInfo from "./MembreInfo" +import MembreSet from "./MembreSet" import ErrorBoundary from "./ErrorBoundary" import Loading from "./Loading" import AddEnvie from "./AddEnvie" -export { MembreList, JeuJavList, MembreInfo, ErrorBoundary, Loading, AddEnvie } +export { MembreList, JeuJavList, MembreInfo, MembreSet, ErrorBoundary, Loading, AddEnvie } diff --git a/src/gsheets/envies.ts b/src/gsheets/envies.ts index dcf21ee..28bf86c 100644 --- a/src/gsheets/envies.ts +++ b/src/gsheets/envies.ts @@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express" import { getList, add } from "./utils" import { Envie, EnvieWithoutId } from "../services/envies" -export const getEnvieList = async ( +export const envieListGet = async ( _request: Request, response: Response, _next: NextFunction @@ -17,13 +17,13 @@ export const getEnvieList = async ( } } -export const addEnvie = async ( +export const envieAdd = async ( request: Request, response: Response, _next: NextFunction ): Promise => { try { - const envie = await add("Envies d'aider", "id", request.body) + const envie = await add("Envies d'aider", request.body) if (envie) { response.status(200).json(envie) } diff --git a/src/gsheets/jeuJav.ts b/src/gsheets/jeuJav.ts index 8ab50f3..7c7e9cd 100644 --- a/src/gsheets/jeuJav.ts +++ b/src/gsheets/jeuJav.ts @@ -3,7 +3,7 @@ import _ from "lodash" import { getList } from "./utils" import { JeuJav } from "../services/jeuJav" -export const getJeuJavList = async ( +export const jeuJavListGet = async ( _request: Request, response: Response, _next: NextFunction @@ -18,7 +18,7 @@ export const getJeuJavList = async ( } } -export const getJeuJavData = async ( +export const jeuJavGet = async ( _request: Request, response: Response, _next: NextFunction diff --git a/src/gsheets/membres.ts b/src/gsheets/membres.ts index 2257db2..2e693fe 100644 --- a/src/gsheets/membres.ts +++ b/src/gsheets/membres.ts @@ -1,8 +1,8 @@ import { Request, Response, NextFunction } from "express" -import { getList, get, add } from "./utils" +import { getList, get, set, add } from "./utils" import { Membre, MembreWithoutId } from "../services/membres" -export const getMembreList = async ( +export const membreListGet = async ( _request: Request, response: Response, _next: NextFunction @@ -17,7 +17,7 @@ export const getMembreList = async ( } } -export const getMembre = async ( +export const membreGet = async ( request: Request, response: Response, _next: NextFunction @@ -31,13 +31,28 @@ export const getMembre = async ( } } -export const addMembre = async ( +export const membreSet = async ( request: Request, response: Response, _next: NextFunction ): Promise => { try { - const membre = await add("Membres", "membreId", request.body) + const envie = await set("Membres", request.body) + if (envie) { + response.status(200).json(envie) + } + } catch (e: unknown) { + response.status(400).json(e) + } +} + +export const membreAdd = async ( + request: Request, + response: Response, + _next: NextFunction +): Promise => { + try { + const membre = await add("Membres", request.body) if (membre) { response.status(200).json(membre) } diff --git a/src/gsheets/utils.ts b/src/gsheets/utils.ts index be3f60b..2dbb23d 100644 --- a/src/gsheets/utils.ts +++ b/src/gsheets/utils.ts @@ -101,10 +101,38 @@ export async function setList( return true } +// eslint-disable-next-line @typescript-eslint/ban-types +export async function set( + sheetName: string, + element: Element +): Promise { + if (!element) { + return undefined + } + const sheet = await getGSheet(sheetName) + + // Load sheet into an array of objects + const rows = await sheet.getRows() + if (!rows[0]) { + throw new Error(`No column types defined in sheet ${sheetName}`) + } + const types = _.pick(rows[0], Object.keys(element || {})) as Record + rows.shift() + + // Replace previous row + const stringifiedRow = stringifyElement(element, types) + const row = rows.find((rowItem) => +rowItem.id === element.id) + if (!row) { + return undefined + } + Object.assign(row, stringifiedRow) + await row.save() + return element +} + // eslint-disable-next-line @typescript-eslint/ban-types export async function add( sheetName: string, - idFieldName: string, partialElement: Partial ): Promise { if (!partialElement) { @@ -118,14 +146,17 @@ export async function add), + id: "number", + ...(_.pick(rows[0], Object.keys(partialElement || {})) as Record< + keyof ElementNoId, + string + >), } // Create full element rows.shift() - const highestId = rows.reduce((id: number, row) => Math.max(id, +row[idFieldName] || 0), 0) - const element = { [idFieldName]: highestId + 1, ...partialElement } as Element + const highestId = rows.reduce((id: number, row) => Math.max(id, +row.id || 0), 0) + const element = { id: highestId + 1, ...partialElement } as Element // Add element const stringifiedRow = stringifyElement(element, types) @@ -241,9 +272,9 @@ function parseElement( } // eslint-disable-next-line @typescript-eslint/ban-types -function stringifyElement( +function stringifyElement( element: Element, - types: Record + types: { id: string } & Record ): Record { const rawElement: Record = _.reduce( types, diff --git a/src/pages/MembrePage/MembrePage.tsx b/src/pages/MembrePage/MembrePage.tsx index 5ac2190..03ee586 100755 --- a/src/pages/MembrePage/MembrePage.tsx +++ b/src/pages/MembrePage/MembrePage.tsx @@ -5,7 +5,7 @@ import { Helmet } from "react-helmet" import { AppState, AppThunk } from "../../store" import { fetchMembreDataIfNeed } from "../../store/membre" -import { MembreInfo } from "../../components" +import { MembreInfo, MembreSet } from "../../components" import styles from "./styles.module.scss" export type Props = RouteComponentProps<{ id: string }> @@ -28,7 +28,12 @@ const MembrePage = ({ match }: Props): JSX.Element => { if (membreInfo.readyStatus === "failure" || !membreInfo.entity) return

Oops! Failed to load data.

- return + return ( +
+ + +
+ ) } return ( diff --git a/src/pages/MembrePage/__tests__/__snapshots__/MembrePage.tsx.snap b/src/pages/MembrePage/__tests__/__snapshots__/MembrePage.tsx.snap index 87be62a..2a37f8c 100644 --- a/src/pages/MembrePage/__tests__/__snapshots__/MembrePage.tsx.snap +++ b/src/pages/MembrePage/__tests__/__snapshots__/MembrePage.tsx.snap @@ -14,22 +14,71 @@ exports[` renders the if loading was successful 1`] = `
-
-

- Membre Info -

-
    -
  • - Prénom: - Amélie -
  • -
  • - Nom: - Aupeix -
  • -
+
+
+

+ Membre Info +

+
    +
  • + Prénom: + Amélie +
  • +
  • + Nom: + Aupeix +
  • +
+
+
+

+ Modifier un membre +

+
+ + + + +
+
`; diff --git a/src/server/index.ts b/src/server/index.ts index 280470c..b6550a0 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -10,9 +10,9 @@ import chalk from "chalk" import devServer from "./devServer" import ssr from "./ssr" -import { getJeuJavList } from "../gsheets/jeuJav" -import { getEnvieList, addEnvie } from "../gsheets/envies" -import { getMembre } from "../gsheets/membres" +import { jeuJavListGet } from "../gsheets/jeuJav" +import { envieListGet, envieAdd } from "../gsheets/envies" +import { membreGet, membreSet } from "../gsheets/membres" import config from "../config" const app = express() @@ -34,10 +34,11 @@ if (__DEV__) devServer(app) // Google Sheets requests app.use(express.json()) -app.get("/JeuJavList", getJeuJavList) -app.get("/GetEnvieList", getEnvieList) -app.get("/GetMembre", getMembre) -app.post("/AddEnvie", addEnvie) +app.get("/JeuJavListGet", jeuJavListGet) +app.get("/EnvieListGet", envieListGet) +app.get("/MembreGet", membreGet) +app.post("/MembreSet", membreSet) +app.post("/EnvieAdd", envieAdd) // Use React server-side rendering middleware app.get("*", ssr) diff --git a/src/services/envies.ts b/src/services/envies.ts index f032192..e04d06b 100644 --- a/src/services/envies.ts +++ b/src/services/envies.ts @@ -17,26 +17,26 @@ export class Envie { } export type EnvieWithoutId = Omit -export interface GetEnvieListResponse { +export interface EnvieListGetResponse { data?: Envie[] error?: Error } -export const getEnvieList = async (): Promise => { +export const envieListGet = async (): Promise => { try { - const { data } = await axios.get(`${config.API_URL}/GetEnvieList`) + const { data } = await axios.get(`${config.API_URL}/EnvieListGet`) return { data } } catch (error) { return { error: error as Error } } } -export interface AddEnvieResponse { +export interface EnvieAddResponse { data?: Envie error?: Error } -export const addEnvie = async (envieWithoutId: EnvieWithoutId): Promise => { +export const envieAdd = async (envieWithoutId: EnvieWithoutId): Promise => { try { - const { data } = await axios.post(`${config.API_URL}/AddEnvie`, envieWithoutId) + const { data } = await axios.post(`${config.API_URL}/EnvieAdd`, envieWithoutId) return { data } } catch (error) { return { error: error as Error } diff --git a/src/services/jeuJav.ts b/src/services/jeuJav.ts index 3a32ceb..038bade 100644 --- a/src/services/jeuJav.ts +++ b/src/services/jeuJav.ts @@ -34,28 +34,26 @@ export class JeuJav { bggPhoto = "" } -export interface JeuJavList { +export interface JeuJavListResponse { data?: JeuJav[] error?: Error } +export const getJeuJavList = async (): Promise => { + try { + const { data } = await axios.get(`${config.API_URL}/JeuJavListGet`) + return { data } + } catch (error) { + return { error: error as Error } + } +} -export interface JeuJavData { +export interface JeuJavResponse { data?: JeuJav error?: Error } - -export const getJeuJavList = async (): Promise => { +export const getJeuJavData = async (id: string): Promise => { try { - const { data } = await axios.get(`${config.API_URL}/JeuJavList`) - return { data } - } catch (error) { - return { error: error as Error } - } -} - -export const getJeuJavData = async (id: string): Promise => { - try { - const { data } = await axios.get(`${config.API_URL}/JeuJav`, { params: { id } }) + const { data } = await axios.get(`${config.API_URL}/JeuJavGet`, { params: { id } }) return { data } } catch (error) { return { error: error as Error } diff --git a/src/services/membres.ts b/src/services/membres.ts index cece7f0..de0e238 100644 --- a/src/services/membres.ts +++ b/src/services/membres.ts @@ -29,41 +29,54 @@ export class Membre { passe = "" } -export type MembreWithoutId = Omit +export type MembreWithoutId = Omit -export interface GetMembreListResponse { +export interface MembreListGetResponse { data?: Membre[] error?: Error } -export const getMembreList = async (): Promise => { +export const membreListGet = async (): Promise => { try { - const { data } = await axios.get(`${config.API_URL}/GetMembreList`) + const { data } = await axios.get(`${config.API_URL}/MembreListGet`) return { data } } catch (error) { return { error: error as Error } } } -export interface GetMembreResponse { +export interface MembreGetResponse { data?: Membre error?: Error } -export const getMembre = async (id: number): Promise => { +export const membreGet = async (id: number): Promise => { try { - const { data } = await axios.get(`${config.API_URL}/GetMembre`, { params: { id } }) + const { data } = await axios.get(`${config.API_URL}/MembreGet`, { params: { id } }) return { data } } catch (error) { return { error: error as Error } } } -export interface AddMembreResponse { +export interface MembreSetResponse { data?: Membre error?: Error } -export const addMembre = async (membreWithoutId: MembreWithoutId): Promise => { +export const membreSet = async (membre: Membre): Promise => { try { - const { data } = await axios.post(`${config.API_URL}/AddMembre`, membreWithoutId) + const { data } = await axios.post(`${config.API_URL}/MembreSet`, membre) + return { data } + } catch (error) { + return { error: error as Error } + } +} + +export interface MembreAddResponse { + data?: Membre + error?: Error +} +export const membreAdd = async (membreWithoutId: MembreWithoutId): Promise => { + try { + const { data } = await axios.post(`${config.API_URL}/MembreAdd`, membreWithoutId) return { data } } catch (error) { return { error: error as Error } diff --git a/src/store/envieAdd.ts b/src/store/envieAdd.ts index 03fbaff..28f6591 100644 --- a/src/store/envieAdd.ts +++ b/src/store/envieAdd.ts @@ -2,14 +2,14 @@ import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolki import { toast } from "react-toastify" import { StateRequest } from "./utils" -import { Envie, EnvieWithoutId, addEnvie } from "../services/envies" +import { Envie, EnvieWithoutId, envieAdd } from "../services/envies" import { AppThunk } from "." const envieAdapter = createEntityAdapter({ selectId: (envie) => envie.id, }) -const envieAdd = createSlice({ +const envieAddSlice = createSlice({ name: "addEnvie", initialState: envieAdapter.getInitialState({ readyStatus: "idle", @@ -29,15 +29,15 @@ const envieAdd = createSlice({ }, }) -export default envieAdd.reducer -export const { getRequesting, getSuccess, getFailure } = envieAdd.actions +export default envieAddSlice.reducer +export const { getRequesting, getSuccess, getFailure } = envieAddSlice.actions -export const postEnvie = +export const sendAddEnvie = (envieWithoutId: EnvieWithoutId): AppThunk => async (dispatch) => { dispatch(getRequesting()) - const { error, data } = await addEnvie(envieWithoutId) + const { error, data } = await envieAdd(envieWithoutId) if (error) { dispatch(getFailure(error.message)) diff --git a/src/store/envieList.ts b/src/store/envieList.ts index 807e6b8..9f14eae 100644 --- a/src/store/envieList.ts +++ b/src/store/envieList.ts @@ -2,7 +2,7 @@ import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolki import { toast } from "react-toastify" import { StateRequest } from "./utils" -import { Envie, getEnvieList } from "../services/envies" +import { Envie, envieListGet } from "../services/envies" import { AppThunk, AppState } from "." const envieAdapter = createEntityAdapter({ @@ -35,7 +35,7 @@ export const { getRequesting, getSuccess, getFailure } = envieList.actions export const fetchEnvieList = (): AppThunk => async (dispatch) => { dispatch(getRequesting()) - const { error, data } = await getEnvieList() + const { error, data } = await envieListGet() if (error) { dispatch(getFailure(error.message)) diff --git a/src/store/membre.ts b/src/store/membre.ts index fc32ef4..2c569ca 100644 --- a/src/store/membre.ts +++ b/src/store/membre.ts @@ -2,7 +2,7 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit" import { toast } from "react-toastify" import { StateRequest } from "./utils" -import { Membre, getMembre } from "../services/membres" +import { Membre, membreGet } from "../services/membres" import { AppThunk, AppState } from "." type StateMembre = { entity?: Membre } & StateRequest @@ -37,7 +37,7 @@ export const fetchMembreData = async (dispatch) => { dispatch(getRequesting()) - const { error, data } = await getMembre(id) + const { error, data } = await membreGet(id) if (error) { dispatch(getFailure(error.message)) diff --git a/src/store/membreList.ts b/src/store/membreList.ts index 7cdcc97..f5f6d67 100644 --- a/src/store/membreList.ts +++ b/src/store/membreList.ts @@ -2,7 +2,7 @@ import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolki import { toast } from "react-toastify" import { StateRequest } from "./utils" -import { Membre, getMembreList } from "../services/membres" +import { Membre, membreListGet } from "../services/membres" import { AppThunk, AppState } from "." const membreAdapter = createEntityAdapter() @@ -35,7 +35,7 @@ export const { getRequesting, getSuccess, getFailure } = membreList.actions export const fetchMembreList = (): AppThunk => async (dispatch) => { dispatch(getRequesting()) - const { error, data } = await getMembreList() + const { error, data } = await membreListGet() if (error) { dispatch(getFailure(error.message)) diff --git a/src/store/membreSet.ts b/src/store/membreSet.ts new file mode 100644 index 0000000..7435b4c --- /dev/null +++ b/src/store/membreSet.ts @@ -0,0 +1,65 @@ +import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit" +import { toast } from "react-toastify" + +import { StateRequest } from "./utils" +import { Membre, membreSet } from "../services/membres" +import { AppThunk } from "." + +const membreAdapter = createEntityAdapter({ + selectId: (membre) => membre.id, +}) + +const membreSetSlice = createSlice({ + name: "membreSet", + initialState: membreAdapter.getInitialState({ + readyStatus: "idle", + } as StateRequest), + reducers: { + getRequesting: (state) => { + state.readyStatus = "request" + }, + getSuccess: (state, { payload }: PayloadAction) => { + state.readyStatus = "success" + membreAdapter.addOne(state, payload) + }, + getFailure: (state, { payload }: PayloadAction) => { + state.readyStatus = "failure" + state.error = payload + }, + }, +}) + +export default membreSetSlice.reducer +export const { getRequesting, getSuccess, getFailure } = membreSetSlice.actions + +export const sendMembreSet = + (membre: Membre): AppThunk => + async (dispatch) => { + dispatch(getRequesting()) + + const { error, data } = await membreSet(membre) + + if (error) { + dispatch(getFailure(error.message)) + toast.error(`Erreur lors de la modification d'un membre: ${error.message}`, { + position: "top-center", + autoClose: 6000, + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }) + } else { + dispatch(getSuccess(data as Membre)) + toast.success("Membre modifié !", { + position: "top-center", + autoClose: 3000, + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }) + } + }