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

26
.all-contributorsrc Normal file
View File

@ -0,0 +1,26 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "pikiou",
"name": "Pierre",
"avatar_url": "https://avatars1.githubusercontent.com/u/16210249?v=4",
"profile": "https://coplay.org",
"contributions": [
"code",
"doc",
"maintenance"
]
},
],
"contributorsPerLine": 7,
"projectName": "intranet",
"projectOwner": "forceoranj",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
}

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
public

58
.eslintrc.js Normal file
View File

@ -0,0 +1,58 @@
module.exports = {
parser: "@typescript-eslint/parser",
extends: [
"airbnb",
"airbnb/hooks",
"plugin:@typescript-eslint/recommended",
"plugin:jest/recommended",
"plugin:jest/style",
"plugin:jest-dom/recommended",
"plugin:testing-library/react",
"prettier",
],
plugins: ["@typescript-eslint", "jest", "jest-dom", "testing-library"],
settings: {
"import/resolver": {
typescript: {},
},
},
env: {
browser: true,
node: true,
es6: true,
jest: true,
},
rules: {
"global-require": "off",
"no-use-before-define": "off",
"no-console": "off",
"no-underscore-dangle": "off",
"no-param-reassign": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-props-no-spreading": "off",
"react/jsx-filename-extension": [
"error",
{
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
],
"import/extensions": "off",
"import/no-extraneous-dependencies": [
"error",
{
devDependencies: true,
},
],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"testing-library/no-node-access": "off",
"testing-library/render-result-naming-convention": "off",
},
globals: {
__CLIENT__: true,
__SERVER__: true,
__DEV__: true,
},
}

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: forceoranj
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

41
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,41 @@
---
name: "\U0001F41B Bug Report"
about: Create a report to help us improve
title: ""
labels: ""
assignees: ""
---
# Bug Report
## Describe the Bug
A clear and concise description of what the bug is.
## How to Reproduce
Steps to reproduce the behavior, please provide code snippets or a repository:
1. Go to '....'
2. Click on '....'
3. See error
## Expected Behavior
Tell me what should happen.
## Screenshots
Add screenshots to help explain your problem.
## Your Environment
- Device: [e.g. MacBook Pro, iPhone12]
- OS: [e.g. macOS, iOS, Windows]
- Browser: [e.g. Chrome, Safari]
- Node version: [e.g. v16.0.0]
- App version: [e.g. v1.0.0]
## Additional Information
Any other information about the problem here.

View File

@ -0,0 +1,25 @@
---
name: "\U0001F4A1 Feature Request"
about: Suggest an idea for this project
title: ""
labels: ""
assignees: ""
---
# Feature Request
## Describe the Feature
A clear and concise description of what you want and what your use case is.
## Describe the Solution You'd Like
A clear and concise description of what you want to happen.
## Describe Alternatives You've Considered
A clear and concise description of any alternative solutions or features you've considered.
## Additional Information
Any other information about the feature here.

21
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,21 @@
---
name: "\U0001F914 Questions and Help"
about: This issue tracker is not for questions. Please ask questions at https://stackoverflow.com/questions/tagged/react.
title: ""
labels: ""
assignees: ""
---
GitHub Issues are reserved for Bug reports and Feature requests. Support requests that are created as issues are likely to be closed. We want to make sure you are able to find the help you seek. Please take a look at the following resources.
## Coding Questions
If you have a coding question related to React, it might be better suited for Stack Overflow. It's a great place to browse through frequent questions about using React, as well as ask for help with specific questions.
https://stackoverflow.com/questions/tagged/react
## Support Forums
There are many online forums which are a great place for discussion about best practices and application architecture.
https://reactjs.org/community/support.html#popular-discussion-forums

35
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,35 @@
<!--
Thanks for your interest in the project. Bugs filed and PRs submitted are appreciated!
Before submitting a pull request, please make sure you're familiar with and follow the instructions in the contributing guidelines (found in the CONTRIBUTING.md file).
Also, please make sure you're familiar with and follow the instructions in the contributing guidelines (found in the CONTRIBUTING.md file).
If you're new to contributing to open source projects, you might find this free video course helpful: https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github
Please fill out the information below to expedite the review and (hopefully) merge of your pull request!
-->
## What
What changes are being made? (e.g. feature, bug, docs, etc.)
## Why
Why are these changes necessary?
## How
How were these changes implemented?
## Checklist
Have you done all of these things?
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
- [ ] Documentation added
- [ ] Tests
- [ ] TypeScript definitions updated
- [ ] Ready to be merged

71
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,71 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
time: "21:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: stylelint-config-standard
versions:
- 22.0.0
- dependency-name: core-js
versions:
- 3.10.2
- dependency-name: "@types/helmet"
versions:
- 4.0.0
- dependency-name: "@types/react-dom"
versions:
- 17.0.0
- 17.0.1
- 17.0.2
- 17.0.3
- dependency-name: eslint-config-prettier
versions:
- 7.2.0
- 8.0.0
- 8.1.0
- 8.2.0
- dependency-name: "@types/react"
versions:
- 17.0.0
- 17.0.1
- 17.0.2
- 17.0.3
- dependency-name: "@types/react-test-renderer"
versions:
- 17.0.0
- 17.0.1
- dependency-name: "@types/serialize-javascript"
versions:
- 5.0.0
- dependency-name: webpack-bundle-analyzer
versions:
- 4.4.0
- 4.4.1
- dependency-name: fork-ts-checker-webpack-plugin
versions:
- 6.1.0
- 6.1.1
- 6.2.0
- 6.2.1
- dependency-name: mini-css-extract-plugin
versions:
- 1.3.5
- 1.3.6
- 1.3.7
- 1.3.8
- 1.3.9
- 1.4.0
- 1.4.1
- dependency-name: webpack-dev-middleware
versions:
- 4.1.0
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
time: "21:00"
open-pull-requests-limit: 10

53
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: CI
on:
push:
branches:
- master
- docker
tags-ignore:
- "**"
pull_request:
branches:
- master
- docker
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x, 15.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Get cached node modules
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run lint
run: yarn lint
- name: Run test
run: yarn test:cov
- name: Run build
run: yarn build
- name: Coveralls GitHub Action
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: "27 0 * * 0"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2.3.4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
# Dependencies
node_modules
# Testing
coverage
# Production
public/*
!public/favicon.ico
!public/logo192.png
!public/logo512.png
!public/manifest.json
# Access
access/gsheets.json
access/token.json
# Misc
.DS_Store
*.log

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged

4
.husky/pre-push Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn test

10
.prettierignore Normal file
View File

@ -0,0 +1,10 @@
# Testing
coverage
# Production
public/*
!public/manifest.json
# Misc
*.log

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": false,
"arrowParens": "always",
"printWidth": 100,
"useTabs": false
}

9
.stylelintrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
plugins: ["stylelint-order"],
extends: [
"stylelint-config-standard",
"stylelint-config-sass-guidelines",
"stylelint-config-prettier",
],
ignoreFiles: ["public/assets/**/*.css", "coverage/**/*.css"],
}

