Adds gSheet specific entity read

This commit is contained in:
forceoranj 2021-11-14 01:44:11 +01:00
parent 193f51b54d
commit c33b9d8f79
43 changed files with 546 additions and 418 deletions

View File

@ -313,7 +313,7 @@ function formulaSafe(value: string): string {
export { SCOPES } export { SCOPES }
class Test { class Test {
envieId = 5 id = 5
envies = "" envies = ""
@ -348,7 +348,7 @@ class Test {
async function testGSheetAPi(): Promise<void> { async function testGSheetAPi(): Promise<void> {
const dataset: Test[] = [ const dataset: Test[] = [
{ {
envieId: 1, id: 1,
envies: "Présenter le festival et son organisation à un nouveau bénévol au téléphone", envies: "Présenter le festival et son organisation à un nouveau bénévol au téléphone",
dateAjout: new Date("2021-10-18T22:00:00.000Z"), dateAjout: new Date("2021-10-18T22:00:00.000Z"),
ignore: true, ignore: true,
@ -362,7 +362,7 @@ async function testGSheetAPi(): Promise<void> {
tictactoe: [true, false, true, false, false, true], tictactoe: [true, false, true, false, false, true],
}, },
{ {
envieId: 5, id: 5,
envies: "Créer de jolies pages webs", envies: "Créer de jolies pages webs",
dateAjout: new Date("2021-10-18T22:00:00.000Z"), dateAjout: new Date("2021-10-18T22:00:00.000Z"),
ignore: false, ignore: false,
@ -372,7 +372,7 @@ async function testGSheetAPi(): Promise<void> {
tictactoe: [], tictactoe: [],
}, },
{ {
envieId: 6, id: 6,
envies: "Modérer un salon Discord", envies: "Modérer un salon Discord",
dateAjout: new Date("2021-10-18T22:00:00.000Z"), dateAjout: new Date("2021-10-18T22:00:00.000Z"),
ignore: true, ignore: true,

View File

@ -1,27 +0,0 @@
/**
* @jest-environment jsdom
*/
import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom"
import Info from "../index"
describe("<Info />", () => {
it("renders", () => {
const tree = render(
<MemoryRouter>
<Info
item={{
membreId: 1,
name: "PeL",
phone: "+886 0970...",
email: "forceoranj@gmail.com",
website: "https://www.parisestludique.fr",
}}
/>
</MemoryRouter>
).container.firstChild
expect(tree).toMatchSnapshot()
})
})

View File

@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Info /> renders 1`] = `
<div
class="UserCard"
>
<h4>
User Info
</h4>
<ul>
<li>
Name:
PeL
</li>
<li>
Phone:
+886 0970...
</li>
<li>
Email:
forceoranj@gmail.com
</li>
<li>
Website:
https://www.parisestludique.fr
</li>
</ul>
</div>
`;

View File

@ -1,22 +0,0 @@
import { memo } from "react"
import { User } from "../../services/jsonPlaceholder"
import styles from "./styles.module.scss"
interface Props {
item: User
}
const Info = ({ item }: Props) => (
<div className={styles.UserCard}>
<h4>User Info</h4>
<ul>
<li>Name: {item.name}</li>
<li>Phone: {item.phone}</li>
<li>Email: {item.email}</li>
<li>Website: {item.website}</li>
</ul>
</div>
)
export default memo(Info)

View File

@ -5,7 +5,7 @@ import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom" import { MemoryRouter } from "react-router-dom"
import mockStore from "../../../utils/mockStore" import mockStore from "../../../utils/mockStore"
import List from "../index" import JeuJavList from "../index"
describe("<List />", () => { describe("<List />", () => {
const renderHelper = (reducer = { readyStatus: "idle" }) => { const renderHelper = (reducer = { readyStatus: "idle" }) => {
@ -13,7 +13,7 @@ describe("<List />", () => {
const { container } = render( const { container } = render(
<ProviderWithStore> <ProviderWithStore>
<MemoryRouter> <MemoryRouter>
<List ids={[5]} /> <JeuJavList ids={[5]} />
</MemoryRouter> </MemoryRouter>
</ProviderWithStore> </ProviderWithStore>
) )

View File

@ -10,7 +10,7 @@ interface Props {
ids: EntityId[] ids: EntityId[]
} }
const List = ({ ids }: Props) => { const JeuJavList = ({ ids }: Props) => {
const { entities: jeuxJav } = useSelector((state: AppState) => state.jeuJavList, shallowEqual) const { entities: jeuxJav } = useSelector((state: AppState) => state.jeuJavList, shallowEqual)
return ( return (
<div className={styles.JeuJavList}> <div className={styles.JeuJavList}>
@ -33,4 +33,4 @@ const List = ({ ids }: Props) => {
) )
} }
export default memo(List) export default memo(JeuJavList)

View File

@ -1,29 +0,0 @@
/**
* @jest-environment jsdom
*/
import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom"
import List from "../index"
describe("<List />", () => {
it("renders", () => {
const tree = render(
<MemoryRouter>
<List
items={[
{
membreId: 1,
name: "PeL",
phone: "+886 0970...",
email: "forceoranj@gmail.com",
website: "https://www.parisestludique.fr",
},
]}
/>
</MemoryRouter>
).container.firstChild
expect(tree).toMatchSnapshot()
})
})

View File

@ -1,20 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<List /> renders 1`] = `
<div
class="UserList"
>
<h4>
User List
</h4>
<ul>
<li>
<a
href="/UserInfo/1"
>
PeL
</a>
</li>
</ul>
</div>
`;

View File

@ -1,24 +0,0 @@
import { memo } from "react"
import { Link } from "react-router-dom"
import { User } from "../../services/jsonPlaceholder"
import styles from "./styles.module.scss"
interface Props {
items: User[]
}
const List = ({ items }: Props) => (
<div className={styles.UserList}>
<h4>User List</h4>
<ul>
{items.map(({ membreId, name }) => (
<li key={membreId}>
<Link to={`/UserInfo/${membreId}`}>{name}</Link>
</li>
))}
</ul>
</div>
)
export default memo(List)

View File

@ -0,0 +1,35 @@
/**
* @jest-environment jsdom
*/
import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom"
import MembreInfo from "../index"
describe("<MembreInfo />", () => {
it("renders", () => {
const tree = render(
<MemoryRouter>
<MembreInfo
item={{
id: 1,
nom: "Aupeix",
prenom: "Amélie",
mail: "pakouille.lakouille@yahoo.fr",
telephone: "0675650392",
photo: "images/membres/$taille/amélie_aupeix.jpg",
alimentation: "Végétarien",
majeur: 1,
privilege: 0,
actif: 0,
commentaire: "",
horodatage: "0000-00-00",
passe: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
}}
/>
</MemoryRouter>
).container.firstChild
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MembreInfo /> renders 1`] = `
<div
class="MembreCard"
>
<h4>
Membre Info
</h4>
<ul>
<li>
Prénom:
Amélie
</li>
<li>
Nom:
Aupeix
</li>
</ul>
</div>
`;

View File

@ -0,0 +1,20 @@
import { memo } from "react"
import { Membre } from "../../services/membres"
import styles from "./styles.module.scss"
interface Props {
item: Membre
}
const MembreInfo = ({ item }: Props) => (
<div className={styles.MembreCard}>
<h4>Membre Info</h4>
<ul>
<li>Prénom: {item.prenom}</li>
<li>Nom: {item.nom}</li>
</ul>
</div>
)
export default memo(MembreInfo)

View File

@ -1,4 +1,4 @@
.user-card { .membre-card {
ul { ul {
padding-left: 17px; padding-left: 17px;

View File

@ -0,0 +1,37 @@
/**
* @jest-environment jsdom
*/
import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom"
import MembreList from "../index"
describe("<MembreList />", () => {
it("renders", () => {
const tree = render(
<MemoryRouter>
<MembreList
items={[
{
id: 1,
nom: "Aupeix",
prenom: "Amélie",
mail: "pakouille.lakouille@yahoo.fr",
telephone: "0675650392",
photo: "images/membres/$taille/amélie_aupeix.jpg",
alimentation: "Végétarien",
majeur: 1,
privilege: 0,
actif: 0,
commentaire: "",
horodatage: "0000-00-00",
passe: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
},
]}
/>
</MemoryRouter>
).container.firstChild
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MembreList /> renders 1`] = `
<div
class="MembreList"
>
<h4>
Membre List
</h4>
<ul>
<li>
<a
href="/Membre/1"
>
<b>
Amélie
</b>
Aupeix
</a>
</li>
</ul>
</div>
`;

View File

@ -0,0 +1,26 @@
import { memo } from "react"
import { Link } from "react-router-dom"
import { Membre } from "../../services/membres"
import styles from "./styles.module.scss"
interface Props {
items: Membre[]
}
const MembreList = ({ items }: Props) => (
<div className={styles.MembreList}>
<h4>Membre List</h4>
<ul>
{items.map(({ id, nom, prenom }) => (
<li key={id}>
<Link to={`/Membre/${id}`}>
<b>{prenom}</b> {nom}
</Link>
</li>
))}
</ul>
</div>
)
export default memo(MembreList)

View File

@ -1,8 +1,8 @@
import List from "./List" import MembreList from "./MembreList"
import JeuJavList from "./JeuJavList" import JeuJavList from "./JeuJavList"
import Info from "./Info" import MembreInfo from "./MembreInfo"
import ErrorBoundary from "./ErrorBoundary" import ErrorBoundary from "./ErrorBoundary"
import Loading from "./Loading" import Loading from "./Loading"
import AddEnvie from "./AddEnvie" import AddEnvie from "./AddEnvie"
export { List, JeuJavList, Info, ErrorBoundary, Loading, AddEnvie } export { MembreList, JeuJavList, MembreInfo, ErrorBoundary, Loading, AddEnvie }

View File

@ -23,7 +23,7 @@ export const addEnvie = async (
_next: NextFunction _next: NextFunction
): Promise<void> => { ): Promise<void> => {
try { try {
const envie = await add<EnvieWithoutId, Envie>("Envies d'aider", "envieId", request.body) const envie = await add<EnvieWithoutId, Envie>("Envies d'aider", "id", request.body)
if (envie) { if (envie) {
response.status(200).json(envie) response.status(200).json(envie)
} }

47
src/gsheets/membres.ts Normal file
View File

@ -0,0 +1,47 @@
import { Request, Response, NextFunction } from "express"
import { getList, get, add } from "./utils"
import { Membre, MembreWithoutId } from "../services/membres"
export const getMembreList = async (
_request: Request,
response: Response,
_next: NextFunction
): Promise<void> => {
try {
const list = await getList<Membre>("Membres", new Membre())
if (list) {
response.status(200).json(list)
}
} catch (e: unknown) {
response.status(400).json(e)
}
}
export const getMembre = async (
request: Request,
response: Response,
_next: NextFunction
): Promise<void> => {
try {
const id = parseInt(request.query.id as string, 10) || -1
const membre = await get<Membre>("Membres", id, new Membre())
response.status(200).json(membre)
} catch (e: unknown) {
response.status(400).json(e)
}
}
export const addMembre = async (
request: Request,
response: Response,
_next: NextFunction
): Promise<void> => {
try {
const membre = await add<MembreWithoutId, Membre>("Membres", "membreId", request.body)
if (membre) {
response.status(200).json(membre)
}
} catch (e: unknown) {
response.status(400).json(e)
}
}

View File

@ -6,8 +6,10 @@ import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadshee
const SCOPES = ["https://www.googleapis.com/auth/spreadsheets"] const SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json") const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json")
type ElementWithId = unknown & { id: number }
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
export async function getList<Element extends object>( export async function getList<Element extends ElementWithId>(
sheetName: string, sheetName: string,
specimen: Element specimen: Element
): Promise<Element[]> { ): Promise<Element[]> {
@ -37,7 +39,17 @@ export async function getList<Element extends object>(
} }
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
export async function setList<Element extends object>( export async function get<Element extends ElementWithId>(
sheetName: string,
membreId: number,
specimen: Element
): Promise<Element | undefined> {
const list = await getList<Element>(sheetName, specimen)
return list.find((element) => element.id === membreId)
}
// eslint-disable-next-line @typescript-eslint/ban-types
export async function setList<Element extends ElementWithId>(
sheetName: string, sheetName: string,
elements: Element[] elements: Element[]
): Promise<true | undefined> { ): Promise<true | undefined> {
@ -90,7 +102,7 @@ export async function setList<Element extends object>(
} }
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
export async function add<ElementNoId extends object, Element extends ElementNoId>( export async function add<ElementNoId extends object, Element extends ElementNoId & ElementWithId>(
sheetName: string, sheetName: string,
idFieldName: string, idFieldName: string,
partialElement: Partial<ElementNoId> partialElement: Partial<ElementNoId>

View File

@ -0,0 +1,48 @@
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 { fetchMembreDataIfNeed } from "../../store/membre"
import { MembreInfo } from "../../components"
import styles from "./styles.module.scss"
export type Props = RouteComponentProps<{ id: string }>
const MembrePage = ({ match }: Props): JSX.Element => {
const { id: rawId } = match.params
const id = +rawId
const dispatch = useDispatch()
const membre = useSelector((state: AppState) => state.membre, shallowEqual)
useEffect(() => {
dispatch(fetchMembreDataIfNeed(id))
}, [dispatch, id])
const renderInfo = () => {
const membreInfo = membre
if (!membreInfo || membreInfo.readyStatus === "request") return <p>Loading...</p>
if (membreInfo.readyStatus === "failure" || !membreInfo.entity)
return <p>Oops! Failed to load data.</p>
return <MembreInfo item={membreInfo.entity} />
}
return (
<div className={styles.Membre}>
<Helmet title="User Info" />
{renderInfo()}
</div>
)
}
interface LoadDataArgs {
params: { id: number }
}
export const loadData = ({ params }: LoadDataArgs): AppThunk[] => [fetchMembreDataIfNeed(params.id)]
export default memo(MembrePage)

View File

@ -4,27 +4,36 @@
import { render } from "@testing-library/react" import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom" import { MemoryRouter } from "react-router-dom"
import { fetchMembreDataIfNeed } from "../../../store/membre"
import mockStore from "../../../utils/mockStore" import mockStore from "../../../utils/mockStore"
import UserInfo from "../UserInfo" import MembrePage from "../MembrePage"
describe("<UserInfo />", () => { describe("<MembrePage />", () => {
const mockData = { const mockData = {
memberId: 1, id: 1,
name: "PeL", nom: "Aupeix",
phone: "+886 0970...", prenom: "Amélie",
email: "forceoranj@gmail.com", mail: "pakouille.lakouille@yahoo.fr",
website: "https://www.parisestludique.fr", telephone: "0675650392",
photo: "images/membres/$taille/amélie_aupeix.jpg",
alimentation: "Végétarien",
majeur: 1,
privilege: 0,
actif: 0,
commentaire: "",
horodatage: "0000-00-00",
passe: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
} }
const { memberId } = mockData const { id } = mockData
const renderHelper = (reducer = {}) => { const renderHelper = (reducer = {}) => {
const { dispatch, ProviderWithStore } = mockStore({ userData: reducer }) const { dispatch, ProviderWithStore } = mockStore({ membre: reducer })
const { container } = render( const { container } = render(
<ProviderWithStore> <ProviderWithStore>
<MemoryRouter> <MemoryRouter>
{/* {/*
@ts-expect-error */} @ts-expect-error */}
<UserInfo match={{ params: { memberId } }} /> <MembrePage match={{ params: { id } }} />
</MemoryRouter> </MemoryRouter>
</ProviderWithStore> </ProviderWithStore>
) )
@ -32,6 +41,13 @@ describe("<UserInfo />", () => {
return { dispatch, firstChild: container.firstChild } 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(fetchMembreDataIfNeed(id).toString())
})
it("renders the loading status if data invalid", () => { it("renders the loading status if data invalid", () => {
expect(renderHelper().firstChild).toMatchSnapshot() expect(renderHelper().firstChild).toMatchSnapshot()
}) })

View File

@ -0,0 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MembrePage /> renders an error if loading failed 1`] = `
<div
class="Membre"
>
<p>
Oops! Failed to load data.
</p>
</div>
`;
exports[`<MembrePage /> renders the <Info /> if loading was successful 1`] = `
<div
class="Membre"
>
<div
class="MembreCard"
>
<h4>
Membre Info
</h4>
<ul>
<li>
Prénom:
Amélie
</li>
<li>
Nom:
Aupeix
</li>
</ul>
</div>
</div>
`;
exports[`<MembrePage /> renders the loading status if data invalid 1`] = `
<div
class="Membre"
>
<p>
Oops! Failed to load data.
</p>
</div>
`;
exports[`<MembrePage /> renders the loading status if requesting data 1`] = `
<div
class="Membre"
>
<p>
Loading...
</p>
</div>
`;

View File

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

View File

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

View File

@ -1,48 +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 { fetchUserDataIfNeed } from "../../store/userData"
import { Info } from "../../components"
import styles from "./styles.module.scss"
export type Props = RouteComponentProps<{ memberId: string }>
const UserInfo = ({ match }: Props): JSX.Element => {
const { memberId: rawId } = match.params
const id = +rawId
const dispatch = useDispatch()
const userData = useSelector((state: AppState) => state.userData, shallowEqual)
useEffect(() => {
dispatch(fetchUserDataIfNeed(id))
}, [dispatch, id])
const renderInfo = () => {
const userInfo = userData
if (!userInfo || userInfo.readyStatus === "request") return <p>Loading...</p>
if (userInfo.readyStatus === "failure" || !userInfo.entity)
return <p>Oops! Failed to load data.</p>
return <Info item={userInfo.entity} />
}
return (
<div className={styles.UserInfo}>
<Helmet title="User Info" />
{renderInfo()}
</div>
)
}
interface LoadDataArgs {
params: { id: number }
}
export const loadData = ({ params }: LoadDataArgs): AppThunk[] => [fetchUserDataIfNeed(params.id)]
export default memo(UserInfo)

View File

@ -1,63 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UserInfo /> renders an error if loading failed 1`] = `
<div
class="UserInfo"
>
<p>
Oops! Failed to load data.
</p>
</div>
`;
exports[`<UserInfo /> renders the <Info /> if loading was successful 1`] = `
<div
class="UserInfo"
>
<div
class="UserCard"
>
<h4>
User Info
</h4>
<ul>
<li>
Name:
PeL
</li>
<li>
Phone:
+886 0970...
</li>
<li>
Email:
forceoranj@gmail.com
</li>
<li>
Website:
https://www.parisestludique.fr
</li>
</ul>
</div>
</div>
`;
exports[`<UserInfo /> renders the loading status if data invalid 1`] = `
<div
class="UserInfo"
>
<p>
Oops! Failed to load data.
</p>
</div>
`;
exports[`<UserInfo /> renders the loading status if requesting data 1`] = `
<div
class="UserInfo"
>
<p>
Loading...
</p>
</div>
`;

View File

@ -2,7 +2,7 @@ import { RouteConfig } from "react-router-config"
import App from "../app" import App from "../app"
import AsyncHome, { loadData as loadHomeData } from "../pages/Home" import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
import AsyncUserInfo, { loadData as loadUserInfoData } from "../pages/UserInfo" import AsyncMembrePage, { loadData as loadMembrePageData } from "../pages/MembrePage"
import NotFound from "../pages/NotFound" import NotFound from "../pages/NotFound"
export default [ export default [
@ -16,9 +16,9 @@ export default [
loadData: loadHomeData, // Add your pre-fetch method here loadData: loadHomeData, // Add your pre-fetch method here
}, },
{ {
path: "/UserInfo/:id", path: "/MembrePage/:id",
component: AsyncUserInfo, component: AsyncMembrePage,
loadData: loadUserInfoData, loadData: loadMembrePageData,
}, },
{ {
component: NotFound, component: NotFound,

View File

@ -12,6 +12,7 @@ import ssr from "./ssr"
import { getJeuJavList } from "../gsheets/jeuJav" import { getJeuJavList } from "../gsheets/jeuJav"
import { getEnvieList, addEnvie } from "../gsheets/envies" import { getEnvieList, addEnvie } from "../gsheets/envies"
import { getMembre } from "../gsheets/membres"
import config from "../config" import config from "../config"
const app = express() const app = express()
@ -33,8 +34,9 @@ if (__DEV__) devServer(app)
// Google Sheets requests // Google Sheets requests
app.use(express.json()) app.use(express.json())
app.get("/JeuJav", getJeuJavList) app.get("/JeuJavList", getJeuJavList)
app.get("/GetEnvieList", getEnvieList) app.get("/GetEnvieList", getEnvieList)
app.get("/GetMembre", getMembre)
app.post("/AddEnvie", addEnvie) app.post("/AddEnvie", addEnvie)
// Use React server-side rendering middleware // Use React server-side rendering middleware

View File

@ -3,7 +3,7 @@ import axios from "axios"
import config from "../config" import config from "../config"
export class Envie { export class Envie {
envieId = 0 id = 0
domaine = "" domaine = ""
@ -15,7 +15,7 @@ export class Envie {
dateAjout = "" dateAjout = ""
} }
export type EnvieWithoutId = Omit<Envie, "envieId"> export type EnvieWithoutId = Omit<Envie, "id">
export interface GetEnvieListResponse { export interface GetEnvieListResponse {
data?: Envie[] data?: Envie[]

View File

@ -3,7 +3,7 @@ import axios from "axios"
import config from "../config" import config from "../config"
export class JeuJav { export class JeuJav {
jeuId = 0 id = 0
titre = "" titre = ""
@ -46,7 +46,7 @@ export interface JeuJavData {
export const getJeuJavList = async (): Promise<JeuJavList> => { export const getJeuJavList = async (): Promise<JeuJavList> => {
try { try {
const { data } = await axios.get(`${config.API_URL}/JeuJav`) const { data } = await axios.get(`${config.API_URL}/JeuJavList`)
return { data } return { data }
} catch (error) { } catch (error) {
return { error: error as Error } return { error: error as Error }
@ -55,7 +55,7 @@ export const getJeuJavList = async (): Promise<JeuJavList> => {
export const getJeuJavData = async (id: string): Promise<JeuJavData> => { export const getJeuJavData = async (id: string): Promise<JeuJavData> => {
try { try {
const { data } = await axios.get(`${config.API_URL}/users/${id}`) const { data } = await axios.get(`${config.API_URL}/JeuJav`, { params: { id } })
return { data } return { data }
} catch (error) { } catch (error) {
return { error: error as Error } return { error: error as Error }

View File

@ -1,37 +0,0 @@
import axios from "axios"
export interface User {
membreId: number
name: string
phone: string
email: string
website: string
}
interface UserList {
data?: User[]
error?: Error
}
interface UserData {
data?: User
error?: Error
}
export const getUserList = async (): Promise<UserList> => {
try {
const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users`)
return { data }
} catch (error) {
return { error: error as Error }
}
}
export const getUserData = async (id: number): Promise<UserData> => {
try {
const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`)
return { data }
} catch (error) {
return { error: error as Error }
}
}

71
src/services/membres.ts Normal file
View File

@ -0,0 +1,71 @@
import axios from "axios"
import config from "../config"
export class Membre {
id = 0
nom = ""
prenom = ""
mail = ""
telephone = ""
photo = ""
alimentation = ""
majeur = 1
privilege = 0
actif = 0
commentaire = ""
horodatage = ""
passe = ""
}
export type MembreWithoutId = Omit<Membre, "membreId">
export interface GetMembreListResponse {
data?: Membre[]
error?: Error
}
export const getMembreList = async (): Promise<GetMembreListResponse> => {
try {
const { data } = await axios.get(`${config.API_URL}/GetMembreList`)
return { data }
} catch (error) {
return { error: error as Error }
}
}
export interface GetMembreResponse {
data?: Membre
error?: Error
}
export const getMembre = async (id: number): Promise<GetMembreResponse> => {
try {
const { data } = await axios.get(`${config.API_URL}/GetMembre`, { params: { id } })
return { data }
} catch (error) {
return { error: error as Error }
}
}
export interface AddMembreResponse {
data?: Membre
error?: Error
}
export const addMembre = async (membreWithoutId: MembreWithoutId): Promise<AddMembreResponse> => {
try {
const { data } = await axios.post(`${config.API_URL}/AddMembre`, membreWithoutId)
return { data }
} catch (error) {
return { error: error as Error }
}
}

View File

@ -13,7 +13,7 @@ jest.mock("axios")
const mockData = { const mockData = {
"5": { "5": {
jeuId: 5, id: 5,
titre: "6 qui prend!", titre: "6 qui prend!",
auteur: "Wolfgang Kramer", auteur: "Wolfgang Kramer",
editeur: "(uncredited) , Design Edge , B", editeur: "(uncredited) , Design Edge , B",

View File

@ -1,41 +1,49 @@
import axios from "axios" import axios from "axios"
import mockStore from "../../utils/mockStore" import mockStore from "../../utils/mockStore"
import userData, { import membre, {
getRequesting, getRequesting,
getSuccess, getSuccess,
getFailure, getFailure,
fetchUserData, fetchMembreData,
initialState, initialState,
} from "../userData" } from "../membre"
jest.mock("axios") jest.mock("axios")
const mockData = { const mockData = {
membreId: 1, id: 1,
name: "PeL", nom: "Aupeix",
phone: "+886 0970...", prenom: "Amélie",
email: "forceoranj@gmail.com", mail: "pakouille.lakouille@yahoo.fr",
website: "https://www.parisestludique.fr", telephone: "0675650392",
photo: "images/membres/$taille/amélie_aupeix.jpg",
alimentation: "Végétarien",
majeur: 1,
privilege: 0,
actif: 0,
commentaire: "",
horodatage: "0000-00-00",
passe: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
} }
const { membreId } = mockData const { id } = mockData
const mockError = "Oops! Something went wrong." const mockError = "Oops! Something went wrong."
describe("userData reducer", () => { describe("membre reducer", () => {
it("should handle initial state correctly", () => { it("should handle initial state correctly", () => {
// @ts-expect-error // @ts-expect-error
expect(userData(undefined, {})).toEqual(initialState) expect(membre(undefined, {})).toEqual(initialState)
}) })
it("should handle requesting correctly", () => { it("should handle requesting correctly", () => {
expect(userData(undefined, { type: getRequesting.type, payload: membreId })).toEqual({ expect(membre(undefined, { type: getRequesting.type, payload: id })).toEqual({
readyStatus: "request", readyStatus: "request",
}) })
}) })
it("should handle success correctly", () => { it("should handle success correctly", () => {
expect( expect(
userData(undefined, { membre(undefined, {
type: getSuccess.type, type: getSuccess.type,
payload: mockData, payload: mockData,
}) })
@ -44,7 +52,7 @@ describe("userData reducer", () => {
it("should handle failure correctly", () => { it("should handle failure correctly", () => {
expect( expect(
userData(undefined, { membre(undefined, {
type: getFailure.type, type: getFailure.type,
payload: mockError, payload: mockError,
}) })
@ -52,8 +60,8 @@ describe("userData reducer", () => {
}) })
}) })
describe("userData action", () => { describe("membre action", () => {
it("fetches user data successful", async () => { it("fetches membre data successful", async () => {
const { dispatch, getActions } = mockStore() const { dispatch, getActions } = mockStore()
const expectedActions = [ const expectedActions = [
{ type: getRequesting.type }, { type: getRequesting.type },
@ -63,11 +71,11 @@ describe("userData action", () => {
// @ts-expect-error // @ts-expect-error
axios.get.mockResolvedValue({ data: mockData }) axios.get.mockResolvedValue({ data: mockData })
await dispatch(fetchUserData(membreId)) await dispatch(fetchMembreData(id))
expect(getActions()).toEqual(expectedActions) expect(getActions()).toEqual(expectedActions)
}) })
it("fetches user data failed", async () => { it("fetches membre data failed", async () => {
const { dispatch, getActions } = mockStore() const { dispatch, getActions } = mockStore()
const expectedActions = [ const expectedActions = [
{ type: getRequesting.type }, { type: getRequesting.type },
@ -77,7 +85,7 @@ describe("userData action", () => {
// @ts-expect-error // @ts-expect-error
axios.get.mockRejectedValue({ message: mockError }) axios.get.mockRejectedValue({ message: mockError })
await dispatch(fetchUserData(membreId)) await dispatch(fetchMembreData(id))
expect(getActions()).toEqual(expectedActions) expect(getActions()).toEqual(expectedActions)
}) })
}) })

View File

@ -1,35 +1,43 @@
import axios from "axios" import axios from "axios"
import mockStore from "../../utils/mockStore" import mockStore from "../../utils/mockStore"
import userList, { import membreList, {
initialState, initialState,
getRequesting, getRequesting,
getSuccess, getSuccess,
getFailure, getFailure,
fetchUserList, fetchMembreList,
} from "../userList" } from "../membreList"
jest.mock("axios") jest.mock("axios")
const mockData = { const mockData = {
"1": { "1": {
membreId: 1, id: 1,
name: "PeL", nom: "Aupeix",
phone: "+886 0970...", prenom: "Amélie",
email: "forceoranj@gmail.com", mail: "pakouille.lakouille@yahoo.fr",
website: "https://www.parisestludique.fr", telephone: "0675650392",
photo: "images/membres/$taille/amélie_aupeix.jpg",
alimentation: "Végétarien",
majeur: 1,
privilege: 0,
actif: 0,
commentaire: "",
horodatage: "0000-00-00",
passe: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O",
}, },
} }
const mockError = "Oops! Something went wrong." const mockError = "Oops! Something went wrong."
describe("userList reducer", () => { describe("membreList reducer", () => {
it("should handle initial state", () => { it("should handle initial state", () => {
// @ts-expect-error // @ts-expect-error
expect(userList(undefined, {})).toEqual(initialState) expect(membreList(undefined, {})).toEqual(initialState)
}) })
it("should handle requesting correctly", () => { it("should handle requesting correctly", () => {
expect(userList(undefined, { type: getRequesting.type })).toEqual({ expect(membreList(undefined, { type: getRequesting.type })).toEqual({
readyStatus: "request", readyStatus: "request",
ids: [], ids: [],
entities: {}, entities: {},
@ -37,7 +45,7 @@ describe("userList reducer", () => {
}) })
it("should handle success correctly", () => { it("should handle success correctly", () => {
expect(userList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({ expect(membreList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({
...initialState, ...initialState,
readyStatus: "success", readyStatus: "success",
ids: [1], ids: [1],
@ -46,7 +54,7 @@ describe("userList reducer", () => {
}) })
it("should handle failure correctly", () => { it("should handle failure correctly", () => {
expect(userList(undefined, { type: getFailure.type, payload: mockError })).toEqual({ expect(membreList(undefined, { type: getFailure.type, payload: mockError })).toEqual({
...initialState, ...initialState,
readyStatus: "failure", readyStatus: "failure",
error: mockError, error: mockError,
@ -54,8 +62,8 @@ describe("userList reducer", () => {
}) })
}) })
describe("userList action", () => { describe("membreList action", () => {
it("fetches user list successful", async () => { it("fetches membre list successful", async () => {
const { dispatch, getActions } = mockStore() const { dispatch, getActions } = mockStore()
const expectedActions = [ const expectedActions = [
{ type: getRequesting.type }, { type: getRequesting.type },
@ -65,11 +73,11 @@ describe("userList action", () => {
// @ts-expect-error // @ts-expect-error
axios.get.mockResolvedValue({ data: mockData }) axios.get.mockResolvedValue({ data: mockData })
await dispatch(fetchUserList()) await dispatch(fetchMembreList())
expect(getActions()).toEqual(expectedActions) expect(getActions()).toEqual(expectedActions)
}) })
it("fetches user list failed", async () => { it("fetches membre list failed", async () => {
const { dispatch, getActions } = mockStore() const { dispatch, getActions } = mockStore()
const expectedActions = [ const expectedActions = [
{ type: getRequesting.type }, { type: getRequesting.type },
@ -79,7 +87,7 @@ describe("userList action", () => {
// @ts-expect-error // @ts-expect-error
axios.get.mockRejectedValue({ message: mockError }) axios.get.mockRejectedValue({ message: mockError })
await dispatch(fetchUserList()) await dispatch(fetchMembreList())
expect(getActions()).toEqual(expectedActions) expect(getActions()).toEqual(expectedActions)
}) })
}) })

View File

@ -6,7 +6,7 @@ import { Envie, EnvieWithoutId, addEnvie } from "../services/envies"
import { AppThunk } from "." import { AppThunk } from "."
const envieAdapter = createEntityAdapter<Envie>({ const envieAdapter = createEntityAdapter<Envie>({
selectId: (envie) => envie.envieId, selectId: (envie) => envie.id,
}) })
const envieAdd = createSlice({ const envieAdd = createSlice({

View File

@ -6,7 +6,7 @@ import { Envie, getEnvieList } from "../services/envies"
import { AppThunk, AppState } from "." import { AppThunk, AppState } from "."
const envieAdapter = createEntityAdapter<Envie>({ const envieAdapter = createEntityAdapter<Envie>({
selectId: (envie) => envie.envieId, selectId: (envie) => envie.id,
}) })
const envieList = createSlice({ const envieList = createSlice({

View File

@ -6,7 +6,7 @@ import { JeuJav, getJeuJavList } from "../services/jeuJav"
import { AppThunk, AppState } from "." import { AppThunk, AppState } from "."
const jeuJavAdapter = createEntityAdapter<JeuJav>({ const jeuJavAdapter = createEntityAdapter<JeuJav>({
selectId: (jeuJav) => jeuJav.jeuId, selectId: (jeuJav) => jeuJav.id,
}) })
export const initialState = jeuJavAdapter.getInitialState({ export const initialState = jeuJavAdapter.getInitialState({

View File

@ -2,23 +2,23 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { toast } from "react-toastify" import { toast } from "react-toastify"
import { StateRequest } from "./utils" import { StateRequest } from "./utils"
import { User, getUserData } from "../services/jsonPlaceholder" import { Membre, getMembre } from "../services/membres"
import { AppThunk, AppState } from "." import { AppThunk, AppState } from "."
type StateUser = { entity?: User } & StateRequest type StateMembre = { entity?: Membre } & StateRequest
export const initialState: StateUser = { export const initialState: StateMembre = {
readyStatus: "idle", readyStatus: "idle",
} }
const userData = createSlice({ const membre = createSlice({
name: "userData", name: "membre",
initialState, initialState,
reducers: { reducers: {
getRequesting: (_) => ({ getRequesting: (_) => ({
readyStatus: "request", readyStatus: "request",
}), }),
getSuccess: (_, { payload }: PayloadAction<User>) => ({ getSuccess: (_, { payload }: PayloadAction<Membre>) => ({
readyStatus: "success", readyStatus: "success",
entity: payload, entity: payload,
}), }),
@ -29,19 +29,19 @@ const userData = createSlice({
}, },
}) })
export default userData.reducer export default membre.reducer
export const { getRequesting, getSuccess, getFailure } = userData.actions export const { getRequesting, getSuccess, getFailure } = membre.actions
export const fetchUserData = export const fetchMembreData =
(id: number): AppThunk => (id: number): AppThunk =>
async (dispatch) => { async (dispatch) => {
dispatch(getRequesting()) dispatch(getRequesting())
const { error, data } = await getUserData(id) const { error, data } = await getMembre(id)
if (error) { if (error) {
dispatch(getFailure(error.message)) dispatch(getFailure(error.message))
toast.error(`Erreur lors du chargement de l'utilisateur ${id}: ${error.message}`, { toast.error(`Erreur lors du chargement du membre ${id}: ${error.message}`, {
position: "top-center", position: "top-center",
autoClose: 6000, autoClose: 6000,
hideProgressBar: true, hideProgressBar: true,
@ -51,18 +51,17 @@ export const fetchUserData =
progress: undefined, progress: undefined,
}) })
} else { } else {
dispatch(getSuccess(data as User)) dispatch(getSuccess(data as Membre))
} }
} }
const shouldFetchUserData = (state: AppState, id: number) => const shouldFetchMembreData = (state: AppState, id: number) =>
state.userData.readyStatus !== "success" || state.membre.readyStatus !== "success" || (state.membre.entity && state.membre.entity.id !== id)
(state.userData.entity && state.userData.entity.membreId !== id)
export const fetchUserDataIfNeed = export const fetchMembreDataIfNeed =
(id: number): AppThunk => (id: number): AppThunk =>
(dispatch, getState) => { (dispatch, getState) => {
if (shouldFetchUserData(getState(), id)) return dispatch(fetchUserData(id)) if (shouldFetchMembreData(getState(), id)) return dispatch(fetchMembreData(id))
return null return null
} }

View File

@ -2,27 +2,25 @@ import { PayloadAction, createSlice, createEntityAdapter } from "@reduxjs/toolki
import { toast } from "react-toastify" import { toast } from "react-toastify"
import { StateRequest } from "./utils" import { StateRequest } from "./utils"
import { User, getUserList } from "../services/jsonPlaceholder" import { Membre, getMembreList } from "../services/membres"
import { AppThunk, AppState } from "." import { AppThunk, AppState } from "."
const userAdapter = createEntityAdapter<User>({ const membreAdapter = createEntityAdapter<Membre>()
selectId: (user) => user.membreId,
})
export const initialState = userAdapter.getInitialState({ export const initialState = membreAdapter.getInitialState({
readyStatus: "idle", readyStatus: "idle",
} as StateRequest) } as StateRequest)
const userList = createSlice({ const membreList = createSlice({
name: "userList", name: "membreList",
initialState, initialState,
reducers: { reducers: {
getRequesting: (state) => { getRequesting: (state) => {
state.readyStatus = "request" state.readyStatus = "request"
}, },
getSuccess: (state, { payload }: PayloadAction<User[]>) => { getSuccess: (state, { payload }: PayloadAction<Membre[]>) => {
state.readyStatus = "success" state.readyStatus = "success"
userAdapter.setAll(state, payload) membreAdapter.setAll(state, payload)
}, },
getFailure: (state, { payload }: PayloadAction<string>) => { getFailure: (state, { payload }: PayloadAction<string>) => {
state.readyStatus = "failure" state.readyStatus = "failure"
@ -31,13 +29,13 @@ const userList = createSlice({
}, },
}) })
export default userList.reducer export default membreList.reducer
export const { getRequesting, getSuccess, getFailure } = userList.actions export const { getRequesting, getSuccess, getFailure } = membreList.actions
export const fetchUserList = (): AppThunk => async (dispatch) => { export const fetchMembreList = (): AppThunk => async (dispatch) => {
dispatch(getRequesting()) dispatch(getRequesting())
const { error, data } = await getUserList() const { error, data } = await getMembreList()
if (error) { if (error) {
dispatch(getFailure(error.message)) dispatch(getFailure(error.message))
@ -51,14 +49,14 @@ export const fetchUserList = (): AppThunk => async (dispatch) => {
progress: undefined, progress: undefined,
}) })
} else { } else {
dispatch(getSuccess(data as User[])) dispatch(getSuccess(data as Membre[]))
} }
} }
const shouldFetchUserList = (state: AppState) => state.userList.readyStatus !== "success" const shouldFetchMembreList = (state: AppState) => state.membreList.readyStatus !== "success"
export const fetchUserListIfNeed = (): AppThunk => (dispatch, getState) => { export const fetchMembreListIfNeed = (): AppThunk => (dispatch, getState) => {
if (shouldFetchUserList(getState())) return dispatch(fetchUserList()) if (shouldFetchMembreList(getState())) return dispatch(fetchMembreList())
return null return null
} }

View File

@ -1,16 +1,16 @@
import { History } from "history" import { History } from "history"
import { connectRouter } from "connected-react-router" import { connectRouter } from "connected-react-router"
import userList from "./userList" import membreList from "./membreList"
import userData from "./userData" import membre from "./membre"
import jeuJavList from "./jeuJavList" import jeuJavList from "./jeuJavList"
import envieList from "./envieList" import envieList from "./envieList"
// Use inferred return type for making correctly Redux types // Use inferred return type for making correctly Redux types
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export default (history: History) => ({ export default (history: History) => ({
userList, membreList,
userData, membre,
jeuJavList, jeuJavList,
envieList, envieList,
router: connectRouter(history) as any, router: connectRouter(history) as any,