Initial commit

This commit is contained in:
forceoranj
2021-10-16 01:53:56 +02:00
commit ebdd8dccdd
104 changed files with 29770 additions and 0 deletions

46
src/pages/Home/Home.tsx Executable file
View File

@@ -0,0 +1,46 @@
import { FC, 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 { fetchJavGameListIfNeed } from "../../store/javGameList"
import { JavGameList } from "../../components"
import styles from "./styles.module.scss"
export type Props = RouteComponentProps
function useList(stateToProp: (state: AppState) => any, fetchDataIfNeed: () => AppThunk) {
const dispatch = useDispatch()
const { readyStatus, items } = useSelector(stateToProp, shallowEqual)
// Fetch client-side data here
useEffect(() => {
dispatch(fetchDataIfNeed())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch])
return () => {
if (!readyStatus || readyStatus === "invalid" || readyStatus === "request")
return <p>Loading...</p>
if (readyStatus === "failure") return <p>Oops, Failed to load list!</p>
return <JavGameList items={items} />
}
}
const Home: FC<Props> = (): JSX.Element => (
<div className={styles.Home}>
<Helmet title="Home" />
{useList((state: AppState) => state.javGameList, fetchJavGameListIfNeed)()}
</div>
)
// Fetch server-side data here
export const loadData = (): AppThunk[] => [
fetchJavGameListIfNeed(),
// More pre-fetched actions...
]
export default memo(Home)

View File

@@ -0,0 +1,78 @@
/**
* @jest-environment jsdom
*/
import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom"
import { fetchJavGameListIfNeed } from "../../../store/javGameList"
import mockStore from "../../../utils/mockStore"
import Home from "../Home"
describe("<Home />", () => {
const renderHelper = (reducer = { readyStatus: "invalid" }) => {
const { dispatch, ProviderWithStore } = mockStore({ javGameList: reducer })
const { container } = render(
<ProviderWithStore>
<MemoryRouter>
{/*
@ts-expect-error */}
<Home />
</MemoryRouter>
</ProviderWithStore>
)
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(fetchJavGameListIfNeed().toString())
})
it("renders the loading status if data invalid", () => {
expect(renderHelper().firstChild).toMatchSnapshot()
})
it("renders the loading status if requesting data", () => {
const reducer = { readyStatus: "request" }
expect(renderHelper(reducer).firstChild).toMatchSnapshot()
})
it("renders an error if loading failed", () => {
const reducer = { readyStatus: "failure" }
expect(renderHelper(reducer).firstChild).toMatchSnapshot()
})
it("renders the <List /> if loading was successful", () => {
const reducer = {
readyStatus: "success",
items: [
{
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()
})
})

View File

@@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Home /> renders an error if loading failed 1`] = `
<div
class="Home"
>
<p>
Oops, Failed to load list!
</p>
</div>
`;
exports[`<Home /> renders the <List /> if loading was successful 1`] = `
<div
class="Home"
>
<div
class="JavGameList"
>
<h4>
JAV Games
</h4>
<ul>
<li>
<a
href="/UserInfo/5"
>
6 qui prend!
</a>
</li>
</ul>
</div>
</div>
`;
exports[`<Home /> renders the loading status if data invalid 1`] = `
<div
class="Home"
>
<p>
Loading...
</p>
</div>
`;
exports[`<Home /> renders the loading status if requesting data 1`] = `
<div
class="Home"
>
<p>
Loading...
</p>
</div>
`;

15
src/pages/Home/index.tsx Executable file
View File

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

View File

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

View File

@@ -0,0 +1,21 @@
/**
* @jest-environment jsdom
*/
import { render } from "@testing-library/react"
import { MemoryRouter } from "react-router-dom"
import NotFound from "../index"
describe("<NotFound />", () => {
it("renders", () => {
const tree = render(
<MemoryRouter>
{/*
@ts-expect-error */}
<NotFound />
</MemoryRouter>
).container.firstChild
expect(tree).toMatchSnapshot()
})
})

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<NotFound /> renders 1`] = `
<div
class="NotFound"
>
<p>
Oops, Page was not found!
</p>
</div>
`;

23
src/pages/NotFound/index.tsx Executable file
View File

@@ -0,0 +1,23 @@
import { memo } from "react"
import { RouteComponentProps } from "react-router-dom"
import { Helmet } from "react-helmet"
import styles from "./styles.module.scss"
type Props = RouteComponentProps
const NotFound = ({ staticContext }: Props) => {
// We have to check if staticContext exists
// because it will be undefined if rendered through a BrowserRoute
/* istanbul ignore next */
if (staticContext) staticContext.statusCode = 404
return (
<div className={styles.NotFound}>
<Helmet title="Oops" />
<p>Oops, Page was not found!</p>
</div>
)
}
export default memo(NotFound)

View File

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

47
src/pages/UserInfo/UserInfo.tsx Executable file
View File

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

View File

@@ -0,0 +1,66 @@
/**
* @jest-environment jsdom
*/
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("<UserInfo />", () => {
const mockData = {
id: "1",
name: "PeL",
phone: "+886 0970...",
email: "forceoranj@gmail.com",
website: "https://www.parisestludique.fr",
}
const { id } = mockData
const renderHelper = (reducer = {}) => {
const { dispatch, ProviderWithStore } = mockStore({ userData: reducer })
const { container } = render(
<ProviderWithStore>
<MemoryRouter>
{/*
@ts-expect-error */}
<UserInfo match={{ params: { id } }} />
</MemoryRouter>
</ProviderWithStore>
)
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" } }
expect(renderHelper(reducer).firstChild).toMatchSnapshot()
})
it("renders an error if loading failed", () => {
const reducer = { [id]: { readyStatus: "failure" } }
expect(renderHelper(reducer).firstChild).toMatchSnapshot()
})
it("renders the <Info /> if loading was successful", () => {
const reducer = { [id]: { readyStatus: "success", item: mockData } }
expect(renderHelper(reducer).firstChild).toMatchSnapshot()
})
})

View File

@@ -0,0 +1,63 @@
// 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>
Loading...
</p>
</div>
`;
exports[`<UserInfo /> renders the loading status if requesting data 1`] = `
<div
class="UserInfo"
>
<p>
Loading...
</p>
</div>
`;

15
src/pages/UserInfo/index.tsx Executable file
View File

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

View File

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