4
CHANGELOG.md Executable file
View File

@ -0,0 +1,4 @@
# Changelog
This project adheres to [Semantic Versioning](http://semver.org).
Every release, along with the migration instructions, is documented on the Github [Releases](https://github.com/forceoranj/intranet/releases) page.

13
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,13 @@
# Contributor Code of Conduct
## Version 0.1-forceoranj/intranet
As contributors and maintainers of the intranet project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
Communication through any of intranet's channels (GitHub, Discord, mailing lists, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the intranet project to do the same.
If any member of the community violates this code of conduct, the maintainers of the intranet project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [forceoranj@gmail.com](mailto:forceoranj@gmail.com).

30
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,30 @@
# Contributing to FORCE ORANGE
When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change.
Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
> Working on your first Pull Request? You can learn how from [this free video series](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github).
## Pull Request Process
1. Fork the repository and create your branch from `master`.
2. Run `yarn` to install dependencies.
3. If youve fixed a bug or added code that should be tested.
4. Ensure the test suite passes by running `yarn test`.
5. Update the [README.md](README.md) with details of changes.
6. Make sure your code lints by running `yarn lint`.
## Development Workflow
After cloning REACT COOL STARTER, run `yarn` to fetch its dependencies. Then, you can run [several commands](https://github.com/forceoranj/intranet#script-commands).
## Style Guide
We use [ESLint](https://eslint.org), [StyleLint](https://stylelint.io) and [Prettier](https://prettier.io) for code style and formatting. Run `yarn lint` after making any changes to the code. Then, our linter will catch most issues that may exist in your code.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at [Airbnbs Style Guide](https://github.com/airbnb/javascript) will guide you in the right direction.
## License
By contributing to REACT COOL STARTER, you agree that your contributions will be licensed under its MIT license.

21
LICENSE Executable file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021 Paris est Ludique
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

123
README.md Normal file
View File

@ -0,0 +1,123 @@
# <b>Intranet of the <i>Paris est Ludique</i> board game festival.</b>
<i>This is the intranet used by the volunteers of the assosication Paris est Ludique.</i><br>
It is built from the boilerplate [React Cool Starter](https://github.com/forceoranj/intranet), on the top of [React](https://facebook.github.io/react), [Redux](https://github.com/reactjs/redux), [React Router](https://reacttraining.com/react-router) and [Express](https://expressjs.com).
## Requirements
- [node](https://nodejs.org/en) >= 12.0
- [npm](https://www.npmjs.com) >= 6.0
## Getting Started
**1. You can start by cloning the repository on your local machine by running:**
```sh
git clone https://github.com/forceoranj/intranet.git
cd intranet
```
**2. Install all of the dependencies:**
```sh
yarn
```
**3. Start to run it:**
```sh
yarn dev # Build, hosts, and hot reload saved modifications
```
Now the app should be running at [http://localhost:3000](http://localhost:3000)
## File editors
Edit files with one of these HTML/CSS/TypeScript editors:
- [Atom](https://atom.io/) with [TypeScript plugin](https://atom.io/packages/ide-typescript)
- [Visual Studio Code](https://code.visualstudio.com/)
- [Webstorm 2018.1](https://www.jetbrains.com/webstorm/download/)
- [Sublime Text](http://www.sublimetext.com/3) with [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation)
## Script Commands
I use [cross-env](https://github.com/kentcdodds/cross-env) to set and use environment variables across platforms. All of the scripts are listed as following:
| `yarn <script>` | Description |
| ---------------- | ---------------------------------------------------------------------------------- |
| `dev` | Runs your app on the development server at `localhost:3000`. HMR will be enabled. |
| `dev:build` | Bundles server-side files in development mode and put it to the `./public/server`. |
| `start` | Runs your app on the production server only at `localhost:8080`. |
| `build` | Bundles both server-side and client-side files. |
| `build:server` | Bundles server-side files in production mode and put it to the `./public/server`. |
| `build:client` | Bundles client-side files in production mode and put it to the `./public/assets`. |
| `analyze:server` | Visualizes the bundle content of server-side. |
| `analyze:client` | Visualizes the bundle content of client-side. |
| `lint` | Lints all `.tsx?`, `.jsx?` and `.scss` files. |
| `lint:code` | Lints all `.tsx?` and `.jsx?` files (With `--fix` to auto fix eslint errors). |
| `lint:type` | Runs type checking for `.tsx?` files. |
| `lint:style` | Lints all `.scss` files (With `--fix` to auto fix stylelint errors). |
| `lint:format` | Formats all files except the file list of `.prettierignore`. |
| `test` | Runs testing. |
| `test:watch` | Runs an interactive test watcher. |
| `test:cov` | Runs testing with code coverage reports. |
| `test:update` | Updates jest snapshot. |
## App Structure
Here is the structure of the app, which serves as generally accepted guidelines and patterns for building scalable apps.
```
.
├── public # Express server static path and Webpack bundles output
│ ├── favicon.ico # App favicon
│ ├── logo192.png # App logo small
│ ├── logo512.png # App logo large
│ └── manifest.json # App favicon and logo manifest
├── src # App source code
│ ├── config # App configuration by environments
│ │ ├── default.ts # Default settings
│ │ ├── index.ts # Configuration entry point
│ │ └── prod.ts # Production settings (overrides the default)
│ ├── components # Reusable components
│ ├── pages # Page components
│ ├── app # App root component
│ ├── store # Redux store creator, actions + reducers (a.k.a slice)
│ ├── services # API calls
│ ├── utils # App-wide utils (e.g. mock store creator for testing etc.)
│ ├── static # Static assets (e.g. images, fonts etc.)
│ ├── theme # App-wide style and vendor CSS framework
│ ├── types # App-wide type definitions
│ ├── client # App bootstrap and rendering (Webpack entry)
│ ├── routes # Routes configuration for both client-side and server-side
│ └── server # Express server (with Webpack dev and hot middlewares)
├── webpack # Webpack configurations
├── jest # Jest configurations
├── babel.config.js # Babel configuration
├── tsconfig.json # TypeScript configuration
├── postcss.config.js # PostCSS configuration
├── .eslintrc.js # ESLint configuration
├── .stylelintrc.js # stylelint configuration
└── nodemon.json # nodemon configuration
```
## Contributors ✨
Thanks goes to these people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://www.parisestludique.fr"><img src="https://avatars1.githubusercontent.com/u/79382808?v=4" width="100px;" alt=""/><br /><sub><b>pikiou</b></sub></a><br /><a href="https://github.com/forceoranj/intranet/commits?author=pikiou" title="Code">💻</a> <a href="https://github.com/forceoranj/intranet/commits?author=pikiou" title="Documentation">📖</a> <a href="#maintenance-forceoranj" title="Maintenance">🚧</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

21
SECURITY.md Normal file
View File

@ -0,0 +1,21 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.

28
babel.config.js Normal file
View File

@ -0,0 +1,28 @@
module.exports = (api) => {
const isWeb = api.caller((caller) => caller && caller.target === "isWeb")
return {
presets: [
[
"@babel/env",
{
useBuiltIns: isWeb ? "usage" : undefined,
corejs: isWeb ? 3 : false,
},
],
"@babel/typescript",
[
"@babel/react",
{
runtime: "automatic",
},
],
],
plugins: ["@loadable/babel-plugin", "@babel/plugin-transform-runtime"],
env: {
development: {
plugins: isWeb ? ["react-refresh/babel"] : undefined,
},
},
}
}

1
jest/assetMock.ts Executable file
View File

@ -0,0 +1 @@
module.exports = "IMAGE_MOCK"

25
jest/config.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
preset: "ts-jest",
rootDir: "../",
testEnvironment: "node",
setupFilesAfterEnv: ["<rootDir>/jest/setup.ts"],
collectCoverageFrom: [
"src/app/**/*.tsx",
"src/pages/**/*.tsx",
"!src/pages/**/index.tsx",
"src/components/**/*.tsx",
"src/store/**/*.ts",
"!src/store/index.ts",
"!src/store/rootReducer.ts",
],
moduleNameMapper: {
".*\\.(css|scss|sass)$": "<rootDir>/jest/styleMock.ts",
".*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/jest/assetMock.ts",
},
globals: {
__DEV__: true,
},
maxConcurrency: 50,
maxWorkers: 1,
}

1
jest/setup.ts Executable file
View File

@ -0,0 +1 @@
import "@testing-library/jest-dom/extend-expect"

4
jest/styleMock.ts Executable file
View File

@ -0,0 +1,4 @@
// @ts-ignore
import idObj from "identity-obj-proxy"
export default idObj

3
nodemon.json Normal file
View File

@ -0,0 +1,3 @@
{
"watch": ["src/server", "webpack", "babel.config.js"]
}

BIN
out.ogv Normal file

Binary file not shown.

15441
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

198
package.json Normal file
View File

@ -0,0 +1,198 @@
{
"name": "intranet",
"version": "2.0.0",
"private": true,
"description": "A starter boilerplate for a universal web application with the best development experience and best practices.",
"license": "MIT",
"homepage": "https://github.com/forceoranj/intranet",
"repository": "https://github.com/forceoranj/intranet",
"bugs": "https://github.com/forceoranj/intranet/issues",
"keywords": [
"starter",
"boilerplate",
"universal",
"react",
"react-hooks",
"redux",
"redux-toolkit",
"react-router",
"express",
"webpack",
"es6+",
"typescript",
"code-splitting",
"react-refresh",
"babel",
"postcss",
"jest",
"unit-testing",
"react-testing-library",
"performance-optimization",
"best-practices",
"eslint",
"stylelint",
"prettier"
],
"author": "Paris est Ludique <forceoranj@gmail.com> (https://github.com/forceoranj)",
"engines": {
"node": ">=12",
"npm": ">=6"
},
"scripts": {
"dev": "yarn dev:build && nodemon ./public/server",
"dev:build": "cross-env NODE_ENV=development webpack --config ./webpack/server.config.ts",
"start": "node ./public/server",
"build": "run-s build:*",
"build:server": "cross-env NODE_ENV=production webpack --config ./webpack/server.config.ts",
"build:client": "cross-env NODE_ENV=production webpack --config ./webpack/client.config.ts",
"analyze:server": "cross-env NODE_ENV=analyze webpack --config ./webpack/server.config.ts",
"analyze:client": "cross-env NODE_ENV=analyze webpack --config ./webpack/client.config.ts",
"lint": "run-s lint:*",
"lint:code": "eslint --fix . --ext .js,.jsx,.ts,.tsx",
"lint:type": "tsc",
"lint:style": "stylelint --fix \"**/*.{css,ts,tsx}\"",
"lint:format": "prettier -w . -u --loglevel silent",
"test": "cross-env NODE_ENV=test jest --config ./jest/config.js",
"test:watch": "yarn test --watch",
"test:cov": "yarn test --coverage",
"test:update": "yarn test -u",
"prepare": "husky install"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --fix",
"*.{css,sass,ts,tsx}": "stylelint --fix",
"**/*": "prettier -w -u"
},
"browserslist": [
"> 1%",
"last 2 versions"
],
"dependencies": {
"@babel/runtime": "^7.14.6",
"@loadable/component": "^5.15.0",
"@loadable/server": "^5.15.0",
"@reduxjs/toolkit": "^1.6.0",
"autoprefixer": "^10.2.6",
"axios": "^0.21.1",
"chalk": "^4.1.1",
"compression": "^1.7.4",
"connected-react-router": "^6.9.1",
"core-js": "^3.15.2",
"cross-env": "^7.0.3",
"express": "^4.17.1",
"fs": "^0.0.1-security",
"googleapis": "^88.2.0",
"helmet": "^4.6.0",
"history": "^4.10.1",
"hpp": "^0.2.3",
"html-minifier": "^4.0.0",
"https": "^1.0.0",
"lodash": "^4.17.21",
"morgan": "^1.10.0",
"normalize.css": "^8.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-redux": "^7.2.4",
"react-router": "^5.2.0",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.2.0",
"readline": "^1.3.0",
"redux-thunk": "^2.3.0",
"serialize-javascript": "^6.0.0",
"serve-favicon": "^2.5.0"
},
"devDependencies": {
"@babel/core": "^7.14.6",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.7",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.14.5",
"@loadable/babel-plugin": "^5.13.2",
"@loadable/webpack-plugin": "^5.15.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/compression": "^1.7.1",
"@types/compression-webpack-plugin": "^6.0.6",
"@types/css-minimizer-webpack-plugin": "^3.0.2",
"@types/express": "^4.17.13",
"@types/hpp": "^0.2.1",
"@types/html-minifier": "^4.0.1",
"@types/jest": "^26.0.24",
"@types/loadable__component": "^5.13.4",
"@types/loadable__server": "^5.12.6",
"@types/loadable__webpack-plugin": "^5.7.3",
"@types/lodash": "^4.14.175",
"@types/mini-css-extract-plugin": "^2.0.1",
"@types/morgan": "^1.9.3",
"@types/react-dom": "^17.0.8",
"@types/react-helmet": "^6.1.1",
"@types/react-router-config": "^5.0.2",
"@types/react-router-dom": "^5.1.7",
"@types/react-test-renderer": "^17.0.1",
"@types/redux-mock-store": "^1.0.2",
"@types/serialize-javascript": "^5.0.1",
"@types/serve-favicon": "^2.5.3",
"@types/terser-webpack-plugin": "^5.0.4",
"@types/webpack-bundle-analyzer": "^4.4.1",
"@types/webpack-manifest-plugin": "^3.0.5",
"@types/webpack-node-externals": "^2.5.2",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"babel-loader": "^8.2.2",
"compression-webpack-plugin": "^8.0.1",
"css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^3.0.2",
"eslint": "^7.14.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-jest-dom": "^3.9.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4",
"eslint-plugin-testing-library": "^4.9.0",
"fork-ts-checker-webpack-plugin": "^6.2.12",
"husky": "^7.0.1",
"identity-obj-proxy": "^3.0.0",
"image-minimizer-webpack-plugin": "^2.2.0",
"imagemin-gifsicle": "^7.0.0",
"imagemin-jpegtran": "^7.0.0",
"imagemin-optipng": "^8.0.0",
"imagemin-svgo": "^9.0.0",
"jest": "^27.0.6",
"lint-staged": "^11.0.0",
"mini-css-extract-plugin": "^2.1.0",
"node-sass": "^6.0.1",
"nodemon": "^2.0.9",
"npm-run-all": "^4.1.5",
"postcss": "^8.3.5",
"postcss-loader": "^6.1.1",
"prettier": "^2.3.2",
"react-refresh": "^0.10.0",
"react-test-renderer": "^17.0.2",
"redux-mock-store": "^1.5.4",
"sass-loader": "^12.1.0",
"source-map-support": "^0.5.19",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-sass-guidelines": "^8.0.0",
"stylelint-config-standard": "^22.0.0",
"stylelint-order": "^4.1.0",
"terser-webpack-plugin": "^5.1.4",
"ts-jest": "^27.0.3",
"ts-node": "^10.0.0",
"typescript": "^4.3.5",
"webpack": "^5.43.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.2",
"webpack-dev-middleware": "^5.0.0",
"webpack-hot-middleware": "^2.25.0",
"webpack-manifest-plugin": "^3.1.1",
"webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0"
}
}

3
postcss.config.js Executable file
View File

@ -0,0 +1,3 @@
module.exports = {
plugins: [require("autoprefixer")],
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "REACT COOL STARTER",
"name": "REACT COOL STARTER",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

42
src/app/__tests__/App.tsx Executable file
View 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()
})
})

View 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
View 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
View 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
View 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()

View 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()
})
})

View File

@ -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`;

View 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

View 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()
})
})

View 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
View 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)

View File

@ -0,0 +1,9 @@
.user-card {
ul {
padding-left: 17px;
li {
margin-bottom: 0.5em;
}
}
}

View 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()
})
})

View File

@ -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>
`;

View 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)

View 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;
}
}

View 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()
})
})

