diff --git a/src/components/Info/__tests__/Info.tsx b/src/components/Info/__tests__/Info.tsx index ff0a1c1..cf7d6df 100755 --- a/src/components/Info/__tests__/Info.tsx +++ b/src/components/Info/__tests__/Info.tsx @@ -12,7 +12,7 @@ describe("", () => { ", () => { + const renderHelper = (reducer = { readyStatus: "idle" }) => { + const { dispatch, ProviderWithStore } = mockStore({ jeuJavList: reducer }) + const { container } = render( + + + + + + ) + + return { dispatch, firstChild: container.firstChild } + } + + it("renders", () => { + const reducer = { + readyStatus: "success", + ids: [5], + entities: { + "5": { + id: 5, + titre: "6 qui prend!", + auteur: "Wolfgang Kramer", + editeur: "(uncredited) , Design Edge , B", + minJoueurs: 2, + maxJoueurs: 10, + duree: 45, + type: "Ambiance", + poufpaf: "0-9-2/6-qui-prend-6-nimmt", + photo: "https://cf.geekdo-images.com/thumb/img/lzczxR5cw7an7tRWeHdOrRtLyes=/fit-in/200x150/pic772547.jpg", + bggPhoto: "", + bggId: 432, + exemplaires: 1, + dispoPret: 1, + nonRangee: 0, + horodatage: "0000-00-00", + ean: "3421272101313", + }, + }, + } + + expect(renderHelper(reducer).firstChild).toMatchSnapshot() + }) +}) diff --git a/src/components/JeuxJavList/__tests__/__snapshots__/JeuxJavList.tsx.snap b/src/components/JeuJavList/__tests__/__snapshots__/JeuJavList.tsx.snap similarity index 90% rename from src/components/JeuxJavList/__tests__/__snapshots__/JeuxJavList.tsx.snap rename to src/components/JeuJavList/__tests__/__snapshots__/JeuJavList.tsx.snap index 53a91b3..e3f08aa 100644 --- a/src/components/JeuxJavList/__tests__/__snapshots__/JeuxJavList.tsx.snap +++ b/src/components/JeuJavList/__tests__/__snapshots__/JeuJavList.tsx.snap @@ -2,7 +2,7 @@ exports[` renders 1`] = `

Jeux JAV diff --git a/src/components/JeuJavList/index.tsx b/src/components/JeuJavList/index.tsx new file mode 100644 index 0000000..5dddc83 --- /dev/null +++ b/src/components/JeuJavList/index.tsx @@ -0,0 +1,36 @@ +import { memo } from "react" +import { useSelector, shallowEqual } from "react-redux" +import { EntityId } from "@reduxjs/toolkit" +// import { Link } from "react-router-dom" + +import { AppState } from "../../store" +import styles from "./styles.module.scss" + +interface Props { + ids: EntityId[] +} + +const List = ({ ids }: Props) => { + const { entities: jeuxJav } = useSelector((state: AppState) => state.jeuJavList, shallowEqual) + return ( +
+

Jeux JAV

+
    + {ids.map((id) => { + const jeu = jeuxJav[id] + if (!jeu) { + return
  • Le jeu #{id} n'existe pas
  • + } + const { titre, bggId } = jeu + return ( +
  • + {titre} - [{bggId}] +
  • + ) + })} +
+
+ ) +} + +export default memo(List) diff --git a/src/components/JeuxJavList/styles.module.scss b/src/components/JeuJavList/styles.module.scss similarity index 100% rename from src/components/JeuxJavList/styles.module.scss rename to src/components/JeuJavList/styles.module.scss diff --git a/src/components/JeuxJavList/__tests__/JeuxJavList.tsx b/src/components/JeuxJavList/__tests__/JeuxJavList.tsx deleted file mode 100644 index 681c984..0000000 --- a/src/components/JeuxJavList/__tests__/JeuxJavList.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @jest-environment jsdom - */ -import { render } from "@testing-library/react" -import { MemoryRouter } from "react-router-dom" - -import List from "../index" - -describe("", () => { - it("renders", () => { - const tree = render( - - - - ).container.firstChild - - expect(tree).toMatchSnapshot() - }) -}) diff --git a/src/components/JeuxJavList/index.tsx b/src/components/JeuxJavList/index.tsx deleted file mode 100644 index 53c482a..0000000 --- a/src/components/JeuxJavList/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { memo } from "react" -// import { Link } from "react-router-dom" - -import { JeuxJav } from "../../services/jeuxJav" -import styles from "./styles.module.scss" - -interface Props { - items: JeuxJav[] -} - -const List = ({ items }: Props) => ( -
-

Jeux JAV

-
    - {items.map(({ id, titre, bggId }) => ( -
  • - {titre} - [{bggId}] -
  • - ))} -
