mirror of
https://github.com/Paris-est-Ludique/intranet.git
synced 2025-06-08 00:24:21 +02:00
Initial commit
This commit is contained in:
commit
ebdd8dccdd
26
.all-contributorsrc
Normal file
26
.all-contributorsrc
Normal 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
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
public
|
58
.eslintrc.js
Normal file
58
.eslintrc.js
Normal 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
12
.github/FUNDING.yml
vendored
Normal 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
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
21
.github/ISSUE_TEMPLATE/question.md
vendored
Normal 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
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
71
.github/dependabot.yml
vendored
Normal 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
53
.github/workflows/ci.yml
vendored
Normal 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
71
.github/workflows/codeql-analysis.yml
vendored
Normal 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
20
.gitignore
vendored
Normal 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
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged
|
4
.husky/pre-push
Executable file
4
.husky/pre-push
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn test
|
10
.prettierignore
Normal file
10
.prettierignore
Normal file
@ -0,0 +1,10 @@
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Production
|
||||
public/*
|
||||
!public/manifest.json
|
||||
|
||||
# Misc
|
||||
*.log
|
||||
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"arrowParens": "always",
|
||||
"printWidth": 100,
|
||||
"useTabs": false
|
||||
}
|
9
.stylelintrc.js
Normal file
9
.stylelintrc.js
Normal 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
4
CHANGELOG.md
Executable 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
13
CODE_OF_CONDUCT.md
Normal 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
30
CONTRIBUTING.md
Normal 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 you’ve 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 [Airbnb’s 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
21
LICENSE
Executable 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
123
README.md
Normal 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
21
SECURITY.md
Normal 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
28
babel.config.js
Normal 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
1
jest/assetMock.ts
Executable file
@ -0,0 +1 @@
|
||||
module.exports = "IMAGE_MOCK"
|
25
jest/config.js
Normal file
25
jest/config.js
Normal 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
1
jest/setup.ts
Executable file
@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom/extend-expect"
|
4
jest/styleMock.ts
Executable file
4
jest/styleMock.ts
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-ignore
|
||||
import idObj from "identity-obj-proxy"
|
||||
|
||||
export default idObj
|
3
nodemon.json
Normal file
3
nodemon.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"watch": ["src/server", "webpack", "babel.config.js"]
|
||||
}
|
15441
package-lock.json
generated
Normal file
15441
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
198
package.json
Normal file
198
package.json
Normal 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
3
postcss.config.js
Executable file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
plugins: [require("autoprefixer")],
|
||||
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal 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
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 }
|
||||
}
|
32
tsconfig.json
Normal file
32
tsconfig.json
Normal 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
Loading…
x
Reference in New Issue
Block a user