View 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
View 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)

View 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;
}
}

View 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()
})
})

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Loading /> renders 1`] = `
<div
class="Loading"
>
<p>
Loading...
</p>
</div>
`;

View 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

View File

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

7
src/components/index.ts Executable file
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
export default {
PORT: 8080,
}

133
src/gsheets/jav.ts Normal file
View 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
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;
}

28
src/routes/index.ts Executable file
View 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
View 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
View 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
View 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
View 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
View 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 }
}
}

View 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
View 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

View 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)
})
})

View 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)
})
})

View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
$color-dark-gray: #333;
$color-white: #eee;

26
src/types/index.d.ts vendored Normal file
View 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
View 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 }
}

32
tsconfig.json Normal file
View File

@ -0,0 +1,32 @@
{
"compilerOptions": {
"sourceMap": true,
"target": "es5",
"module": "commonjs",
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "react-jsx",
// Specify module resolution strategy: "node" (Node.js) or "classic" (TypeScript pre-1.6)
"moduleResolution": "node",
// Enable all strict type-checking options. Recommended by TS
"strict": true,
// Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports.
// Implies "allowSyntheticDefaultImports". Recommended by TS
"esModuleInterop": true,
// Skip type checking of declaration files. Recommended by TS
"skipLibCheck": true,
// Disallow inconsistently-cased references to the same file. Recommended by TS
"forceConsistentCasingInFileNames": true,
// Do not emit outputs
"noEmit": true,
// Raise error on expressions and declarations with an implied "any" type
"noImplicitAny": true,
// Report errors on unused locals
"noUnusedLocals": true,
// Report errors on unused parameters
"noUnusedParameters": true,
// Report error when not all code paths in function return a value
"noImplicitReturns": true,
// Report errors for fallthrough cases in switch statement
"noFallthroughCasesInSwitch": true
}
}

Some files were not shown because too many files have changed in this diff Show More