mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-09-11 05:46:28 +02:00
Initial commit
This commit is contained in:
42
src/app/__tests__/App.tsx
Executable file
42
src/app/__tests__/App.tsx
Executable file
@@ -0,0 +1,42 @@
|
||||
import renderer from "react-test-renderer"
|
||||
import { Provider } from "react-redux"
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
|
||||
import App from ".."
|
||||
|
||||
describe("<App />", () => {
|
||||
it("renders", () => {
|
||||
const mockStore = {
|
||||
default: () => null,
|
||||
subscribe: () => null,
|
||||
dispatch: () => null,
|
||||
getState: () => ({ home: () => null }),
|
||||
}
|
||||
const mockRoute = {
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
exact: true,
|
||||
component: () => (
|
||||
<div>
|
||||
<h1>Welcome Home!</h1>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const tree = renderer
|
||||
.create(
|
||||
// @ts-expect-error
|
||||
<Provider store={mockStore}>
|
||||
<MemoryRouter>
|
||||
<App route={mockRoute} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
)
|
||||
.toJSON()
|
||||
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
30
src/app/__tests__/__snapshots__/App.tsx.snap
Normal file
30
src/app/__tests__/__snapshots__/App.tsx.snap
Normal file
@@ -0,0 +1,30 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<App /> renders 1`] = `
|
||||
<div
|
||||
className="App"
|
||||
>
|
||||
<a
|
||||
className="header"
|
||||
href="/"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
role="presentation"
|
||||
src="IMAGE_MOCK"
|
||||
/>
|
||||
<h1>
|
||||
<em>
|
||||
REACT COOL STARTER
|
||||
</em>
|
||||
</h1>
|
||||
</a>
|
||||
<hr />
|
||||
<div>
|
||||
<h1>
|
||||
Welcome Home!
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
30
src/app/index.tsx
Executable file
30
src/app/index.tsx
Executable file
@@ -0,0 +1,30 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import { RouteConfig, renderRoutes } from "react-router-config"
|
||||
import { Helmet } from "react-helmet"
|
||||
|
||||
import logo from "../static/logo.svg"
|
||||
import config from "../config"
|
||||
// Import your global styles here
|
||||
import "normalize.css/normalize.css"
|
||||
import styles from "./styles.module.scss"
|
||||
|
||||
interface Route {
|
||||
route: { routes: RouteConfig[] }
|
||||
}
|
||||
|
||||
const App = ({ route }: Route): JSX.Element => (
|
||||
<div className={styles.App}>
|
||||
<Helmet {...config.APP} />
|
||||
<Link to="/" className={styles.header}>
|
||||
<img src={logo} alt="Logo" role="presentation" />
|
||||
<h1>
|
||||
<em>{config.APP.title}</em>
|
||||
</h1>
|
||||
</Link>
|
||||
<hr />
|
||||
{/* Child routes won't render without this */}
|
||||
{renderRoutes(route.routes)}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default App
|
30
src/app/styles.module.scss
Executable file
30
src/app/styles.module.scss
Executable file
@@ -0,0 +1,30 @@
|
||||
@import "../theme/variables";
|
||||
|
||||
body {
|
||||
background-color: $color-dark-gray;
|
||||
font-family: "Helvetica-Light", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.App {
|
||||
color: $color-white;
|
||||
|
||||
.header {
|
||||
color: inherit;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
padding: 15px;
|
||||
text-decoration: none;
|
||||
|
||||
img {
|
||||
float: left;
|
||||
height: 70px;
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
29
src/client/index.tsx
Executable file
29
src/client/index.tsx
Executable file
@@ -0,0 +1,29 @@
|
||||
import ReactDOM from "react-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { ConnectedRouter } from "connected-react-router"
|
||||
import { RouteConfig, renderRoutes } from "react-router-config"
|
||||
import { loadableReady } from "@loadable/component"
|
||||
|
||||
import createStore from "../store"
|
||||
import routes from "../routes"
|
||||
|
||||
// Get the initial state from server-side rendering
|
||||
const initialState = window.__INITIAL_STATE__
|
||||
const { store, history } = createStore({ initialState })
|
||||
|
||||
const render = (Routes: RouteConfig[]) =>
|
||||
ReactDOM.hydrate(
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>{renderRoutes(Routes)}</ConnectedRouter>
|
||||
</Provider>,
|
||||
document.getElementById("react-view")
|
||||
)
|
||||
|
||||
// loadable-component setup
|
||||
loadableReady(() => render(routes as RouteConfig[]))
|
||||
|
||||
/**
|
||||
* A temporary workaround for Webpack v5 + HMR, why? see this issue: https://github.com/webpack-contrib/webpack-hot-middleware/issues/390
|
||||
*/
|
||||
// @ts-expect-error
|
||||
if (module.hot) module.hot.accept()
|
39
src/components/ErrorBoundary/__tests__/ErrorBoundary.tsx
Normal file
39
src/components/ErrorBoundary/__tests__/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { ReactNode } from "react"
|
||||
import { render, screen } from "@testing-library/react"
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
|
||||
import ErrorBoundary from "../index"
|
||||
|
||||
describe("<ErrorBoundary />", () => {
|
||||
const tree = (children?: ReactNode) =>
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</MemoryRouter>
|
||||
).container.firstChild
|
||||
|
||||
it("renders nothing if no children", () => {
|
||||
expect(tree()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("renders children if no error", () => {
|
||||
expect(
|
||||
tree(
|
||||
<div>
|
||||
<h1>I am PeL</h1>
|
||||
</div>
|
||||
)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("renders error view if an error occurs", () => {
|
||||
console.error = jest.fn()
|
||||
|
||||
tree(<div>{new Error()}</div>)
|
||||
|
||||
expect(screen.getByTestId("error-view")).toBeInTheDocument()
|
||||
})
|
||||
})
|
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ErrorBoundary /> renders children if no error 1`] = `
|
||||
<div>
|
||||
<h1>
|
||||
I am PeL
|
||||
</h1>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<ErrorBoundary /> renders nothing if no children 1`] = `null`;
|
45
src/components/ErrorBoundary/index.tsx
Executable file
45
src/components/ErrorBoundary/index.tsx
Executable file
@@ -0,0 +1,45 @@
|
||||
import { ReactNode, PureComponent } from "react"
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode
|
||||
}
|
||||
interface State {
|
||||
error: Error | null
|
||||
errorInfo: { componentStack: string } | null
|
||||
}
|
||||
|
||||
class ErrorBoundary extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = { error: null, errorInfo: null }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: { componentStack: string }): void {
|
||||
// Catch errors in any components below and re-render with error message
|
||||
this.setState({ error, errorInfo })
|
||||
|
||||
// You can also log error messages to an error reporting service here
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
const { children } = this.props
|
||||
const { errorInfo, error } = this.state
|
||||
|
||||
// If there's an error, render error path
|
||||
return errorInfo ? (
|
||||
<div data-testid="error-view">
|
||||
<h2>Something went wrong.</h2>
|
||||
<details style={{ whiteSpace: "pre-wrap" }}>
|
||||
{error && error.toString()}
|
||||
<br />
|
||||
{errorInfo.componentStack}
|
||||
</details>
|
||||
</div>
|
||||
) : (
|
||||
children || null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
27
src/components/Info/__tests__/Info.tsx
Executable file
27
src/components/Info/__tests__/Info.tsx
Executable file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @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={{
|
||||
id: 1,
|
||||
name: "PeL",
|
||||
phone: "+886 0970...",
|
||||
email: "forceoranj@gmail.com",
|
||||
website: "https://www.parisestludique.fr",
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
).container.firstChild
|
||||
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
29
src/components/Info/__tests__/__snapshots__/Info.tsx.snap
Normal file
29
src/components/Info/__tests__/__snapshots__/Info.tsx.snap
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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>
|
||||
`;
|
22
src/components/Info/index.tsx
Executable file
22
src/components/Info/index.tsx
Executable file
@@ -0,0 +1,22 @@
|
||||
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)
|
9
src/components/Info/styles.module.scss
Executable file
9
src/components/Info/styles.module.scss
Executable file
@@ -0,0 +1,9 @@
|
||||
.user-card {
|
||||
ul {
|
||||
padding-left: 17px;
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
41
src/components/JavGameList/__tests__/JavGameList.tsx
Normal file
41
src/components/JavGameList/__tests__/JavGameList.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @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={[
|
||||
{
|
||||
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",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
).container.firstChild
|
||||
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
@@ -0,0 +1,20 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<List /> renders 1`] = `
|
||||
<div
|
||||
class="JavGameList"
|
||||
>
|
||||
<h4>
|
||||
JAV Games
|
||||
</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="/UserInfo/5"
|
||||
>
|
||||
6 qui prend!
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
24
src/components/JavGameList/index.tsx
Normal file
24
src/components/JavGameList/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { memo } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import { JavGame } from "../../services/javGames"
|
||||
import styles from "./styles.module.scss"
|
||||
|
||||
interface Props {
|
||||
items: JavGame[]
|
||||
}
|
||||
|
||||
const List = ({ items }: Props) => (
|
||||
<div className={styles.JavGameList}>
|
||||
<h4>JAV Games</h4>
|
||||
<ul>
|
||||
{items.map(({ id, titre }) => (
|
||||
<li key={id}>
|
||||
<Link to={`/UserInfo/${id}`}>{titre}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default memo(List)
|
17
src/components/JavGameList/styles.module.scss
Normal file
17
src/components/JavGameList/styles.module.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
29
src/components/List/__tests__/List.tsx
Executable file
29
src/components/List/__tests__/List.tsx
Executable file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @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={[
|
||||
{
|
||||
id: 1,
|
||||
name: "PeL",
|
||||
phone: "+886 0970...",
|
||||
email: "forceoranj@gmail.com",
|
||||
website: "https://www.parisestludique.fr",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
).container.firstChild
|
||||
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
20
src/components/List/__tests__/__snapshots__/List.tsx.snap
Normal file
20
src/components/List/__tests__/__snapshots__/List.tsx.snap
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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>
|
||||
`;
|
24
src/components/List/index.tsx
Executable file
24
src/components/List/index.tsx
Executable file
@@ -0,0 +1,24 @@
|
||||
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(({ id, name }) => (
|
||||
<li key={id}>
|
||||
<Link to={`/UserInfo/${id}`}>{name}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default memo(List)
|
17
src/components/List/styles.module.scss
Executable file
17
src/components/List/styles.module.scss
Executable file
@@ -0,0 +1,17 @@
|
||||
@import "../../theme/variables";
|
||||
|
||||
.UserList {
|
||||
color: $color-white;
|
||||
|
||||
ul {
|
||||
padding-left: 17px;
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
19
src/components/Loading/__tests__/Loading.tsx
Executable file
19
src/components/Loading/__tests__/Loading.tsx
Executable file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { render } from "@testing-library/react"
|
||||
import { MemoryRouter } from "react-router-dom"
|
||||
|
||||
import Loading from "../index"
|
||||
|
||||
describe("<Loading />", () => {
|
||||
it("renders", () => {
|
||||
const tree = render(
|
||||
<MemoryRouter>
|
||||
<Loading />
|
||||
</MemoryRouter>
|
||||
).container.firstChild
|
||||
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
})
|
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Loading /> renders 1`] = `
|
||||
<div
|
||||
class="Loading"
|
||||
>
|
||||
<p>
|
||||
Loading...
|
||||
</p>
|
||||
</div>
|
||||
`;
|
9
src/components/Loading/index.tsx
Executable file
9
src/components/Loading/index.tsx
Executable file
@@ -0,0 +1,9 @@
|
||||
import styles from "./styles.module.scss"
|
||||
|
||||
const Loading = (): JSX.Element => (
|
||||
<div className={styles.Loading}>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Loading
|
3
src/components/Loading/styles.module.scss
Executable file
3
src/components/Loading/styles.module.scss
Executable file
@@ -0,0 +1,3 @@
|
||||
.Loading {
|
||||
padding: 0 15px;
|
||||
}
|
7
src/components/index.ts
Executable file
7
src/components/index.ts
Executable file
@@ -0,0 +1,7 @@
|
||||
import List from "./List"
|
||||
import JavGameList from "./JavGameList"
|
||||
import Info from "./Info"
|
||||
import ErrorBoundary from "./ErrorBoundary"
|
||||
import Loading from "./Loading"
|
||||
|
||||
export { List, JavGameList, Info, ErrorBoundary, Loading }
|
16
src/config/default.ts
Executable file
16
src/config/default.ts
Executable file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
HOST: "localhost",
|
||||
PORT: 3000,
|
||||
API_URL: "",
|
||||
APP: {
|
||||
htmlAttributes: { lang: "en" },
|
||||
title: "REACT COOL STARTER",
|
||||
titleTemplate: "REACT COOL STARTER - %s",
|
||||
meta: [
|
||||
{
|
||||
name: "description",
|
||||
content: "The best react universal starter boilerplate in the world.",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
4
src/config/index.ts
Executable file
4
src/config/index.ts
Executable file
@@ -0,0 +1,4 @@
|
||||
import defaultConfig from "./default"
|
||||
import prodConfig from "./prod"
|
||||
|
||||
export default __DEV__ ? defaultConfig : { ...defaultConfig, ...prodConfig }
|
3
src/config/prod.ts
Executable file
3
src/config/prod.ts
Executable file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
PORT: 8080,
|
||||
}
|
133
src/gsheets/jav.ts
Normal file
133
src/gsheets/jav.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import readline from "readline"
|
||||
import _ from "lodash"
|
||||
import { Request, Response, NextFunction } from "express"
|
||||
import { google } from "googleapis"
|
||||
|
||||
const SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
|
||||
const TOKEN_PATH = path.resolve(process.cwd(), "access/token.json")
|
||||
const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json")
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export const getJAVGameList = async (
|
||||
_request: Request,
|
||||
response: Response,
|
||||
_next: NextFunction
|
||||
): Promise<void> => {
|
||||
const auth = await authorize(JSON.parse(fs.readFileSync(CRED_PATH, "utf8")))
|
||||
const sheets = google.sheets({ version: "v4", auth })
|
||||
const r = await sheets.spreadsheets.values.get({
|
||||
spreadsheetId: "1pMMKcYx6NXLOqNn6pLHJTPMTOLRYZmSNg2QQcAu7-Pw",
|
||||
range: "Ongoing!A1:T",
|
||||
})
|
||||
|
||||
console.log("r?.data?.values", r?.data?.values)
|
||||
if (r?.data?.values && _.isArray(r.data.values)) {
|
||||
const list = _.map(r.data.values, (val: any) => ({
|
||||
id: val[0],
|
||||
titre: val[1],
|
||||
}))
|
||||
response.status(200).json(list)
|
||||
}
|
||||
// if (r?.data?.values) {
|
||||
// const rows: JAVGame[] = r.data.values as JAVGame[]
|
||||
// if (rows) {
|
||||
// if (rows.length) {
|
||||
// console.log('Name, Major:')
|
||||
// // Print columns A and E, which correspond to indices 0 and 4.
|
||||
// rows.map((row) => {
|
||||
// console.log(`${row[0]}, ${row[4]}`)
|
||||
// })
|
||||
// return { data: rows }
|
||||
// } else {
|
||||
// console.log('No data found.')
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export const getJAVGameData = async (
|
||||
_request: Request,
|
||||
response: Response,
|
||||
_next: NextFunction
|
||||
): Promise<void> => {
|
||||
console.log("CRED_PATH", CRED_PATH)
|
||||
console.log("fs.readFileSync(CRED_PATH, 'utf8')")
|
||||
const auth = await authorize(JSON.parse(fs.readFileSync(CRED_PATH, "utf8")))
|
||||
const sheets = google.sheets({ version: "v4", auth })
|
||||
const r = await sheets.spreadsheets.values.get({
|
||||
spreadsheetId: "1pMMKcYx6NXLOqNn6pLHJTPMTOLRYZmSNg2QQcAu7-Pw",
|
||||
range: "Ongoing!A1:T",
|
||||
})
|
||||
|
||||
console.log("r?.data?.values", r?.data?.values)
|
||||
response.status(200).json(r?.data?.values)
|
||||
// if (r?.data?.values) {
|
||||
// const rows: JAVGame[] = r.data.values as JAVGame[]
|
||||
// if (rows) {
|
||||
// if (rows.length) {
|
||||
// console.log('Name, Major:')
|
||||
// // Print columns A and E, which correspond to indices 0 and 4.
|
||||
// rows.map((row) => {
|
||||
// console.log(`${row[0]}, ${row[4]}`)
|
||||
// })
|
||||
// return { data: rows }
|
||||
// } else {
|
||||
// console.log('No data found.')
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
async function authorize(cred: any) {
|
||||
const {
|
||||
client_secret: clientSecret,
|
||||
client_id: clientId,
|
||||
redirect_uris: redirectUris,
|
||||
} = cred.web
|
||||
const oAuth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUris[0])
|
||||
|
||||
if (fs.existsSync(TOKEN_PATH)) {
|
||||
oAuth2Client.setCredentials(JSON.parse(fs.readFileSync(TOKEN_PATH, "utf8")))
|
||||
return oAuth2Client
|
||||
}
|
||||
|
||||
return getNewToken<typeof oAuth2Client>(oAuth2Client)
|
||||
}
|
||||
|
||||
async function getNewToken<T = any>(oAuth2Client: any): Promise<T> {
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
scope: SCOPES,
|
||||
})
|
||||
|
||||
console.log("Authorize this app by visiting this url:", authUrl)
|
||||
const code = await readlineAsync("Enter the code from that page here: ")
|
||||
const token = await new Promise((resolve, reject) => {
|
||||
oAuth2Client.getToken(code, (err: any, _token: any) =>
|
||||
err ? reject(err) : resolve(_token)
|
||||
)
|
||||
})
|
||||
oAuth2Client.setCredentials(token)
|
||||
// Store the token to disk for later program executions
|
||||
fs.writeFileSync(TOKEN_PATH, JSON.stringify(token))
|
||||
console.log("Token stored to", TOKEN_PATH)
|
||||
|
||||
return oAuth2Client
|
||||
}
|
||||
|
||||
async function readlineAsync(question: string) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close()
|
||||
resolve(answer)
|
||||
})
|
||||
})
|
||||
}
|
46
src/pages/Home/Home.tsx
Executable file
46
src/pages/Home/Home.tsx
Executable 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)
|
78
src/pages/Home/__tests__/Home.tsx
Executable file
78
src/pages/Home/__tests__/Home.tsx
Executable 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()
|
||||
})
|
||||
})
|
54
src/pages/Home/__tests__/__snapshots__/Home.tsx.snap
Normal file
54
src/pages/Home/__tests__/__snapshots__/Home.tsx.snap
Normal 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
15
src/pages/Home/index.tsx
Executable 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 }
|
3
src/pages/Home/styles.module.scss
Executable file
3
src/pages/Home/styles.module.scss
Executable file
@@ -0,0 +1,3 @@
|
||||
.Home {
|
||||
padding: 0 15px;
|
||||
}
|
21
src/pages/NotFound/__tests__/NotFound.tsx
Executable file
21
src/pages/NotFound/__tests__/NotFound.tsx
Executable 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()
|
||||
})
|
||||
})
|
11
src/pages/NotFound/__tests__/__snapshots__/NotFound.tsx.snap
Normal file
11
src/pages/NotFound/__tests__/__snapshots__/NotFound.tsx.snap
Normal 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
23
src/pages/NotFound/index.tsx
Executable 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)
|
3
src/pages/NotFound/styles.module.scss
Executable file
3
src/pages/NotFound/styles.module.scss
Executable file
@@ -0,0 +1,3 @@
|
||||
.NotFound {
|
||||
padding: 0 15px;
|
||||
}
|
47
src/pages/UserInfo/UserInfo.tsx
Executable file
47
src/pages/UserInfo/UserInfo.tsx
Executable 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)
|
66
src/pages/UserInfo/__tests__/UserInfo.tsx
Executable file
66
src/pages/UserInfo/__tests__/UserInfo.tsx
Executable 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()
|
||||
})
|
||||
})
|
63
src/pages/UserInfo/__tests__/__snapshots__/UserInfo.tsx.snap
Normal file
63
src/pages/UserInfo/__tests__/__snapshots__/UserInfo.tsx.snap
Normal 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
15
src/pages/UserInfo/index.tsx
Executable 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 }
|
3
src/pages/UserInfo/styles.module.scss
Executable file
3
src/pages/UserInfo/styles.module.scss
Executable file
@@ -0,0 +1,3 @@
|
||||
.UserInfo {
|
||||
padding: 0 15px;
|
||||
}
|
28
src/routes/index.ts
Executable file
28
src/routes/index.ts
Executable file
@@ -0,0 +1,28 @@
|
||||
import { RouteConfig } from "react-router-config"
|
||||
|
||||
import App from "../app"
|
||||
import AsyncHome, { loadData as loadHomeData } from "../pages/Home"
|
||||
import AsyncUserInfo, { loadData as loadUserInfoData } from "../pages/UserInfo"
|
||||
import NotFound from "../pages/NotFound"
|
||||
|
||||
export default [
|
||||
{
|
||||
component: App,
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
exact: true,
|
||||
component: AsyncHome, // Add your page here
|
||||
loadData: loadHomeData, // Add your pre-fetch method here
|
||||
},
|
||||
{
|
||||
path: "/UserInfo/:id",
|
||||
component: AsyncUserInfo,
|
||||
loadData: loadUserInfoData,
|
||||
},
|
||||
{
|
||||
component: NotFound,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as RouteConfig[]
|
28
src/server/devServer.ts
Normal file
28
src/server/devServer.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Express } from "express"
|
||||
import chalk from "chalk"
|
||||
|
||||
import config from "../config"
|
||||
|
||||
export default (app: Express): void => {
|
||||
const webpack = require("webpack")
|
||||
const webpackConfig = require("../../webpack/client.config").default
|
||||
const compiler = webpack(webpackConfig)
|
||||
const instance = require("webpack-dev-middleware")(compiler, {
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
serverSideRender: true,
|
||||
})
|
||||
|
||||
app.use(instance)
|
||||
app.use(
|
||||
require("webpack-hot-middleware")(compiler, {
|
||||
log: false,
|
||||
path: "/__webpack_hmr",
|
||||
heartbeat: 10 * 1000,
|
||||
})
|
||||
)
|
||||
|
||||
instance.waitUntilValid(() => {
|
||||
const url = `http://${config.HOST}:${config.PORT}`
|
||||
console.info(chalk.green(`==> 🌎 Listening at ${url}`))
|
||||
})
|
||||
}
|
42
src/server/index.ts
Executable file
42
src/server/index.ts
Executable file
@@ -0,0 +1,42 @@
|
||||
import path from "path"
|
||||
import express from "express"
|
||||
import logger from "morgan"
|
||||
import compression from "compression"
|
||||
import helmet from "helmet"
|
||||
import hpp from "hpp"
|
||||
import favicon from "serve-favicon"
|
||||
import chalk from "chalk"
|
||||
|
||||
import devServer from "./devServer"
|
||||
import ssr from "./ssr"
|
||||
|
||||
import { getJAVGameList } from "../gsheets/jav"
|
||||
import config from "../config"
|
||||
|
||||
const app = express()
|
||||
|
||||
// Use helmet to secure Express with various HTTP headers
|
||||
app.use(helmet({ contentSecurityPolicy: false }))
|
||||
// Prevent HTTP parameter pollution
|
||||
app.use(hpp())
|
||||
// Compress all requests
|
||||
app.use(compression())
|
||||
|
||||
// Use for http request debug (show errors only)
|
||||
app.use(logger("dev", { skip: (_, res) => res.statusCode < 400 }))
|
||||
app.use(favicon(path.resolve(process.cwd(), "public/favicon.ico")))
|
||||
app.use(express.static(path.resolve(process.cwd(), "public")))
|
||||
|
||||
// Enable dev-server in development
|
||||
if (__DEV__) devServer(app)
|
||||
|
||||
// Google Sheets requests
|
||||
app.get("/javGames", getJAVGameList)
|
||||
|
||||
// Use React server-side rendering middleware
|
||||
app.get("*", ssr)
|
||||
|
||||
// @ts-expect-error
|
||||
app.listen(config.PORT, config.HOST, (error) => {
|
||||
if (error) console.error(chalk.red(`==> 😭 OMG!!! ${error}`))
|
||||
})
|
63
src/server/renderHtml.ts
Executable file
63
src/server/renderHtml.ts
Executable file
@@ -0,0 +1,63 @@
|
||||
import { ChunkExtractor } from "@loadable/server"
|
||||
import { HelmetData } from "react-helmet"
|
||||
import serialize from "serialize-javascript"
|
||||
import { minify } from "html-minifier"
|
||||
|
||||
export default (
|
||||
head: HelmetData,
|
||||
extractor: ChunkExtractor,
|
||||
htmlContent: string,
|
||||
initialState: typeof window.__INITIAL_STATE__
|
||||
): any => {
|
||||
const html = `
|
||||
<!doctype html>
|
||||
<html ${head.htmlAttributes.toString()}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000" />
|
||||
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
${head.title.toString()}
|
||||
${head.base.toString()}
|
||||
${head.meta.toString()}
|
||||
${head.link.toString()}
|
||||
|
||||
<!-- Insert bundled styles into <link> tag -->
|
||||
${extractor.getLinkTags()}
|
||||
${extractor.getStyleTags()}
|
||||
</head>
|
||||
<body>
|
||||
<!-- Insert the router, which passed from server-side -->
|
||||
<div id="react-view">${htmlContent}</div>
|
||||
|
||||
<!-- Store the initial state into window -->
|
||||
<script>
|
||||
// Use serialize-javascript for mitigating XSS attacks. See the following security issues:
|
||||
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
|
||||
window.__INITIAL_STATE__=${serialize(initialState)};
|
||||
</script>
|
||||
|
||||
<!-- Insert bundled scripts into <script> tag -->
|
||||
${extractor.getScriptTags()}
|
||||
${head.script.toString()}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// html-minifier configuration, refer to "https://github.com/kangax/html-minifier" for more configuration
|
||||
const minifyConfig = {
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
trimCustomFragments: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
minifyURLs: true,
|
||||
}
|
||||
|
||||
// Minify HTML in production
|
||||
return __DEV__ ? html : minify(html, minifyConfig)
|
||||
}
|
83
src/server/ssr.tsx
Normal file
83
src/server/ssr.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import path from "path"
|
||||
import { renderToString } from "react-dom/server"
|
||||
import { StaticRouter } from "react-router-dom"
|
||||
import { renderRoutes, matchRoutes } from "react-router-config"
|
||||
import { Provider } from "react-redux"
|
||||
import { ChunkExtractor } from "@loadable/server"
|
||||
import { Helmet } from "react-helmet"
|
||||
import chalk from "chalk"
|
||||
import { Request, Response, NextFunction } from "express"
|
||||
import { Action } from "@reduxjs/toolkit"
|
||||
|
||||
import createStore from "../store"
|
||||
import renderHtml from "./renderHtml"
|
||||
import routes from "../routes"
|
||||
|
||||
export default async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
const { store } = createStore({ url: req.url })
|
||||
|
||||
// The method for loading data from server-side
|
||||
const loadBranchData = (): Promise<any> => {
|
||||
const branch = matchRoutes(routes, req.path)
|
||||
const promises = branch.map(({ route, match }) => {
|
||||
if (route.loadData)
|
||||
return Promise.all(
|
||||
route
|
||||
.loadData({
|
||||
params: match.params,
|
||||
getState: store.getState,
|
||||
req,
|
||||
res,
|
||||
})
|
||||
.map((item: Action) => store.dispatch(item))
|
||||
)
|
||||
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
try {
|
||||
// Load data from server-side first
|
||||
await loadBranchData()
|
||||
|
||||
const statsFile = path.resolve(process.cwd(), "public/loadable-stats")
|
||||
const extractor = new ChunkExtractor({ statsFile })
|
||||
|
||||
const staticContext: Record<string, any> = {}
|
||||
const App = extractor.collectChunks(
|
||||
<Provider store={store}>
|
||||
{/* Setup React-Router server-side rendering */}
|
||||
<StaticRouter location={req.path} context={staticContext}>
|
||||
{renderRoutes(routes)}
|
||||
</StaticRouter>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
const initialState = store.getState()
|
||||
const htmlContent = renderToString(App)
|
||||
// head must be placed after "renderToString"
|
||||
// see: https://github.com/nfl/react-helmet#server-usage
|
||||
const head = Helmet.renderStatic()
|
||||
|
||||
// Check if the render result contains a redirect, if so we need to set
|
||||
// the specific status and redirect header and end the response
|
||||
if (staticContext.url) {
|
||||
res.status(301).setHeader("Location", staticContext.url)
|
||||
res.end()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Pass the route and initial state into html template, the "statusCode" comes from <NotFound />
|
||||
res.status(staticContext.statusCode === "404" ? 404 : 200).send(
|
||||
renderHtml(head, extractor, htmlContent, initialState)
|
||||
)
|
||||
} catch (error) {
|
||||
res.status(404).send("Not Found :(")
|
||||
console.error(chalk.red(`==> 😭 Rendering routes error: ${error}`))
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
51
src/services/javGames.ts
Normal file
51
src/services/javGames.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import axios from "axios"
|
||||
|
||||
import config from "../config"
|
||||
|
||||
export interface JavGame {
|
||||
id: number
|
||||
titre: string
|
||||
auteur: string
|
||||
editeur: string
|
||||
minJoueurs: number
|
||||
maxJoueurs: number
|
||||
duree: number
|
||||
type: "Ambiance" | "Famille" | "Expert" | ""
|
||||
poufpaf: string
|
||||
photo: string
|
||||
bggPhoto: string
|
||||
bggId: number
|
||||
exemplaires: number // Defaults to 1
|
||||
dispoPret: number
|
||||
nonRangee: number
|
||||
horodatage: string
|
||||
ean: string
|
||||
}
|
||||
|
||||
export interface JavGameList {
|
||||
data?: JavGame[]
|
||||
error?: Error
|
||||
}
|
||||
|
||||
export interface JavGameData {
|
||||
data?: JavGame
|
||||
error?: Error
|
||||
}
|
||||
|
||||
export const getJavGameList = async (): Promise<JavGameList> => {
|
||||
try {
|
||||
const { data } = await axios.get(`${config.API_URL}/javGames`)
|
||||
return { data }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
}
|
||||
}
|
||||
|
||||
export const getJavGameData = async (id: string): Promise<JavGameData> => {
|
||||
try {
|
||||
const { data } = await axios.get(`${config.API_URL}/users/${id}`)
|
||||
return { data }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
}
|
||||
}
|
37
src/services/jsonPlaceholder.ts
Normal file
37
src/services/jsonPlaceholder.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import axios from "axios"
|
||||
|
||||
export interface User {
|
||||
id: 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: string): Promise<UserData> => {
|
||||
try {
|
||||
const { data } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`)
|
||||
return { data }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
}
|
||||
}
|
13
src/static/logo.svg
Executable file
13
src/static/logo.svg
Executable file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="570" height="510" viewBox="0, 0, 570, 510">
|
||||
<g id="Background">
|
||||
<rect x="0" y="0" width="570" height="510" fill="#000000" fill-opacity="0"/>
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<path d="M334.696,254.628 C334.696,282.334 312.235,304.795 284.529,304.795 C256.823,304.795 234.362,282.334 234.362,254.628 C234.362,226.922 256.823,204.461 284.529,204.461 C312.235,204.461 334.696,226.922 334.696,254.628 z" fill="#00D8FF"/>
|
||||
<path d="M284.529,152.628 C351.885,152.628 414.457,162.293 461.636,178.535 C518.48,198.104 553.43,227.768 553.43,254.628 C553.43,282.619 516.389,314.131 455.347,334.356 C409.196,349.647 348.468,357.628 284.529,357.628 C218.975,357.628 156.899,350.136 110.239,334.187 C51.193,314.005 15.628,282.084 15.628,254.628 C15.628,227.986 48.998,198.552 105.043,179.012 C152.398,162.503 216.515,152.628 284.529,152.628 z" fill-opacity="0" stroke="#00D8FF" stroke-width="24" stroke-miterlimit="10"/>
|
||||
<path d="M195.736,203.922 C229.385,145.574 269.017,96.198 306.656,63.442 C352.006,23.976 395.163,8.519 418.431,21.937 C442.679,35.92 451.473,83.751 438.498,146.733 C428.688,194.351 405.264,250.945 373.322,306.334 C340.573,363.122 303.072,413.153 265.945,445.606 C218.964,486.674 173.545,501.535 149.76,487.819 C126.681,474.509 117.854,430.898 128.926,372.586 C138.281,323.316 161.758,262.841 195.736,203.922 z" fill-opacity="0" stroke="#00D8FF" stroke-width="24" stroke-miterlimit="10"/>
|
||||
<path d="M195.821,306.482 C162.075,248.19 139.09,189.195 129.509,140.227 C117.965,81.228 126.127,36.118 149.373,22.661 C173.597,8.637 219.428,24.905 267.513,67.601 C303.869,99.881 341.201,148.438 373.236,203.774 C406.08,260.507 430.697,317.983 440.272,366.356 C452.389,427.569 442.581,474.34 418.819,488.096 C395.762,501.444 353.57,487.312 308.58,448.597 C270.567,415.886 229.898,365.344 195.821,306.482 z" fill-opacity="0" stroke="#00D8FF" stroke-width="24" stroke-miterlimit="10"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
96
src/store/__tests__/javGameList.ts
Normal file
96
src/store/__tests__/javGameList.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import axios from "axios"
|
||||
|
||||
import mockStore from "../../utils/mockStore"
|
||||
import javGameList, {
|
||||
initialState,
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
fetchJavGameList,
|
||||
} from "../javGameList"
|
||||
|
||||
jest.mock("axios")
|
||||
|
||||
const mockData = [
|
||||
{
|
||||
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",
|
||||
},
|
||||
]
|
||||
const mockError = "Oops! Something went wrong."
|
||||
|
||||
describe("javGameList reducer", () => {
|
||||
it("should handle initial state", () => {
|
||||
// @ts-expect-error
|
||||
expect(javGameList(undefined, {})).toEqual(initialState)
|
||||
})
|
||||
|
||||
it("should handle requesting correctly", () => {
|
||||
expect(javGameList(undefined, { type: getRequesting.type })).toEqual({
|
||||
readyStatus: "request",
|
||||
items: [],
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle success correctly", () => {
|
||||
expect(javGameList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({
|
||||
...initialState,
|
||||
readyStatus: "success",
|
||||
items: mockData,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle failure correctly", () => {
|
||||
expect(javGameList(undefined, { type: getFailure.type, payload: mockError })).toEqual({
|
||||
...initialState,
|
||||
readyStatus: "failure",
|
||||
error: mockError,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("javGameList action", () => {
|
||||
it("fetches javGame list successful", async () => {
|
||||
const { dispatch, getActions } = mockStore()
|
||||
const expectedActions = [
|
||||
{ type: getRequesting.type },
|
||||
{ type: getSuccess.type, payload: mockData },
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
axios.get.mockResolvedValue({ data: mockData })
|
||||
|
||||
await dispatch(fetchJavGameList())
|
||||
expect(getActions()).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
it("fetches javGame list failed", async () => {
|
||||
const { dispatch, getActions } = mockStore()
|
||||
const expectedActions = [
|
||||
{ type: getRequesting.type },
|
||||
{ type: getFailure.type, payload: mockError },
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
axios.get.mockRejectedValue({ message: mockError })
|
||||
|
||||
await dispatch(fetchJavGameList())
|
||||
expect(getActions()).toEqual(expectedActions)
|
||||
})
|
||||
})
|
83
src/store/__tests__/userData.ts
Normal file
83
src/store/__tests__/userData.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import axios from "axios"
|
||||
|
||||
import mockStore from "../../utils/mockStore"
|
||||
import userData, { getRequesting, getSuccess, getFailure, fetchUserData } from "../userData"
|
||||
|
||||
jest.mock("axios")
|
||||
|
||||
const mockData = {
|
||||
id: 1,
|
||||
name: "PeL",
|
||||
phone: "+886 0970...",
|
||||
email: "forceoranj@gmail.com",
|
||||
website: "https://www.parisestludique.fr",
|
||||
}
|
||||
const { id } = mockData
|
||||
const mockError = "Oops! Something went wrong."
|
||||
|
||||
describe("userData reducer", () => {
|
||||
it("should handle initial state correctly", () => {
|
||||
// @ts-expect-error
|
||||
expect(userData(undefined, {})).toEqual({})
|
||||
})
|
||||
|
||||
it("should handle requesting correctly", () => {
|
||||
expect(userData(undefined, { type: getRequesting.type, payload: id })).toEqual({
|
||||
[id]: { readyStatus: "request" },
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle success correctly", () => {
|
||||
expect(
|
||||
userData(undefined, {
|
||||
type: getSuccess.type,
|
||||
payload: { id, item: mockData },
|
||||
})
|
||||
).toEqual({
|
||||
[id]: { readyStatus: "success", item: mockData },
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle failure correctly", () => {
|
||||
expect(
|
||||
userData(undefined, {
|
||||
type: getFailure.type,
|
||||
payload: { id, error: mockError },
|
||||
})
|
||||
).toEqual({
|
||||
[id]: { 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 } },
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
axios.get.mockResolvedValue({ data: mockData })
|
||||
|
||||
await dispatch(fetchUserData(strId))
|
||||
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 } },
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
axios.get.mockRejectedValue({ message: mockError })
|
||||
|
||||
await dispatch(fetchUserData(strId))
|
||||
expect(getActions()).toEqual(expectedActions)
|
||||
})
|
||||
})
|
84
src/store/__tests__/userList.ts
Normal file
84
src/store/__tests__/userList.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import axios from "axios"
|
||||
|
||||
import mockStore from "../../utils/mockStore"
|
||||
import userList, {
|
||||
initialState,
|
||||
getRequesting,
|
||||
getSuccess,
|
||||
getFailure,
|
||||
fetchUserList,
|
||||
} from "../userList"
|
||||
|
||||
jest.mock("axios")
|
||||
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "PeL",
|
||||
phone: "+886 0970...",
|
||||
email: "forceoranj@gmail.com",
|
||||
website: "https://www.parisestludique.fr",
|
||||
},
|
||||
]
|
||||
const mockError = "Oops! Something went wrong."
|
||||
|
||||
describe("userList reducer", () => {
|
||||
it("should handle initial state", () => {
|
||||
// @ts-expect-error
|
||||
expect(userList(undefined, {})).toEqual(initialState)
|
||||
})
|
||||
|
||||
it("should handle requesting correctly", () => {
|
||||
expect(userList(undefined, { type: getRequesting.type })).toEqual({
|
||||
readyStatus: "request",
|
||||
items: [],
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle success correctly", () => {
|
||||
expect(userList(undefined, { type: getSuccess.type, payload: mockData })).toEqual({
|
||||
...initialState,
|
||||
readyStatus: "success",
|
||||
items: mockData,
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle failure correctly", () => {
|
||||
expect(userList(undefined, { type: getFailure.type, payload: mockError })).toEqual({
|
||||
...initialState,
|
||||
readyStatus: "failure",
|
||||
error: mockError,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("userList action", () => {
|
||||
it("fetches user list successful", async () => {
|
||||
const { dispatch, getActions } = mockStore()
|
||||
const expectedActions = [
|
||||
{ type: getRequesting.type },
|
||||
{ type: getSuccess.type, payload: mockData },
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
axios.get.mockResolvedValue({ data: mockData })
|
||||
|
||||
await dispatch(fetchUserList())
|
||||
expect(getActions()).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
it("fetches user list failed", async () => {
|
||||
const { dispatch, getActions } = mockStore()
|
||||
const expectedActions = [
|
||||
{ type: getRequesting.type },
|
||||
{ type: getFailure.type, payload: mockError },
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
axios.get.mockRejectedValue({ message: mockError })
|
||||
|
||||
await dispatch(fetchUserList())
|
||||
expect(getActions()).toEqual(expectedActions)
|
||||
})
|
||||
})
|
41
src/store/index.ts
Normal file
41
src/store/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createMemoryHistory, createBrowserHistory } from "history"
|
||||
import { Action, configureStore } from "@reduxjs/toolkit"
|
||||
import { ThunkAction } from "redux-thunk"
|
||||
import { routerMiddleware } from "connected-react-router"
|
||||
|
||||
import createRootReducer from "./rootReducer"
|
||||
|
||||
interface Arg {
|
||||
initialState?: typeof window.__INITIAL_STATE__
|
||||
url?: string
|
||||
}
|
||||
|
||||
// Use inferred return type for making correctly Redux types
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
const createStore = ({ initialState, url }: Arg = {}) => {
|
||||
const history = __SERVER__
|
||||
? createMemoryHistory({ initialEntries: [url || "/"] })
|
||||
: createBrowserHistory()
|
||||
const store = configureStore({
|
||||
preloadedState: initialState,
|
||||
reducer: createRootReducer(history),
|
||||
middleware: (getDefaultMiddleware) => [
|
||||
// Included default middlewares: https://redux-toolkit.js.org/api/getDefaultMiddleware#included-default-middleware
|
||||
...getDefaultMiddleware(),
|
||||
routerMiddleware(history),
|
||||
],
|
||||
devTools: __DEV__,
|
||||
})
|
||||
|
||||
return { store, history }
|
||||
}
|
||||
|
||||
const { store } = createStore()
|
||||
|
||||
export type AppState = ReturnType<typeof store.getState>
|
||||
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
export type AppThunk = ThunkAction<void, AppState, unknown, Action<string>>
|
||||
|
||||
export default createStore
|
57
src/store/javGameList.ts
Normal file
57
src/store/javGameList.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
import { JavGame, getJavGameList } from "../services/javGames"
|
||||
import { AppThunk, AppState } from "."
|
||||
|
||||
interface JavGameList {
|
||||
readyStatus: string
|
||||
items: JavGame[]
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const initialState: JavGameList = {
|
||||
readyStatus: "invalid",
|
||||
items: [],
|
||||
error: null,
|
||||
}
|
||||
|
||||
const javGameList = createSlice({
|
||||
name: "javGameList",
|
||||
initialState,
|
||||
reducers: {
|
||||
getRequesting: (state: JavGameList) => {
|
||||
state.readyStatus = "request"
|
||||
},
|
||||
getSuccess: (state, { payload }: PayloadAction<JavGame[]>) => {
|
||||
state.readyStatus = "success"
|
||||
state.items = payload
|
||||
},
|
||||
getFailure: (state, { payload }: PayloadAction<string>) => {
|
||||
state.readyStatus = "failure"
|
||||
state.error = payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default javGameList.reducer
|
||||
export const { getRequesting, getSuccess, getFailure } = javGameList.actions
|
||||
|
||||
export const fetchJavGameList = (): AppThunk => async (dispatch) => {
|
||||
dispatch(getRequesting())
|
||||
|
||||
const { error, data } = await getJavGameList()
|
||||
|
||||
if (error) {
|
||||
dispatch(getFailure(error.message))
|
||||
} else {
|
||||
dispatch(getSuccess(data as JavGame[]))
|
||||
}
|
||||
}
|
||||
|
||||
const shouldFetchJavGameList = (state: AppState) => state.javGameList.readyStatus !== "success"
|
||||
|
||||
export const fetchJavGameListIfNeed = (): AppThunk => (dispatch, getState) => {
|
||||
if (shouldFetchJavGameList(getState())) return dispatch(fetchJavGameList())
|
||||
|
||||
return null
|
||||
}
|
16
src/store/rootReducer.ts
Normal file
16
src/store/rootReducer.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { History } from "history"
|
||||
import { connectRouter } from "connected-react-router"
|
||||
|
||||
import userList from "./userList"
|
||||
import userData from "./userData"
|
||||
import javGameList from "./javGameList"
|
||||
|
||||
// Use inferred return type for making correctly Redux types
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export default (history: History) => ({
|
||||
userList,
|
||||
userData,
|
||||
javGameList,
|
||||
router: connectRouter(history) as any,
|
||||
// Register more reducers...
|
||||
})
|
66
src/store/userData.ts
Normal file
66
src/store/userData.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
import { User, getUserData } from "../services/jsonPlaceholder"
|
||||
import { AppThunk, AppState } from "."
|
||||
|
||||
interface UserDate {
|
||||
[id: string]: {
|
||||
readyStatus: string
|
||||
item?: User
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface Success {
|
||||
id: string
|
||||
item: User
|
||||
}
|
||||
|
||||
interface Failure {
|
||||
id: string
|
||||
error: string
|
||||
}
|
||||
|
||||
const userData = createSlice({
|
||||
name: "userData",
|
||||
initialState: {} as UserDate,
|
||||
reducers: {
|
||||
getRequesting: (state, { payload }: PayloadAction<string>) => {
|
||||
state[payload] = { readyStatus: "request" }
|
||||
},
|
||||
getSuccess: (state, { payload }: PayloadAction<Success>) => {
|
||||
state[payload.id] = { readyStatus: "success", item: payload.item }
|
||||
},
|
||||
getFailure: (state, { payload }: PayloadAction<Failure>) => {
|
||||
state[payload.id] = { readyStatus: "failure", error: payload.error }
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default userData.reducer
|
||||
export const { getRequesting, getSuccess, getFailure } = userData.actions
|
||||
|
||||
export const fetchUserData =
|
||||
(id: string): AppThunk =>
|
||||
async (dispatch) => {
|
||||
dispatch(getRequesting(id))
|
||||
|
||||
const { error, data } = await getUserData(id)
|
||||
|
||||
if (error) {
|
||||
dispatch(getFailure({ id, error: error.message }))
|
||||
} else {
|
||||
dispatch(getSuccess({ id, item: data as User }))
|
||||
}
|
||||
}
|
||||
|
||||
const shouldFetchUserData = (state: AppState, id: string) =>
|
||||
state.userData[id]?.readyStatus !== "success"
|
||||
|
||||
export const fetchUserDataIfNeed =
|
||||
(id: string): AppThunk =>
|
||||
(dispatch, getState) => {
|
||||
if (shouldFetchUserData(getState(), id)) return dispatch(fetchUserData(id))
|
||||
|
||||
return null
|
||||
}
|
57
src/store/userList.ts
Normal file
57
src/store/userList.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
import { User, getUserList } from "../services/jsonPlaceholder"
|
||||
import { AppThunk, AppState } from "."
|
||||
|
||||
interface UserList {
|
||||
readyStatus: string
|
||||
items: User[]
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const initialState: UserList = {
|
||||
readyStatus: "invalid",
|
||||
items: [],
|
||||
error: null,
|
||||
}
|
||||
|
||||
const userList = createSlice({
|
||||
name: "userList",
|
||||
initialState,
|
||||
reducers: {
|
||||
getRequesting: (state: UserList) => {
|
||||
state.readyStatus = "request"
|
||||
},
|
||||
getSuccess: (state, { payload }: PayloadAction<User[]>) => {
|
||||
state.readyStatus = "success"
|
||||
state.items = payload
|
||||
},
|
||||
getFailure: (state, { payload }: PayloadAction<string>) => {
|
||||
state.readyStatus = "failure"
|
||||
state.error = payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default userList.reducer
|
||||
export const { getRequesting, getSuccess, getFailure } = userList.actions
|
||||
|
||||
export const fetchUserList = (): AppThunk => async (dispatch) => {
|
||||
dispatch(getRequesting())
|
||||
|
||||
const { error, data } = await getUserList()
|
||||
|
||||
if (error) {
|
||||
dispatch(getFailure(error.message))
|
||||
} else {
|
||||
dispatch(getSuccess(data as User[]))
|
||||
}
|
||||
}
|
||||
|
||||
const shouldFetchUserList = (state: AppState) => state.userList.readyStatus !== "success"
|
||||
|
||||
export const fetchUserListIfNeed = (): AppThunk => (dispatch, getState) => {
|
||||
if (shouldFetchUserList(getState())) return dispatch(fetchUserList())
|
||||
|
||||
return null
|
||||
}
|
2
src/theme/variables.scss
Executable file
2
src/theme/variables.scss
Executable file
@@ -0,0 +1,2 @@
|
||||
$color-dark-gray: #333;
|
||||
$color-white: #eee;
|
26
src/types/index.d.ts
vendored
Normal file
26
src/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
declare const __CLIENT__: boolean
|
||||
declare const __SERVER__: boolean
|
||||
declare const __DEV__: boolean
|
||||
|
||||
declare module "*.svg"
|
||||
declare module "*.gif"
|
||||
declare module "*.png"
|
||||
declare module "*.jpg"
|
||||
declare module "*.jpeg"
|
||||
declare module "*.webp"
|
||||
declare module "*.css"
|
||||
declare module "*.scss"
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface Global {
|
||||
__CLIENT__: boolean
|
||||
__SERVER__: boolean
|
||||
__DEV__: boolean
|
||||
$RefreshReg$: () => void
|
||||
$RefreshSig$$: () => void
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
__INITIAL_STATE__: Record<string, unknown>
|
||||
}
|
16
src/utils/mockStore.tsx
Normal file
16
src/utils/mockStore.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ReactNode } from "react"
|
||||
import { Provider } from "react-redux"
|
||||
import thunk from "redux-thunk"
|
||||
import configurecreateMockStore from "redux-mock-store"
|
||||
|
||||
export default (obj = {}): Record<string, any> => {
|
||||
const store = configurecreateMockStore([thunk])(obj)
|
||||
const originalDispatch = store.dispatch
|
||||
store.dispatch = jest.fn(originalDispatch)
|
||||
|
||||
const ProviderWithStore = ({ children }: { children: ReactNode }) => (
|
||||
<Provider store={store}>{children}</Provider>
|
||||
)
|
||||
|
||||
return { ...store, ProviderWithStore }
|
||||
}
|
Reference in New Issue
Block a user