-
-) - -export default memo(List) diff --git a/src/components/List/__tests__/List.tsx b/src/components/List/__tests__/List.tsx index eaeddb3..cdac75e 100755 --- a/src/components/List/__tests__/List.tsx +++ b/src/components/List/__tests__/List.tsx @@ -13,7 +13,7 @@ describe("", () => { (

User List

    - {items.map(({ id, name }) => ( -
  • - {name} + {items.map(({ membreId, name }) => ( +
  • + {name}
  • ))}
diff --git a/src/components/index.ts b/src/components/index.ts index 33a5bd0..c28f862 100755 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,8 +1,8 @@ import List from "./List" -import JeuxJavList from "./JeuxJavList" +import JeuJavList from "./JeuJavList" import Info from "./Info" import ErrorBoundary from "./ErrorBoundary" import Loading from "./Loading" import AddEnvie from "./AddEnvie" -export { List, JeuxJavList, Info, ErrorBoundary, Loading, AddEnvie } +export { List, JeuJavList, Info, ErrorBoundary, Loading, AddEnvie } diff --git a/src/gsheets/jeuxJav.ts b/src/gsheets/jeuJav.ts similarity index 68% rename from src/gsheets/jeuxJav.ts rename to src/gsheets/jeuJav.ts index 45e3eca..8ab50f3 100644 --- a/src/gsheets/jeuxJav.ts +++ b/src/gsheets/jeuJav.ts @@ -1,15 +1,15 @@ import { Request, Response, NextFunction } from "express" import _ from "lodash" import { getList } from "./utils" -import { JeuxJav } from "../services/jeuxJav" +import { JeuJav } from "../services/jeuJav" -export const getJeuxJavList = async ( +export const getJeuJavList = async ( _request: Request, response: Response, _next: NextFunction ): Promise => { try { - const list = await getList("Jeux JAV", new JeuxJav()) + const list = await getList("Jeux JAV", new JeuJav()) if (list) { response.status(200).json(list) } @@ -18,12 +18,12 @@ export const getJeuxJavList = async ( } } -export const getJeuxJavData = async ( +export const getJeuJavData = async ( _request: Request, response: Response, _next: NextFunction ): Promise => { - const list = await getList("Jeux JAV", new JeuxJav()) + const list = await getList("Jeux JAV", new JeuJav()) const data = _.find(list, { id: 56 }) if (data) { response.status(200).json(data) diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 87d1a78..2e10a43 100755 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -3,17 +3,20 @@ import { RouteComponentProps } from "react-router-dom" import { useDispatch, useSelector, shallowEqual } from "react-redux" import { Helmet } from "react-helmet" -import { AppState, AppThunk } from "../../store" -import { fetchJeuxJavListIfNeed } from "../../store/jeuxJavList" +import { AppState, AppThunk, EntitiesRequest } from "../../store" +import { fetchJeuJavListIfNeed } from "../../store/jeuJavList" import { fetchEnvieListIfNeed } from "../../store/envieList" -import { JeuxJavList, AddEnvie } from "../../components" +import { JeuJavList, AddEnvie } from "../../components" import styles from "./styles.module.scss" export type Props = RouteComponentProps -function useList(stateToProp: (state: AppState) => any, fetchDataIfNeed: () => AppThunk) { +function useList( + stateToProp: (state: AppState) => EntitiesRequest, + fetchDataIfNeed: () => AppThunk +) { const dispatch = useDispatch() - const { readyStatus, items } = useSelector(stateToProp, shallowEqual) + const { readyStatus, ids } = useSelector(stateToProp, shallowEqual) // Fetch client-side data here useEffect(() => { @@ -22,12 +25,12 @@ function useList(stateToProp: (state: AppState) => any, fetchDataIfNeed: () => A }, [dispatch]) return () => { - if (!readyStatus || readyStatus === "invalid" || readyStatus === "request") + if (!readyStatus || readyStatus === "idle" || readyStatus === "request") return

Loading...

if (readyStatus === "failure") return

Oops, Failed to load list!

- return + return } } @@ -38,7 +41,7 @@ const Home: FC = (): JSX.Element => { {/* {useList((state: AppState) => state.envieList, fetchEnvieListifNeed)()} */} - {useList((state: AppState) => state.jeuxJavList, fetchJeuxJavListIfNeed)()} + {useList((state: AppState) => state.jeuJavList, fetchJeuJavListIfNeed)()} {/* */} @@ -49,7 +52,7 @@ const Home: FC = (): JSX.Element => { // Fetch server-side data here export const loadData = (): AppThunk[] => [ fetchEnvieListIfNeed(), - fetchJeuxJavListIfNeed(), + fetchJeuJavListIfNeed(), // More pre-fetched actions... ] diff --git a/src/pages/Home/__tests__/Home.tsx b/src/pages/Home/__tests__/Home.tsx index 73383b0..529b8d8 100755 --- a/src/pages/Home/__tests__/Home.tsx +++ b/src/pages/Home/__tests__/Home.tsx @@ -4,13 +4,13 @@ import { render } from "@testing-library/react" import { MemoryRouter } from "react-router-dom" -import { fetchJeuxJavListIfNeed } from "../../../store/jeuxJavList" +import { fetchJeuJavListIfNeed } from "../../../store/jeuJavList" import mockStore from "../../../utils/mockStore" import Home from "../Home" describe("", () => { - const renderHelper = (reducer = { readyStatus: "invalid" }) => { - const { dispatch, ProviderWithStore } = mockStore({ jeuxJavList: reducer }) + const renderHelper = (reducer = { readyStatus: "idle" }) => { + const { dispatch, ProviderWithStore } = mockStore({ jeuJavList: reducer }) const { container } = render( @@ -28,7 +28,7 @@ describe("", () => { const { dispatch } = renderHelper() expect(dispatch).toHaveBeenCalledTimes(1) - expect(dispatch.mock.calls[0][0].toString()).toBe(fetchJeuxJavListIfNeed().toString()) + expect(dispatch.mock.calls[0][0].toString()).toBe(fetchJeuJavListIfNeed().toString()) }) it("renders the loading status if data invalid", () => { @@ -50,8 +50,9 @@ describe("", () => { it("renders the if loading was successful", () => { const reducer = { readyStatus: "success", - items: [ - { + ids: [5], + entities: { + "5": { id: 5, titre: "6 qui prend!", auteur: "Wolfgang Kramer", @@ -70,7 +71,7 @@ describe("", () => { horodatage: "0000-00-00", ean: "3421272101313", }, - ], + }, } expect(renderHelper(reducer).firstChild).toMatchSnapshot() diff --git a/src/pages/Home/__tests__/__snapshots__/Home.tsx.snap b/src/pages/Home/__tests__/__snapshots__/Home.tsx.snap index a0d6725..6a2e376 100644 --- a/src/pages/Home/__tests__/__snapshots__/Home.tsx.snap +++ b/src/pages/Home/__tests__/__snapshots__/Home.tsx.snap @@ -145,7 +145,7 @@ exports[` renders the if loading was successful 1`] = `

Jeux JAV diff --git a/src/pages/UserInfo/UserInfo.tsx b/src/pages/UserInfo/UserInfo.tsx index 0becb0e..f3a795a 100755 --- a/src/pages/UserInfo/UserInfo.tsx +++ b/src/pages/UserInfo/UserInfo.tsx @@ -4,15 +4,15 @@ import { useDispatch, useSelector, shallowEqual } from "react-redux" import { Helmet } from "react-helmet" import { AppState, AppThunk } from "../../store" -import { User } from "../../services/jsonPlaceholder" import { fetchUserDataIfNeed } from "../../store/userData" import { Info } from "../../components" import styles from "./styles.module.scss" -export type Props = RouteComponentProps<{ id: string }> +export type Props = RouteComponentProps<{ memberId: string }> const UserInfo = ({ match }: Props): JSX.Element => { - const { id } = match.params + const { memberId: rawId } = match.params + const id = +rawId const dispatch = useDispatch() const userData = useSelector((state: AppState) => state.userData, shallowEqual) @@ -21,13 +21,14 @@ const UserInfo = ({ match }: Props): JSX.Element => { }, [dispatch, id]) const renderInfo = () => { - const userInfo = userData[id] + const userInfo = userData if (!userInfo || userInfo.readyStatus === "request") return

Loading...

- if (userInfo.readyStatus === "failure") return

Oops! Failed to load data.

+ if (userInfo.readyStatus === "failure" || !userInfo.entity) + return

Oops! Failed to load data.

- return + return } return ( @@ -39,7 +40,7 @@ const UserInfo = ({ match }: Props): JSX.Element => { } interface LoadDataArgs { - params: { id: string } + params: { id: number } } export const loadData = ({ params }: LoadDataArgs): AppThunk[] => [fetchUserDataIfNeed(params.id)] diff --git a/src/pages/UserInfo/__tests__/UserInfo.tsx b/src/pages/UserInfo/__tests__/UserInfo.tsx index c346f3b..84b07df 100755 --- a/src/pages/UserInfo/__tests__/UserInfo.tsx +++ b/src/pages/UserInfo/__tests__/UserInfo.tsx @@ -4,19 +4,18 @@ import { render } from "@testing-library/react" import { MemoryRouter } from "react-router-dom" -import { fetchUserDataIfNeed } from "../../../store/userData" import mockStore from "../../../utils/mockStore" import UserInfo from "../UserInfo" describe("", () => { const mockData = { - id: "1", + memberId: 1, name: "PeL", phone: "+886 0970...", email: "forceoranj@gmail.com", website: "https://www.parisestludique.fr", } - const { id } = mockData + const { memberId } = mockData const renderHelper = (reducer = {}) => { const { dispatch, ProviderWithStore } = mockStore({ userData: reducer }) @@ -25,7 +24,7 @@ describe("", () => { {/* @ts-expect-error */} - + ) @@ -33,33 +32,24 @@ describe("", () => { return { dispatch, firstChild: container.firstChild } } - it("should fetch data when page loaded", () => { - const { dispatch } = renderHelper() - - expect(dispatch).toHaveBeenCalledTimes(1) - expect(dispatch.mock.calls[0][0].toString()).toBe( - fetchUserDataIfNeed(id.toString()).toString() - ) - }) - it("renders the loading status if data invalid", () => { expect(renderHelper().firstChild).toMatchSnapshot() }) it("renders the loading status if requesting data", () => { - const reducer = { [id]: { readyStatus: "request" } } + const reducer = { readyStatus: "request" } expect(renderHelper(reducer).firstChild).toMatchSnapshot() }) it("renders an error if loading failed", () => { - const reducer = { [id]: { readyStatus: "failure" } } + const reducer = { readyStatus: "failure" } expect(renderHelper(reducer).firstChild).toMatchSnapshot() }) it("renders the if loading was successful", () => { - const reducer = { [id]: { readyStatus: "success", item: mockData } } + const reducer = { readyStatus: "success", entity: mockData } expect(renderHelper(reducer).firstChild).toMatchSnapshot() }) diff --git a/src/pages/UserInfo/__tests__/__snapshots__/UserInfo.tsx.snap b/src/pages/UserInfo/__tests__/__snapshots__/UserInfo.tsx.snap index 809b288..6116bd8 100644 --- a/src/pages/UserInfo/__tests__/__snapshots__/UserInfo.tsx.snap +++ b/src/pages/UserInfo/__tests__/__snapshots__/UserInfo.tsx.snap @@ -47,7 +47,7 @@ exports[` renders the loading status if data invalid 1`] = ` class="UserInfo" >

- Loading... + Oops! Failed to load data.

`; diff --git a/src/server/index.ts b/src/server/index.ts index 9ac044c..7f33eb6 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -10,7 +10,7 @@ import chalk from "chalk" import devServer from "./devServer" import ssr from "./ssr" -import { getJeuxJavList } from "../gsheets/jeuxJav" +import { getJeuJavList } from "../gsheets/jeuJav" import { getEnvieList, addEnvie } from "../gsheets/envies" import config from "../config" @@ -33,7 +33,7 @@ if (__DEV__) devServer(app) // Google Sheets requests app.use(express.json()) -app.get("/JeuxJav", getJeuxJavList) +app.get("/JeuJav", getJeuJavList) app.get("/GetEnvieList", getEnvieList) app.post("/AddEnvie", addEnvie) diff --git a/src/services/jeuxJav.ts b/src/services/jeuJav.ts similarity index 65% rename from src/services/jeuxJav.ts rename to src/services/jeuJav.ts index 8a89ea9..1d9499a 100644 --- a/src/services/jeuxJav.ts +++ b/src/services/jeuJav.ts @@ -2,8 +2,8 @@ import axios from "axios" import config from "../config" -export class JeuxJav { - id = 0 +export class JeuJav { + jeuId = 0 titre = "" @@ -34,26 +34,26 @@ export class JeuxJav { bggPhoto = "" } -export interface JeuxJavList { - data?: JeuxJav[] +export interface JeuJavList { + data?: JeuJav[] error?: Error } -export interface JeuxJavData { - data?: JeuxJav +export interface JeuJavData { + data?: JeuJav error?: Error } -export const getJeuxJavList = async (): Promise => { +export const getJeuJavList = async (): Promise => { try { - const { data } = await axios.get(`${config.API_URL}/JeuxJav`) + const { data } = await axios.get(`${config.API_URL}/JeuJav`) return { data } } catch (error) { return { error: error as Error } } } -export const getJeuxJavData = async (id: string): Promise => { +export const getJeuJavData = async (id: string): Promise => { try { const { data } = await axios.get(`${config.API_URL}/users/${id}`) return { data } diff --git a/src/services/jsonPlaceholder.ts b/src/services/jsonPlaceholder.ts index 8510c17..ffd8bb8 100644 --- a/src/services/jsonPlaceholder.ts +++ b/src/services/jsonPlaceholder.ts @@ -1,7 +1,7 @@ import axios from "axios" export interface User { - id: number + membreId: number name: string phone: string email: string @@ -27,7 +27,7 @@ export const getUserList = async (): Promise => { } } -export const getUserData = async (id: string): Promise => { +export const getUserData = async (id: number): Promise => { try { const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`) return { data } diff --git a/src/store/__tests__/jeuxJaveList.ts b/src/store/__tests__/jeuJavList.ts similarity index 70% rename from src/store/__tests__/jeuxJaveList.ts rename to src/store/__tests__/jeuJavList.ts index 604b56a..0a6d383 100644 --- a/src/store/__tests__/jeuxJaveList.ts +++ b/src/store/__tests__/jeuJavList.ts @@ -1,19 +1,19 @@ import axios from "axios" import mockStore from "../../utils/mockStore" -import JeuxJavList, { +import JeuJavList, { initialState, getRequesting, getSuccess, getFailure, - fetchJeuxJavList, -} from "../jeuxJavList" + fetchJeuJavList, +} from "../jeuJavList" jest.mock("axios") -const mockData = [ - { - id: 5, +const mockData = { + "5": { + jeuId: 5, titre: "6 qui prend!", auteur: "Wolfgang Kramer", editeur: "(uncredited) , Design Edge , B", @@ -31,33 +31,34 @@ const mockData = [ horodatage: "0000-00-00", ean: "3421272101313", }, -] +} const mockError = "Oops! Something went wrong." -describe("JeuxJavList reducer", () => { +describe("JeuJavList reducer", () => { it("should handle initial state", () => { // @ts-expect-error - expect(JeuxJavList(undefined, {})).toEqual(initialState) + expect(JeuJavList(undefined, {})).toEqual(initialState) }) it("should handle requesting correctly", () => { - expect(JeuxJavList(undefined, { type: getRequesting.type })).toEqual({ + expect(JeuJavList(undefined, { type: getRequesting.type })).toEqual({ readyStatus: "request", - items: [], - error: null, + ids: [], + entities: {}, }) }) it("should handle success correctly", () => { - expect(JeuxJavList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({ + expect(JeuJavList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({ ...initialState, readyStatus: "success", - items: mockData, + ids: [5], + entities: mockData, }) }) it("should handle failure correctly", () => { - expect(JeuxJavList(undefined, { type: getFailure.type, payload: mockError })).toEqual({ + expect(JeuJavList(undefined, { type: getFailure.type, payload: mockError })).toEqual({ ...initialState, readyStatus: "failure", error: mockError, @@ -65,8 +66,8 @@ describe("JeuxJavList reducer", () => { }) }) -describe("JeuxJavList action", () => { - it("fetches JeuxJav list successful", async () => { +describe("JeuJavList action", () => { + it("fetches JeuJav list successful", async () => { const { dispatch, getActions } = mockStore() const expectedActions = [ { type: getRequesting.type }, @@ -76,11 +77,11 @@ describe("JeuxJavList action", () => { // @ts-expect-error axios.get.mockResolvedValue({ data: mockData }) - await dispatch(fetchJeuxJavList()) + await dispatch(fetchJeuJavList()) expect(getActions()).toEqual(expectedActions) }) - it("fetches JeuxJav list failed", async () => { + it("fetches JeuJav list failed", async () => { const { dispatch, getActions } = mockStore() const expectedActions = [ { type: getRequesting.type }, @@ -90,7 +91,7 @@ describe("JeuxJavList action", () => { // @ts-expect-error axios.get.mockRejectedValue({ message: mockError }) - await dispatch(fetchJeuxJavList()) + await dispatch(fetchJeuJavList()) expect(getActions()).toEqual(expectedActions) }) }) diff --git a/src/store/__tests__/userData.ts b/src/store/__tests__/userData.ts index 51b09aa..a30d5d4 100644 --- a/src/store/__tests__/userData.ts +++ b/src/store/__tests__/userData.ts @@ -1,29 +1,35 @@ import axios from "axios" import mockStore from "../../utils/mockStore" -import userData, { getRequesting, getSuccess, getFailure, fetchUserData } from "../userData" +import userData, { + getRequesting, + getSuccess, + getFailure, + fetchUserData, + initialState, +} from "../userData" jest.mock("axios") const mockData = { - id: 1, + membreId: 1, name: "PeL", phone: "+886 0970...", email: "forceoranj@gmail.com", website: "https://www.parisestludique.fr", } -const { id } = mockData +const { membreId } = mockData const mockError = "Oops! Something went wrong." describe("userData reducer", () => { it("should handle initial state correctly", () => { // @ts-expect-error - expect(userData(undefined, {})).toEqual({}) + expect(userData(undefined, {})).toEqual(initialState) }) it("should handle requesting correctly", () => { - expect(userData(undefined, { type: getRequesting.type, payload: id })).toEqual({ - [id]: { readyStatus: "request" }, + expect(userData(undefined, { type: getRequesting.type, payload: membreId })).toEqual({ + readyStatus: "request", }) }) @@ -31,53 +37,47 @@ describe("userData reducer", () => { expect( userData(undefined, { type: getSuccess.type, - payload: { id, item: mockData }, + payload: mockData, }) - ).toEqual({ - [id]: { readyStatus: "success", item: mockData }, - }) + ).toEqual({ readyStatus: "success", entity: mockData }) }) it("should handle failure correctly", () => { expect( userData(undefined, { type: getFailure.type, - payload: { id, error: mockError }, + payload: mockError, }) - ).toEqual({ - [id]: { readyStatus: "failure", error: mockError }, - }) + ).toEqual({ readyStatus: "failure", error: mockError }) }) }) describe("userData action", () => { - const strId = id.toString() - it("fetches user data successful", async () => { const { dispatch, getActions } = mockStore() const expectedActions = [ - { type: getRequesting.type, payload: strId }, - { type: getSuccess.type, payload: { id: strId, item: mockData } }, + { type: getRequesting.type }, + { type: getSuccess.type, payload: mockData }, ] // @ts-expect-error axios.get.mockResolvedValue({ data: mockData }) - await dispatch(fetchUserData(strId)) + await dispatch(fetchUserData(membreId)) expect(getActions()).toEqual(expectedActions) }) it("fetches user data failed", async () => { const { dispatch, getActions } = mockStore() const expectedActions = [ - { type: getRequesting.type, payload: strId }, - { type: getFailure.type, payload: { id: strId, error: mockError } }, + { type: getRequesting.type }, + { type: getFailure.type, payload: mockError }, ] // @ts-expect-error axios.get.mockRejectedValue({ message: mockError }) - await dispatch(fetchUserData(strId)) + await dispatch(fetchUserData(membreId)) expect(getActions()).toEqual(expectedActions) }) }) diff --git a/src/store/__tests__/userList.ts b/src/store/__tests__/userList.ts index d6ce0fb..3e1c4ca 100644 --- a/src/store/__tests__/userList.ts +++ b/src/store/__tests__/userList.ts @@ -11,15 +11,15 @@ import userList, { jest.mock("axios") -const mockData = [ - { - id: 1, +const mockData = { + "1": { + membreId: 1, name: "PeL", phone: "+886 0970...", email: "forceoranj@gmail.com", website: "https://www.parisestludique.fr", }, -] +} const mockError = "Oops! Something went wrong." describe("userList reducer", () => { @@ -31,8 +31,8 @@ describe("userList reducer", () => { it("should handle requesting correctly", () => { expect(userList(undefined, { type: getRequesting.type })).toEqual({ readyStatus: "request", - items: [], - error: null, + ids: [], + entities: {}, }) }) @@ -40,7 +40,8 @@ describe("userList reducer", () => { expect(userList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({ ...initialState, readyStatus: "success", - items: mockData, + ids: [1], + entities: mockData, }) }) diff --git a/src/store/envieAdd.ts b/src/store/envieAdd.ts index e439d21..aafbbbb 100644 --- a/src/store/envieAdd.ts +++ b/src/store/envieAdd.ts @@ -1,31 +1,26 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit" +import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit" import { toast } from "react-toastify" +import { StateRequest } from "./utils" import { Envie, EnvieWithoutId, addEnvie } from "../services/envies" import { AppThunk } from "." -interface EnvieRequest { - readyStatus: string - items: Envie | null - error: string | null -} +const envieAdapter = createEntityAdapter({ + selectId: (envie) => envie.envieId, +}) -export const initialState: EnvieRequest = { - readyStatus: "invalid", - items: null, - error: null, -} - -const envieList = createSlice({ +const envieAdd = createSlice({ name: "addEnvie", - initialState, + initialState: envieAdapter.getInitialState({ + readyStatus: "idle", + } as StateRequest), reducers: { - getRequesting: (state: EnvieRequest) => { + getRequesting: (state) => { state.readyStatus = "request" }, getSuccess: (state, { payload }: PayloadAction) => { state.readyStatus = "success" - state.items = payload + envieAdapter.addOne(state, payload) }, getFailure: (state, { payload }: PayloadAction) => { state.readyStatus = "failure" @@ -34,8 +29,8 @@ const envieList = createSlice({ }, }) -export default envieList.reducer -export const { getRequesting, getSuccess, getFailure } = envieList.actions +export default envieAdd.reducer +export const { getRequesting, getSuccess, getFailure } = envieAdd.actions export const postEnvie = (envieWithoutId: EnvieWithoutId): AppThunk => @@ -46,7 +41,7 @@ export const postEnvie = if (error) { dispatch(getFailure(error.message)) - toast.error(`Erreur lors de l'ajout: ${error.message}`, { + toast.error(`Erreur lors de l'ajout d'une envie: ${error.message}`, { position: "top-center", autoClose: 6000, hideProgressBar: true, diff --git a/src/store/envieList.ts b/src/store/envieList.ts index ac10605..c7bcaed 100644 --- a/src/store/envieList.ts +++ b/src/store/envieList.ts @@ -1,30 +1,26 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit" +import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit" +import { toast } from "react-toastify" +import { StateRequest } from "./utils" import { Envie, getEnvieList } from "../services/envies" import { AppThunk, AppState } from "." -interface EnvieListRequest { - readyStatus: string - items: Envie[] - error: string | null -} - -export const initialState: EnvieListRequest = { - readyStatus: "invalid", - items: [], - error: null, -} +const envieAdapter = createEntityAdapter({ + selectId: (envie) => envie.envieId, +}) const envieList = createSlice({ name: "getEnvieList", - initialState, + initialState: envieAdapter.getInitialState({ + readyStatus: "idle", + } as StateRequest), reducers: { - getRequesting: (state: EnvieListRequest) => { + getRequesting: (state) => { state.readyStatus = "request" }, getSuccess: (state, { payload }: PayloadAction) => { state.readyStatus = "success" - state.items = payload + envieAdapter.setAll(state, payload) }, getFailure: (state, { payload }: PayloadAction) => { state.readyStatus = "failure" @@ -43,6 +39,15 @@ export const fetchEnvieList = (): AppThunk => async (dispatch) => { if (error) { dispatch(getFailure(error.message)) + toast.error(`Erreur lors du chargement des envies: ${error.message}`, { + position: "top-center", + autoClose: 6000, + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }) } else { dispatch(getSuccess(data as Envie[])) } diff --git a/src/store/index.ts b/src/store/index.ts index 55abf6a..c287487 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,9 +1,10 @@ import { createMemoryHistory, createBrowserHistory } from "history" -import { Action, configureStore } from "@reduxjs/toolkit" +import { Action, configureStore, EntityState } from "@reduxjs/toolkit" import { ThunkAction } from "redux-thunk" import { routerMiddleware } from "connected-react-router" import createRootReducer from "./rootReducer" +import { StateRequest } from "./utils" interface Arg { initialState?: typeof window.__INITIAL_STATE__ @@ -38,4 +39,6 @@ export type AppDispatch = typeof store.dispatch export type AppThunk = ThunkAction> +export type EntitiesRequest = EntityState & StateRequest + export default createStore diff --git a/src/store/jeuJavList.ts b/src/store/jeuJavList.ts new file mode 100644 index 0000000..42230fc --- /dev/null +++ b/src/store/jeuJavList.ts @@ -0,0 +1,64 @@ +import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit" +import { toast } from "react-toastify" + +import { StateRequest } from "./utils" +import { JeuJav, getJeuJavList } from "../services/jeuJav" +import { AppThunk, AppState } from "." + +const jeuJavAdapter = createEntityAdapter({ + selectId: (jeuJav) => jeuJav.jeuId, +}) + +export const initialState = jeuJavAdapter.getInitialState({ + readyStatus: "idle", +} as StateRequest) + +const jeuJavList = createSlice({ + name: "jeuJavList", + initialState, + reducers: { + getRequesting: (state) => { + state.readyStatus = "request" + }, + getSuccess: (state, { payload }: PayloadAction) => { + state.readyStatus = "success" + jeuJavAdapter.setAll(state, payload) + }, + getFailure: (state, { payload }: PayloadAction) => { + state.readyStatus = "failure" + state.error = payload + }, + }, +}) + +export default jeuJavList.reducer +export const { getRequesting, getSuccess, getFailure } = jeuJavList.actions + +export const fetchJeuJavList = (): AppThunk => async (dispatch) => { + dispatch(getRequesting()) + + const { error, data } = await getJeuJavList() + + if (error) { + dispatch(getFailure(error.message)) + toast.error(`Erreur lors du chargement des jeux JAV: ${error.message}`, { + position: "top-center", + autoClose: 6000, + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }) + } else { + dispatch(getSuccess(data as JeuJav[])) + } +} + +const shouldFetchJeuJavList = (state: AppState) => state.jeuJavList.readyStatus !== "success" + +export const fetchJeuJavListIfNeed = (): AppThunk => (dispatch, getState) => { + if (shouldFetchJeuJavList(getState())) return dispatch(fetchJeuJavList()) + + return null +} diff --git a/src/store/jeuxJavList.ts b/src/store/jeuxJavList.ts deleted file mode 100644 index c0480e8..0000000 --- a/src/store/jeuxJavList.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit" - -import { JeuxJav, getJeuxJavList } from "../services/jeuxJav" -import { AppThunk, AppState } from "." - -interface JeuxJavList { - readyStatus: string // TODO Change it to: "invalid" | "request" | "success" | "failure" - items: JeuxJav[] - error: string | null -} - -export const initialState: JeuxJavList = { - readyStatus: "invalid", - items: [], - error: null, -} - -const jeuxJavList = createSlice({ - name: "jeuxJavList", - initialState, - reducers: { - getRequesting: (state: JeuxJavList) => { - state.readyStatus = "request" - }, - getSuccess: (state, { payload }: PayloadAction) => { - state.readyStatus = "success" - state.items = payload - }, - getFailure: (state, { payload }: PayloadAction) => { - state.readyStatus = "failure" - state.error = payload - }, - }, -}) - -export default jeuxJavList.reducer -export const { getRequesting, getSuccess, getFailure } = jeuxJavList.actions - -export const fetchJeuxJavList = (): AppThunk => async (dispatch) => { - dispatch(getRequesting()) - - const { error, data } = await getJeuxJavList() - - if (error) { - dispatch(getFailure(error.message)) - } else { - dispatch(getSuccess(data as JeuxJav[])) - } -} - -const shouldFetchJeuxJavList = (state: AppState) => state.jeuxJavList.readyStatus !== "success" - -export const fetchJeuxJavListIfNeed = (): AppThunk => (dispatch, getState) => { - if (shouldFetchJeuxJavList(getState())) return dispatch(fetchJeuxJavList()) - - return null -} diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 7693247..d9c22ac 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -3,7 +3,7 @@ import { connectRouter } from "connected-react-router" import userList from "./userList" import userData from "./userData" -import jeuxJavList from "./jeuxJavList" +import jeuJavList from "./jeuJavList" import envieList from "./envieList" // Use inferred return type for making correctly Redux types @@ -11,7 +11,7 @@ import envieList from "./envieList" export default (history: History) => ({ userList, userData, - jeuxJavList, + jeuJavList, envieList, router: connectRouter(history) as any, // Register more reducers... diff --git a/src/store/userData.ts b/src/store/userData.ts index 7c9e603..ea9f020 100644 --- a/src/store/userData.ts +++ b/src/store/userData.ts @@ -1,39 +1,31 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit" +import { toast } from "react-toastify" +import { StateRequest } from "./utils" import { User, getUserData } from "../services/jsonPlaceholder" import { AppThunk, AppState } from "." -interface UserDate { - [id: string]: { - readyStatus: string - item?: User - error?: string - } -} +type StateUser = { entity?: User } & StateRequest -interface Success { - id: string - item: User -} - -interface Failure { - id: string - error: string +export const initialState: StateUser = { + readyStatus: "idle", } const userData = createSlice({ name: "userData", - initialState: {} as UserDate, + initialState, reducers: { - getRequesting: (state, { payload }: PayloadAction) => { - state[payload] = { readyStatus: "request" } - }, - getSuccess: (state, { payload }: PayloadAction) => { - state[payload.id] = { readyStatus: "success", item: payload.item } - }, - getFailure: (state, { payload }: PayloadAction) => { - state[payload.id] = { readyStatus: "failure", error: payload.error } - }, + getRequesting: (_) => ({ + readyStatus: "request", + }), + getSuccess: (_, { payload }: PayloadAction) => ({ + readyStatus: "success", + entity: payload, + }), + getFailure: (_, { payload }: PayloadAction) => ({ + readyStatus: "failure", + error: payload, + }), }, }) @@ -41,24 +33,34 @@ export default userData.reducer export const { getRequesting, getSuccess, getFailure } = userData.actions export const fetchUserData = - (id: string): AppThunk => + (id: number): AppThunk => async (dispatch) => { - dispatch(getRequesting(id)) + dispatch(getRequesting()) const { error, data } = await getUserData(id) if (error) { - dispatch(getFailure({ id, error: error.message })) + dispatch(getFailure(error.message)) + toast.error(`Erreur lors du chargement de l'utilisateur ${id}: ${error.message}`, { + position: "top-center", + autoClose: 6000, + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }) } else { - dispatch(getSuccess({ id, item: data as User })) + dispatch(getSuccess(data as User)) } } -const shouldFetchUserData = (state: AppState, id: string) => - state.userData[id]?.readyStatus !== "success" +const shouldFetchUserData = (state: AppState, id: number) => + state.userData.readyStatus !== "success" || + (state.userData.entity && state.userData.entity.membreId !== id) export const fetchUserDataIfNeed = - (id: string): AppThunk => + (id: number): AppThunk => (dispatch, getState) => { if (shouldFetchUserData(getState(), id)) return dispatch(fetchUserData(id)) diff --git a/src/store/userList.ts b/src/store/userList.ts index 7f2ec63..df56627 100644 --- a/src/store/userList.ts +++ b/src/store/userList.ts @@ -1,30 +1,28 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit" +import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolkit" +import { toast } from "react-toastify" +import { StateRequest } from "./utils" import { User, getUserList } from "../services/jsonPlaceholder" import { AppThunk, AppState } from "." -interface UserList { - readyStatus: string - items: User[] - error: string | null -} +const userAdapter = createEntityAdapter({ + selectId: (user) => user.membreId, +}) -export const initialState: UserList = { - readyStatus: "invalid", - items: [], - error: null, -} +export const initialState = userAdapter.getInitialState({ + readyStatus: "idle", +} as StateRequest) const userList = createSlice({ name: "userList", initialState, reducers: { - getRequesting: (state: UserList) => { + getRequesting: (state) => { state.readyStatus = "request" }, getSuccess: (state, { payload }: PayloadAction) => { state.readyStatus = "success" - state.items = payload + userAdapter.setAll(state, payload) }, getFailure: (state, { payload }: PayloadAction) => { state.readyStatus = "failure" @@ -43,6 +41,15 @@ export const fetchUserList = (): AppThunk => async (dispatch) => { if (error) { dispatch(getFailure(error.message)) + toast.error(`Erreur lors du chargement des utilisateurs: ${error.message}`, { + position: "top-center", + autoClose: 6000, + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }) } else { dispatch(getSuccess(data as User[])) } diff --git a/src/store/utils.ts b/src/store/utils.ts new file mode 100644 index 0000000..8b57c41 --- /dev/null +++ b/src/store/utils.ts @@ -0,0 +1,4 @@ +export interface StateRequest { + readyStatus: "idle" | "request" | "success" | "failure" + error?: string +}