├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .huskyrc ├── .lintstagedrc.js ├── .npmignore ├── .nvmrc ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .vscodeignore ├── README.md ├── commitlint.config.js ├── config ├── aliases.js ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── modules.js ├── paths.js ├── pnpTs.js ├── webpack.config.js └── webpackDevServer.config.js ├── jest.config.js ├── jsconfig.path.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt ├── signin-callback.html └── silent-renew.html ├── scripts ├── build.js ├── service-worker │ ├── builder.js │ └── sw-template.js ├── start.js └── test.js ├── src ├── App.test.tsx ├── App.tsx ├── components │ ├── Maybe │ │ └── index.tsx │ ├── PageTitle │ │ └── index.tsx │ ├── PrivateRoute │ │ └── index.tsx │ └── PublicRoute │ │ └── index.tsx ├── helpers │ ├── api.ts │ ├── endpoints.ts │ ├── history.ts │ ├── localStorage.ts │ └── notify.ts ├── hooks │ ├── useMedia.ts │ ├── useOffline.ts │ └── useUser.ts ├── index.tsx ├── react-app-env.d.ts ├── screens │ ├── Auth │ │ ├── components │ │ │ └── Main │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── screens │ │ │ └── Login │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ └── Dashboard │ │ ├── components │ │ └── Actor │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ ├── index.tsx │ │ └── screens │ │ └── Fame │ │ ├── index.module.scss │ │ └── index.tsx ├── serviceWorker.ts ├── setupTests.ts ├── state │ ├── index.app.ts │ └── reducer.app.ts └── styles │ ├── _text-colors.scss │ ├── _vars.scss │ ├── app.scss │ └── index.scss ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": [ 4 | [ 5 | "import", 6 | { 7 | "libraryName": "antd", 8 | "libraryDirectory": "es", 9 | "style": "css" 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.json, *.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build 3 | src/registerServiceWorker.js 4 | src/**/__tests__/** 5 | src/typings 6 | scripts/service-worker/ 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | extends: [ 5 | "react-app", 6 | "eslint:recommended", 7 | "plugin:react/recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier/@typescript-eslint", 10 | "plugin:prettier/recommended", 11 | ], 12 | plugins: [ "react", "@typescript-eslint", "prettier" ], 13 | env: { 14 | commonjs: true, 15 | es6: true, 16 | node: true, 17 | jest: true, 18 | }, 19 | parserOptions: { 20 | ecmaVersion: 2018, 21 | sourceType: "module", 22 | ecmaFeatures: { 23 | jsx: true, 24 | }, 25 | }, 26 | rules: { 27 | "prettier/prettier": 0, 28 | curly: "warn", 29 | "no-console": "off", 30 | strict: [ "error", "global" ], 31 | "no-useless-catch": "off", 32 | "@typescript-eslint/no-var-requires": "off", 33 | quotes: [ "error", "double" ], 34 | "no-tabs": 0, 35 | indent: [ 0, "tab" ], 36 | semi: [ 1, "always" ], 37 | "dot-location": [ "warn", "property" ], 38 | eqeqeq: [ "warn", "allow-null" ], 39 | "@typescript-eslint/explicit-member-accessibility": 0, 40 | "@typescript-eslint/explicit-function-return-type": 0, 41 | "@typescript-eslint/interface-name-prefix": 0, 42 | "@typescript-eslint/ban-ts-ignore": 0, 43 | "react/display-name": 0, 44 | "react/prop-types": 0, 45 | "@typescript-eslint/no-namespace": 0, 46 | "@typescript-eslint/no-explicit-any": 0, 47 | "@typescript-eslint/triple-slash-reference": 0, 48 | "@typescript-eslint/no-unused-vars": "error", 49 | }, 50 | settings: { 51 | react: { 52 | pragma: "React", 53 | version: "detect", 54 | }, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "src/**/*.{js,jsx,ts,tsx}": [ "npm run test:ci", "prettier --write", "git add" ], 3 | "src/**/*.{css,scss}": [ "prettier --write", "git add" ], 4 | "*.{json,md}": [ "prettier --write", "git add" ] 5 | }; 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ./ignore 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.15.3 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": true, 5 | "semi": true, 6 | "trailingComma": "all", 7 | "overrides": [ 8 | { 9 | "files": [".prettierrc", ".json"], 10 | "options": { "parser": "json", "useTabs": false, "tabWidth": 2 } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "editorconfig.editorconfig", 6 | "shinnn.stylelint", 7 | "dsznajder.es7-react-js-snippets", 8 | "wix.vscode-import-cost" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug App", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceFolder}/src", 10 | "sourceMapPathOverrides": { 11 | "webpack:///src/*": "${webRoot}/*" 12 | } 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Debug Tests", 18 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 19 | "cwd": "${workspaceRoot}", 20 | "args": ["--i", "--config", "jest.config.js"] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | { "language": "typescript", "autoFix": true }, 7 | { "language": "typescriptreact", "autoFix": true } 8 | ], 9 | "editor.formatOnSave": true, 10 | "jest.debugMode": true, 11 | "jest.pathToJest": "node_modules/.bin/jest" 12 | } 13 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | **/*.map 5 | .gitignore 6 | tsconfig.json 7 | **/__mocks__/** 8 | **/tests/** 9 | **/*.ts 10 | **/tsconfig.json 11 | jsconfig.json 12 | jest.json 13 | tslint.json 14 | .travis.yml 15 | yarn.lock 16 | yarn-error.log 17 | scripts/ 18 | coverage 19 | .github/** 20 | images/** 21 | !images/vscode-jest.png 22 | webpack.config.js 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A bare minimum react-typescript-hooks-sample boilerplate. 2 | 3 | ### Technoliges: 4 | - React v^16.11.0 5 | - React hooks and context lifting data between components 6 | - Typescript 7 | - Jest 8 | - React-router 9 | - Axios 10 | - Scss 11 | - Prettier 12 | - Commitlint and lintstaged 13 | - Eslint 14 | - Editorconfig 15 | - Ramda 16 | 17 | Login username/password: `username=test` & `password=test` 18 | 19 | ## Directory Structure: 20 | ```bash 21 | ├── config 22 | ├── public 23 | ├── src 24 | │ ├── components 25 | │ ├── helpers 26 | │ ├── hooks 27 | │ ├── screens 28 | │ ├── state 29 | │ └── styles 30 | ├── build 31 | ├── README.md 32 | ├── .babelrc 33 | ├── .editorconfig 34 | ├── .eslintrc 35 | ├── .nvmrc 36 | ├── .prettier 37 | ├── commitlint.config.js 38 | ├── tsconfig.json 39 | ├── jest.config.js 40 | └── .gitignore 41 | ``` 42 | 43 | #### Screens folder 44 | This folder is of great importance and its subfolders are depended on the routes structure that you have created. For namely if you have a route like this: 45 | ```js 46 | http://localhost:3000/auth/login 47 | ``` 48 | So you have to create 2 subfolders into **screens** folder as below: 49 | ```bash 50 | ├── screens 51 | │ ├── Auth 52 | │ │ ├── Login 53 | │ │ | ├── index.tsx 54 | | | | index.tsx 55 | └── index.tsx 56 | ``` 57 | 58 | ### Available Scripts 59 | 60 | In the project directory, you can run: 61 | 62 | ### `npm start` 63 | 64 | Runs the app in the development mode.
65 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 66 | 67 | ### `npm test` 68 | 69 | Launches the test runner in the interactive watch mode.
70 | 71 | ### `npm run build` 72 | 73 | Builds the app for production to the `build` folder.
74 | It correctly bundles React in production mode and optimizes the build for the best performance. 75 | 76 | ### `npm run eject` 77 | 78 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 79 | 80 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 81 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /config/aliases.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | src: path.resolve(__dirname, "../src"), 5 | assets: path.resolve(__dirname, "../src/assets"), 6 | state: path.resolve(__dirname, "../src/state"), 7 | hoc: path.resolve(__dirname, "../src/hoc"), 8 | hooks: path.resolve(__dirname, "../src/hooks"), 9 | styles: path.resolve(__dirname, "../src/styles"), 10 | helpers: path.resolve(__dirname, "../src/helpers"), 11 | screens: path.resolve(__dirname, "../src/screens"), 12 | services: path.resolve(__dirname, "../src/services"), 13 | components: path.resolve(__dirname, "../src/components"), 14 | "package.json": path.resolve(__dirname, "..", "package.json"), 15 | }; 16 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const paths = require("./paths"); 4 | 5 | // Make sure that including paths.js after env.js will read .env variables. 6 | delete require.cache[require.resolve("./paths")]; 7 | 8 | const NODE_ENV = process.env.NODE_ENV; 9 | if (!NODE_ENV) { 10 | throw new Error("The NODE_ENV environment variable is required but was not specified."); 11 | } 12 | 13 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 14 | const dotenvFiles = [ 15 | `${paths.dotenv}.${NODE_ENV}.local`, 16 | `${paths.dotenv}.${NODE_ENV}`, 17 | // Don't include `.env.local` for `test` environment 18 | // since normally you expect tests to produce the same 19 | // results for everyone 20 | NODE_ENV !== "test" && `${paths.dotenv}.local`, 21 | paths.dotenv, 22 | ].filter(Boolean); 23 | 24 | // Load environment variables from .env* files. Suppress warnings using silent 25 | // if this file is missing. dotenv will never modify any environment variables 26 | // that have already been set. Variable expansion is supported in .env files. 27 | // https://github.com/motdotla/dotenv 28 | // https://github.com/motdotla/dotenv-expand 29 | dotenvFiles.forEach(dotenvFile => { 30 | if (fs.existsSync(dotenvFile)) { 31 | require("dotenv-expand")( 32 | require("dotenv").config({ 33 | path: dotenvFile, 34 | }), 35 | ); 36 | } 37 | }); 38 | 39 | // We support resolving modules according to `NODE_PATH`. 40 | // This lets you use absolute paths in imports inside large monorepos: 41 | // https://github.com/facebook/create-react-app/issues/253. 42 | // It works similar to `NODE_PATH` in Node itself: 43 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 44 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 45 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 46 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 47 | // We also resolve them to make sure all tools using them work consistently. 48 | const appDirectory = fs.realpathSync(process.cwd()); 49 | process.env.NODE_PATH = (process.env.NODE_PATH || "") 50 | .split(path.delimiter) 51 | .filter(folder => folder && !path.isAbsolute(folder)) 52 | .map(folder => path.resolve(appDirectory, folder)) 53 | .join(path.delimiter); 54 | 55 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 56 | // injected into the application via DefinePlugin in Webpack configuration. 57 | const REACT_APP = /^REACT_APP_/i; 58 | 59 | function getClientEnvironment(publicUrl) { 60 | const raw = Object.keys(process.env) 61 | .filter(key => REACT_APP.test(key)) 62 | .reduce( 63 | (env, key) => { 64 | env[key] = process.env[key]; 65 | return env; 66 | }, 67 | { 68 | // Useful for determining whether we’re running in production mode. 69 | // Most importantly, it switches React into the correct mode. 70 | NODE_ENV: process.env.NODE_ENV || "development", 71 | // Useful for resolving the correct path to static assets in `public`. 72 | // For example, . 73 | // This should only be used as an escape hatch. Normally you would put 74 | // images into the `src` and `import` them in code to get their paths. 75 | PUBLIC_URL: publicUrl, 76 | }, 77 | ); 78 | // Stringify all values so we can feed into Webpack DefinePlugin 79 | const stringified = { 80 | "process.env": Object.keys(raw).reduce((env, key) => { 81 | env[key] = JSON.stringify(raw[key]); 82 | return env; 83 | }, {}), 84 | }; 85 | 86 | return { raw, stringified }; 87 | } 88 | 89 | module.exports = getClientEnvironment; 90 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // This is a custom Jest transformer turning style imports into empty objects. 2 | // http://facebook.github.io/jest/docs/en/webpack.html 3 | 4 | module.exports = { 5 | process() { 6 | return "module.exports = {};"; 7 | }, 8 | getCacheKey() { 9 | // The output is always the same. 10 | return "cssTransform"; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFileName = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFileName}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const resolve = require('resolve'); 8 | 9 | /** 10 | * Get the baseUrl of a compilerOptions object. 11 | * 12 | * @param {Object} options 13 | */ 14 | function getAdditionalModulePaths(options = {}) { 15 | const baseUrl = options.baseUrl; 16 | 17 | // We need to explicitly check for null and undefined (and not a falsy value) because 18 | // TypeScript treats an empty string as `.`. 19 | if (baseUrl == null) { 20 | // If there's no baseUrl set we respect NODE_PATH 21 | // Note that NODE_PATH is deprecated and will be removed 22 | // in the next major release of create-react-app. 23 | 24 | const nodePath = process.env.NODE_PATH || ''; 25 | return nodePath.split(path.delimiter).filter(Boolean); 26 | } 27 | 28 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 29 | 30 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 31 | // the default behavior. 32 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 33 | return null; 34 | } 35 | 36 | // Allow the user set the `baseUrl` to `appSrc`. 37 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 38 | return [paths.appSrc]; 39 | } 40 | 41 | // Otherwise, throw an error. 42 | throw new Error( 43 | chalk.red.bold( 44 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 45 | ' Create React App does not support other values at this time.' 46 | ) 47 | ); 48 | } 49 | 50 | function getModules() { 51 | // Check if TypeScript is setup 52 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 53 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 54 | 55 | if (hasTsConfig && hasJsConfig) { 56 | throw new Error( 57 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 58 | ); 59 | } 60 | 61 | let config; 62 | 63 | // If there's a tsconfig.json we assume it's a 64 | // TypeScript project and set up the config 65 | // based on tsconfig.json 66 | if (hasTsConfig) { 67 | const ts = require(resolve.sync('typescript', { 68 | basedir: paths.appNodeModules, 69 | })); 70 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 71 | // Otherwise we'll check if there is jsconfig.json 72 | // for non TS projects. 73 | } else if (hasJsConfig) { 74 | config = require(paths.appJsConfig); 75 | } 76 | 77 | config = config || {}; 78 | const options = config.compilerOptions || {}; 79 | 80 | const additionalModulePaths = getAdditionalModulePaths(options); 81 | 82 | return { 83 | additionalModulePaths: additionalModulePaths, 84 | hasTsConfig, 85 | }; 86 | } 87 | 88 | module.exports = getModules(); 89 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const url = require("url"); 4 | 5 | // Make sure any symlinks in the project folder are resolved: 6 | // https://github.com/facebook/create-react-app/issues/637 7 | const appDirectory = fs.realpathSync(process.cwd()); 8 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 9 | 10 | const envPublicUrl = process.env.PUBLIC_URL; 11 | 12 | function ensureSlash (inputPath, needsSlash) { 13 | const hasSlash = inputPath.endsWith("/"); 14 | if (hasSlash && !needsSlash) { 15 | return inputPath.substr(0, inputPath.length - 1); 16 | } else if (!hasSlash && needsSlash) { 17 | return `${inputPath}/`; 18 | } else { 19 | return inputPath; 20 | } 21 | } 22 | 23 | const getPublicUrl = appPackageJson => 24 | envPublicUrl || require(appPackageJson).publicUrl; 25 | 26 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 27 | // "public path" at which the app is served. 28 | // Webpack needs to know it to put the right 15 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/silent-renew.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = "production"; 5 | process.env.NODE_ENV = "production"; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on("unhandledRejection", err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require("../config/env"); 16 | 17 | const path = require("path"); 18 | const chalk = require("react-dev-utils/chalk"); 19 | const fs = require("fs-extra"); 20 | const webpack = require("webpack"); 21 | const configFactory = require("../config/webpack.config"); 22 | const paths = require("../config/paths"); 23 | const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles"); 24 | const formatWebpackMessages = require("react-dev-utils/formatWebpackMessages"); 25 | const printHostingInstructions = require("react-dev-utils/printHostingInstructions"); 26 | const FileSizeReporter = require("react-dev-utils/FileSizeReporter"); 27 | const printBuildError = require("react-dev-utils/printBuildError"); 28 | 29 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; 30 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 31 | const useYarn = fs.existsSync(paths.yarnLockFile); 32 | 33 | // These sizes are pretty large. We'll warn for bundles exceeding them. 34 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 35 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 36 | 37 | const isInteractive = process.stdout.isTTY; 38 | 39 | // Warn and crash if required files are missing 40 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 41 | process.exit(1); 42 | } 43 | 44 | // Generate configuration 45 | const config = configFactory("production"); 46 | 47 | // We require that you explicitly set browsers and do not fall back to 48 | // browserslist defaults. 49 | const { checkBrowsers } = require("react-dev-utils/browsersHelper"); 50 | checkBrowsers(paths.appPath, isInteractive) 51 | .then(() => { 52 | // First, read the current file sizes in build directory. 53 | // This lets us display how much they changed later. 54 | return measureFileSizesBeforeBuild(paths.appBuild); 55 | }) 56 | .then(previousFileSizes => { 57 | // Remove all content but keep the directory so that 58 | // if you're in it, you don't end up in Trash 59 | fs.emptyDirSync(paths.appBuild); 60 | // Merge with the public folder 61 | copyPublicFolder(); 62 | // Start the webpack build 63 | return build(previousFileSizes); 64 | }) 65 | .then( 66 | ({ stats, previousFileSizes, warnings }) => { 67 | if (warnings.length) { 68 | console.log(chalk.yellow("Compiled with warnings.\n")); 69 | console.log(warnings.join("\n\n")); 70 | console.log( 71 | "\nSearch for the " + 72 | chalk.underline(chalk.yellow("keywords")) + 73 | " to learn more about each warning.", 74 | ); 75 | console.log("To ignore, add " + chalk.cyan("// eslint-disable-next-line") + " to the line before.\n"); 76 | } else { 77 | console.log(chalk.green("Compiled successfully.\n")); 78 | } 79 | 80 | console.log("File sizes after gzip:\n"); 81 | printFileSizesAfterBuild( 82 | stats, 83 | previousFileSizes, 84 | paths.appBuild, 85 | WARN_AFTER_BUNDLE_GZIP_SIZE, 86 | WARN_AFTER_CHUNK_GZIP_SIZE, 87 | ); 88 | console.log(); 89 | 90 | const appPackage = require(paths.appPackageJson); 91 | const publicUrl = paths.publicUrl; 92 | const publicPath = config.output.publicPath; 93 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 94 | printHostingInstructions(appPackage, publicUrl, publicPath, buildFolder, useYarn); 95 | }, 96 | err => { 97 | console.log(chalk.red("Failed to compile.\n")); 98 | printBuildError(err); 99 | process.exit(1); 100 | }, 101 | ) 102 | .catch(err => { 103 | if (err && err.message) { 104 | console.log(err.message); 105 | } 106 | process.exit(1); 107 | }); 108 | 109 | // Create the production build and print the deployment instructions. 110 | function build(previousFileSizes) { 111 | // We used to support resolving modules according to `NODE_PATH`. 112 | // This now has been deprecated in favor of jsconfig/tsconfig.json 113 | // This lets you use absolute paths in imports inside large monorepos: 114 | if (process.env.NODE_PATH) { 115 | console.log( 116 | chalk.yellow( 117 | "Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.", 118 | ), 119 | ); 120 | console.log(); 121 | } 122 | 123 | console.log("Creating an optimized production build..."); 124 | 125 | const compiler = webpack(config); 126 | return new Promise((resolve, reject) => { 127 | compiler.run((err, stats) => { 128 | let messages; 129 | if (err) { 130 | if (!err.message) { 131 | return reject(err); 132 | } 133 | messages = formatWebpackMessages({ 134 | errors: [err.message], 135 | warnings: [], 136 | }); 137 | } else { 138 | messages = formatWebpackMessages(stats.toJson({ all: false, warnings: true, errors: true })); 139 | } 140 | if (messages.errors.length) { 141 | // Only keep the first error. Others are often indicative 142 | // of the same problem, but confuse the reader with noise. 143 | if (messages.errors.length > 1) { 144 | messages.errors.length = 1; 145 | } 146 | return reject(new Error(messages.errors.join("\n\n"))); 147 | } 148 | if ( 149 | process.env.CI && 150 | (typeof process.env.CI !== "string" || process.env.CI.toLowerCase() !== "false") && 151 | messages.warnings.length 152 | ) { 153 | console.log( 154 | chalk.yellow( 155 | "\nTreating warnings as errors because process.env.CI = true.\n" + 156 | "Most CI servers set it automatically.\n", 157 | ), 158 | ); 159 | return reject(new Error(messages.warnings.join("\n\n"))); 160 | } 161 | 162 | return resolve({ 163 | stats, 164 | previousFileSizes, 165 | warnings: messages.warnings, 166 | }); 167 | }); 168 | }); 169 | } 170 | 171 | function copyPublicFolder() { 172 | fs.copySync(paths.appPublic, paths.appBuild, { 173 | dereference: true, 174 | filter: file => file !== paths.appHtml, 175 | }); 176 | } 177 | -------------------------------------------------------------------------------- /scripts/service-worker/builder.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { injectManifest } = require("workbox-build"); 3 | 4 | const buildPath = path.join(__dirname, "../../build/"); 5 | const workboxConfig = { 6 | globDirectory: buildPath, 7 | exclude: ["service-worker.js"], 8 | swSrc: "./sw-template.js", 9 | swDest: path.resolve(buildPath, "service-worker.js"), 10 | }; 11 | 12 | injectManifest(workboxConfig).then(({ count, size }) => { 13 | console.log(`Generated ${workboxConfig.swDest}, which will precache ${count} files, totaling ${size} bytes.`); 14 | }); 15 | -------------------------------------------------------------------------------- /scripts/service-worker/sw-template.js: -------------------------------------------------------------------------------- 1 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 2 | 3 | if (workbox) { 4 | console.log("Workbox is loaded"); 5 | 6 | const CACHE_NAME_DETAILS = { 7 | prefix: "react-test", 8 | suffix: "v1.0", 9 | precache: "install-time", 10 | runtime: "run-time", 11 | }; 12 | workbox.core.setCacheNameDetails(CACHE_NAME_DETAILS); 13 | workbox.core.skipWaiting(); 14 | workbox.core.clientsClaim(); 15 | 16 | /* injection point for manifest files. */ 17 | workbox.precaching.precacheAndRoute([], { 18 | cleanUrls: true, 19 | }); 20 | 21 | workbox.routing.registerRoute( 22 | new RegExp(/\/static\/.*\.(?:js|css)/), 23 | new workbox.strategies.StaleWhileRevalidate({ 24 | cacheName: `${CACHE_NAME_DETAILS.prefix}-assets-${CACHE_NAME_DETAILS.suffix}`, 25 | cacheExpiration: { 26 | maxEntries: 3, 27 | maxAgeSeconds: 7 * 24 * 60 * 60, 28 | }, 29 | }), 30 | ); 31 | 32 | workbox.routing.registerRoute( 33 | /.*\.(?:png|jpe?g|svg|gif)/, 34 | new workbox.strategies.StaleWhileRevalidate({ 35 | cacheName: `${CACHE_NAME_DETAILS.prefix}-images-${CACHE_NAME_DETAILS.suffix}`, 36 | cacheExpiration: { 37 | maxEntries: 3, 38 | maxAgeSeconds: 7 * 24 * 60 * 60, 39 | }, 40 | }), 41 | ); 42 | 43 | workbox.routing.registerRoute( 44 | /.*\.(?:ttf|woff2?|eot)/, 45 | new workbox.strategies.StaleWhileRevalidate({ 46 | cacheName: `${CACHE_NAME_DETAILS.prefix}-fonts-${CACHE_NAME_DETAILS.suffix}`, 47 | cacheExpiration: { 48 | maxEntries: 3, 49 | maxAgeSeconds: 7 * 24 * 60 * 60, 50 | }, 51 | }), 52 | ); 53 | 54 | workbox.routing.registerRoute(/[?&]bustprecache=.*$/i, new workbox.strategies.NetworkFirst()); 55 | /* custom cache rules*/ 56 | workbox.routing.registerNavigationRoute("/index.html", { 57 | blacklist: [/^\/_/, /\/[^\/]+\.[^\/]+$/], 58 | }); 59 | 60 | self.addEventListener("activate", function() { 61 | caches.keys().then(function(cacheNames) { 62 | return Promise.all( 63 | cacheNames.map(function(cacheName) { 64 | if (!cacheName.endsWith(CACHE_NAME_DETAILS.suffix)) { 65 | return caches.delete(cacheName); 66 | } 67 | }), 68 | ); 69 | }); 70 | self.clients.matchAll().then(clients => { 71 | clients.forEach(client => { 72 | client.postMessage({ 73 | type: "versionCheck", 74 | version: CACHE_NAME_DETAILS.suffix, 75 | }); 76 | }); 77 | }); 78 | // ); 79 | }); 80 | workbox.routing.setCatchHandler(({ event }) => { 81 | switch (event.request.destination) { 82 | case "document": 83 | return caches.match(url); 84 | break; 85 | 86 | default: 87 | // If we don't have a fallback, just return an error response. 88 | return Response.error(); 89 | } 90 | }); 91 | } else { 92 | console.log("Boo! Workbox didn't load"); 93 | } 94 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = "development"; 5 | process.env.NODE_ENV = "development"; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on("unhandledRejection", err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require("../config/env"); 16 | 17 | const fs = require("fs"); 18 | const chalk = require("react-dev-utils/chalk"); 19 | const webpack = require("webpack"); 20 | const WebpackDevServer = require("webpack-dev-server"); 21 | const clearConsole = require("react-dev-utils/clearConsole"); 22 | const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles"); 23 | const { choosePort, createCompiler, prepareProxy, prepareUrls } = require("react-dev-utils/WebpackDevServerUtils"); 24 | const openBrowser = require("react-dev-utils/openBrowser"); 25 | const paths = require("../config/paths"); 26 | const configFactory = require("../config/webpack.config"); 27 | const createDevServerConfig = require("../config/webpackDevServer.config"); 28 | const internalIp = require("internal-ip"); 29 | 30 | const useYarn = fs.existsSync(paths.yarnLockFile); 31 | const isInteractive = process.stdout.isTTY; 32 | 33 | // Warn and crash if required files are missing 34 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 35 | process.exit(1); 36 | } 37 | 38 | // Tools like Cloud9 rely on this. 39 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 40 | const HOST = internalIp.v4.sync() || process.env.HOST || "0.0.0.0"; 41 | 42 | if (process.env.HOST) { 43 | console.log( 44 | chalk.cyan(`Attempting to bind to HOST environment variable: ${chalk.yellow(chalk.bold(process.env.HOST))}`), 45 | ); 46 | console.log("If this was unintentional, check that you haven't mistakenly set it in your shell."); 47 | console.log(`Learn more here: ${chalk.yellow("https://bit.ly/CRA-advanced-config")}`); 48 | console.log(); 49 | } 50 | 51 | // We require that you explicitly set browsers and do not fall back to 52 | // browserslist defaults. 53 | const { checkBrowsers } = require("react-dev-utils/browsersHelper"); 54 | checkBrowsers(paths.appPath, isInteractive) 55 | .then(() => { 56 | // We attempt to use the default port but if it is busy, we offer the user to 57 | // run on a different port. `choosePort()` Promise resolves to the next free port. 58 | return choosePort(HOST, DEFAULT_PORT); 59 | }) 60 | .then(port => { 61 | if (port == null) { 62 | // We have not found a port. 63 | return; 64 | } 65 | const config = configFactory("development"); 66 | const protocol = process.env.HTTPS === "true" ? "https" : "http"; 67 | const appName = require(paths.appPackageJson).name; 68 | const useTypeScript = fs.existsSync(paths.appTsConfig); 69 | const urls = prepareUrls(protocol, HOST, port); 70 | const devSocket = { 71 | warnings: warnings => devServer.sockWrite(devServer.sockets, "warnings", warnings), 72 | errors: errors => devServer.sockWrite(devServer.sockets, "errors", errors), 73 | }; 74 | // Create a webpack compiler that is configured with custom messages. 75 | const compiler = createCompiler({ 76 | appName, 77 | config, 78 | devSocket, 79 | urls, 80 | useYarn, 81 | useTypeScript, 82 | webpack, 83 | }); 84 | // Load proxy config 85 | const proxySetting = require(paths.appPackageJson).proxy; 86 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 87 | // Serve webpack assets generated by the compiler over a web server. 88 | const serverConfig = createDevServerConfig(proxyConfig, urls.lanUrlForConfig); 89 | const devServer = new WebpackDevServer(compiler, serverConfig); 90 | // Launch WebpackDevServer. 91 | devServer.listen(port, HOST, err => { 92 | if (err) { 93 | return console.log(err); 94 | } 95 | if (isInteractive) { 96 | clearConsole(); 97 | } 98 | 99 | // We used to support resolving modules according to `NODE_PATH`. 100 | // This now has been deprecated in favor of jsconfig/tsconfig.json 101 | // This lets you use absolute paths in imports inside large monorepos: 102 | if (process.env.NODE_PATH) { 103 | console.log( 104 | chalk.yellow( 105 | "Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.", 106 | ), 107 | ); 108 | console.log(); 109 | } 110 | 111 | console.log(chalk.cyan("Starting the development server...\n")); 112 | openBrowser(urls.localUrlForBrowser); 113 | }); 114 | 115 | ["SIGINT", "SIGTERM"].forEach(function(sig) { 116 | process.on(sig, function() { 117 | devServer.close(); 118 | process.exit(); 119 | }); 120 | }); 121 | }) 122 | .catch(err => { 123 | if (err && err.message) { 124 | console.log(err.message); 125 | } 126 | process.exit(1); 127 | }); 128 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = "test"; 3 | process.env.NODE_ENV = "test"; 4 | process.env.PUBLIC_URL = ""; 5 | 6 | // Makes the script crash on unhandled rejections instead of silently 7 | // ignoring them. In the future, promise rejections that are not handled will 8 | // terminate the Node.js process with a non-zero exit code. 9 | process.on("unhandledRejection", err => { 10 | throw err; 11 | }); 12 | 13 | // Ensure environment variables are read. 14 | require("../config/env"); 15 | 16 | const jest = require("jest"); 17 | const execSync = require("child_process").execSync; 18 | const argv = process.argv.slice(2); 19 | 20 | function isInGitRepository() { 21 | try { 22 | execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" }); 23 | return true; 24 | } catch (e) { 25 | return false; 26 | } 27 | } 28 | 29 | function isInMercurialRepository() { 30 | try { 31 | execSync("hg --cwd . root", { stdio: "ignore" }); 32 | return true; 33 | } catch (e) { 34 | return false; 35 | } 36 | } 37 | 38 | // Watch unless on CI or explicitly running all tests 39 | if (!process.env.CI && argv.indexOf("--watchAll") === -1 && argv.indexOf("--watchAll=false") === -1) { 40 | // https://github.com/facebook/create-react-app/issues/5210 41 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 42 | argv.push(hasSourceControl ? "--watch" : "--watchAll"); 43 | } 44 | 45 | jest.run(argv); 46 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Router, Switch, Redirect } from "react-router-dom"; 3 | // Shared components 4 | import PublicRoute from "components/PublicRoute"; 5 | import PrivateRoute from "components/PrivateRoute"; 6 | // Local components 7 | import Auth from "screens/Auth"; 8 | import Dashboard from "screens/Dashboard"; 9 | // Utilities and hooks 10 | import history from "helpers/history"; 11 | import { read } from "helpers/localStorage"; 12 | import { appReducer, appInitialState } from "state/reducer.app"; 13 | import { AppDispatchContext, AppStateContext } from "state/index.app"; 14 | // Styles 15 | import "./styles/app.scss"; 16 | 17 | const App: React.FC = () => { 18 | const [appState, appDispatch] = React.useReducer(appReducer, appInitialState); 19 | 20 | React.useEffect(() => { 21 | if (read("token")) { 22 | appDispatch({ 23 | type: "LOGGED_IN", 24 | }); 25 | } 26 | }, []); 27 | 28 | return ( 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | ); 43 | }; 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /src/components/Maybe/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props { 4 | condition?: boolean; 5 | } 6 | 7 | // eslint-disable-next-line no-extra-boolean-cast 8 | const Maybe: React.FC = ({ children, condition }) => (Boolean(condition) ? children : null); 9 | 10 | export default Maybe; 11 | -------------------------------------------------------------------------------- /src/components/PageTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Helmet } from "react-helmet"; 3 | 4 | interface IContentItem { 5 | summary: string; 6 | smallIconUrl: string; 7 | title: string; 8 | contentUrl: string; 9 | } 10 | interface IProps { 11 | title?: string; 12 | description?: string; 13 | contentItem?: IContentItem | null; 14 | } 15 | 16 | const PageTitle: React.FC = ({ title, description, contentItem }: IProps) => { 17 | return ( 18 | 19 | {title} 20 | {description && !contentItem && } 21 | {contentItem && [ 22 | , 23 | , 24 | , 25 | , 26 | , 27 | , 28 | , 29 | , 30 | , 31 | , 32 | ]} 33 | 34 | ); 35 | }; 36 | 37 | export default PageTitle; 38 | -------------------------------------------------------------------------------- /src/components/PrivateRoute/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-ignore */ 2 | import * as React from "react"; 3 | import { Route, Redirect } from "react-router-dom"; 4 | import { RouteProps } from "react-router"; 5 | // Utilities and hooks 6 | import { getRedirectPath } from "helpers/history"; 7 | import { useAppState } from "state/index.app"; 8 | 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 10 | // @ts-ignore 11 | interface IProps extends RouteProps { 12 | path: string; 13 | props?: any; 14 | component: React.ElementType; 15 | } 16 | const PrivateRoute: React.FC = ({ 17 | component: Component, 18 | props: authenticatedComponentProps, 19 | ...rest 20 | }) => { 21 | const state = useAppState(); 22 | // Show the component only when the user is logged in 23 | // Otherwise, redirect the user to /auth/login page 24 | return ( 25 | { 28 | // @ts-ignore 29 | const redirect = getRedirectPath(props); 30 | // @ts-ignore 31 | return state.isLoggedIn ? ( 32 | 33 | ) : ( 34 | 35 | ); 36 | }} 37 | /> 38 | ); 39 | }; 40 | 41 | export default PrivateRoute; 42 | -------------------------------------------------------------------------------- /src/components/PublicRoute/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Route, Redirect } from "react-router-dom"; 3 | import { RouteProps } from "react-router"; 4 | import { useAppState } from "state/index.app"; 5 | // Hooks and Utilities 6 | 7 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 8 | // @ts-ignore 9 | interface IProps extends RouteProps { 10 | path: string; 11 | component: React.ElementType; 12 | props?: any; 13 | /** 14 | * restricted = false meaning public route 15 | * restricted = true meaning protected route 16 | */ 17 | restricted: boolean; 18 | } 19 | const PublicRoute: React.FC = ({ 20 | component: Component, 21 | restricted, 22 | props: unauthenticatedRouteProps, 23 | ...rest 24 | }) => { 25 | const state = useAppState(); 26 | 27 | return ( 28 | 31 | // @ts-ignore 32 | state.isLoggedIn && restricted ? ( 33 | 34 | ) : ( 35 | 36 | ) 37 | } 38 | /> 39 | ); 40 | }; 41 | 42 | export default PublicRoute; 43 | -------------------------------------------------------------------------------- /src/helpers/api.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios"; 2 | import * as R from "ramda"; 3 | 4 | const baseURL = window.location.origin; 5 | // a new instance of axios with a custom config. 6 | const instance: AxiosInstance = axios.create({ 7 | baseURL, 8 | }); 9 | 10 | instance.interceptors.response.use(response => { 11 | return R.pathOr(response, ["data"])(response); 12 | }); 13 | 14 | export default instance; 15 | -------------------------------------------------------------------------------- /src/helpers/endpoints.ts: -------------------------------------------------------------------------------- 1 | import api from "./api"; 2 | 3 | interface BaseRequest { 4 | data: T; 5 | } 6 | ///////Authentication functions 7 | export interface ILogin { 8 | email: string; 9 | password: string; 10 | } 11 | export const login = (user: ILogin): Promise> => { 12 | return api.post("login", { password: user.password, username: user.email }); 13 | }; 14 | export const logout = (): Promise => { 15 | return api.post("logout"); 16 | }; 17 | ///// Fames 18 | export interface IFame { 19 | id: string; 20 | name: string; 21 | dob: string; 22 | image: string; 23 | } 24 | export const fetchFame = (id?: string): Promise> => { 25 | return api.get(`fames/${id}`); 26 | }; 27 | export const fetchFames = (): Promise> => { 28 | return api.get("fames"); 29 | }; 30 | -------------------------------------------------------------------------------- /src/helpers/history.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory, Location, History } from "history"; 2 | import qs from "qs"; 3 | 4 | interface ILocation extends Location { 5 | query: { 6 | [query: string]: string; 7 | }; 8 | state: { 9 | [state: string]: string; 10 | }; 11 | } 12 | interface IHistoryProps extends History { 13 | location: ILocation; 14 | } 15 | 16 | const history = createBrowserHistory() as IHistoryProps; 17 | 18 | history.location = { 19 | ...history.location, 20 | query: qs.parse(history.location.search.substr(1)), 21 | state: {}, 22 | }; 23 | history.listen(() => { 24 | history.location = { 25 | ...history.location, 26 | query: qs.parse(history.location.search.substr(1)), 27 | state: history.location.state || {}, 28 | }; 29 | }); 30 | 31 | const getRedirectPath = (router = window) => encodeURIComponent(`${router.location.pathname}${router.location.search}`); 32 | const { go, goBack, push, replace } = history; 33 | 34 | export { go, goBack, push, replace, getRedirectPath }; 35 | export default history; 36 | -------------------------------------------------------------------------------- /src/helpers/localStorage.ts: -------------------------------------------------------------------------------- 1 | type Key = string; 2 | type Read = string | {} | T[] | number | boolean | undefined; 3 | /** 4 | * Register an item via key and value in local storage 5 | * @param {Key} key 6 | * @param {any} data 7 | * @returns {any} retrun 8 | */ 9 | export function store(key: Key, data: any, storage = window.localStorage): any { 10 | if (!window.localStorage || !key) { 11 | return; 12 | } 13 | storage.setItem(key, JSON.stringify(data)); 14 | } 15 | /** 16 | * Get an item in local storage db 17 | * @param {Key} key 18 | * @returns {Read} 19 | */ 20 | export function read(key: Key, storage = window.localStorage): Read { 21 | if (!storage || !key) { 22 | return; 23 | } 24 | const item: any = storage.getItem(key); 25 | if (!item) { 26 | return; 27 | } 28 | 29 | const parse = JSON.parse; 30 | try { 31 | return parse(item); 32 | } catch (error) { 33 | return parse(`"${item}"`); 34 | } 35 | } 36 | /** 37 | * Remove an item in local storage db 38 | * @param {Key} key 39 | * @returns {any} 40 | */ 41 | export function remove(key: Key, storage = window.localStorage): any { 42 | if (!storage || !key) { 43 | return; 44 | } 45 | 46 | storage.removeItem(key); 47 | } 48 | -------------------------------------------------------------------------------- /src/helpers/notify.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 2 | // @ts-ignore 3 | import snackbar from "node-snackbar/dist/snackbar"; 4 | import "node-snackbar/dist/snackbar.css"; 5 | 6 | type NotifyPosition = "bottom-right" | "bottom-center" | "bottom-left" | "top-right" | "top-center" | "top-left"; 7 | interface INotify { 8 | text?: string; 9 | textColor: string; 10 | width: string | number; 11 | showAction: boolean; 12 | actionText: string; 13 | actionTextColor: string; 14 | backgroundColor: string; 15 | pos: NotifyPosition; 16 | duration: number; 17 | customClass: string; 18 | onClose: (element: HTMLDivElement) => void; 19 | onActionClick: (element: HTMLDivElement) => void; 20 | [setting: string]: any; 21 | } 22 | type Notify = Partial; 23 | export const showNotify = (settings: Notify | Error): void => { 24 | if (settings) { 25 | return snackbar.show({ 26 | customClass: "snakbarify", 27 | actionText: "Close", 28 | showAction: false, 29 | pos: "bottom-center", 30 | ...settings, 31 | } as Notify); 32 | } 33 | }; 34 | 35 | export const closeNotify = (): void => snackbar.close(); 36 | 37 | export default showNotify; 38 | -------------------------------------------------------------------------------- /src/hooks/useMedia.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | // Use Media 4 | export default function useMedia(queries: any, values: { [x: string]: any }, defaultValue: any) { 5 | const mediaQueryLists = queries.map((q: string) => window.matchMedia(q)); 6 | const getValue = () => { 7 | const index = mediaQueryLists.findIndex((mql: { matches: any }) => mql.matches); 8 | return typeof values[index] !== "undefined" ? values[index] : defaultValue; 9 | }; 10 | 11 | const [value, setValue] = useState(getValue); 12 | 13 | useEffect(() => { 14 | const handler = () => setValue(getValue); 15 | mediaQueryLists.forEach((mql: { addListener: (arg0: () => void) => void }) => mql.addListener(handler)); 16 | return () => 17 | mediaQueryLists.forEach((mql: { removeListener: (arg0: () => void) => void }) => 18 | mql.removeListener(handler), 19 | ); 20 | // eslint-disable-next-line react-hooks/exhaustive-deps 21 | }, []); 22 | 23 | return value; 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/useOffline.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | interface IUseOffline { 4 | isOffline: boolean; 5 | setOffline: (status: boolean) => void; 6 | } 7 | 8 | function useOffline(): IUseOffline { 9 | const [isOffline, setOffline] = useState(false); 10 | 11 | useEffect(() => { 12 | window.addEventListener("online", () => { 13 | setOffline(false); 14 | }); 15 | window.addEventListener("offline", () => { 16 | setOffline(true); 17 | }); 18 | 19 | return () => { 20 | window.removeEventListener("online", () => { 21 | setOffline(false); 22 | }); 23 | window.removeEventListener("offline", () => { 24 | setOffline(true); 25 | }); 26 | }; 27 | }); 28 | 29 | return { isOffline, setOffline }; 30 | } 31 | 32 | export default useOffline; 33 | -------------------------------------------------------------------------------- /src/hooks/useUser.ts: -------------------------------------------------------------------------------- 1 | // Utilities 2 | import history, { push, getRedirectPath } from "helpers/history"; 3 | import * as R from "ramda"; 4 | import { remove, store } from "helpers/localStorage"; 5 | import { useAppDispatch } from "state/index.app"; 6 | import { login as LoginUser, ILogin, logout as LogoutUser } from "helpers/endpoints"; 7 | 8 | function useUser() { 9 | const dispatch = useAppDispatch(); 10 | async function login({ email, password }: ILogin) { 11 | try { 12 | const res = await LoginUser({ 13 | email, 14 | password, 15 | }); 16 | 17 | const isSuccess = res.data.success; 18 | if (isSuccess) { 19 | store("token", "test-isSuccess"); 20 | 21 | dispatch({ 22 | type: "LOGGED_IN", 23 | }); 24 | 25 | const redirectPath = R.pathOr(null, ["query", "redirect"])(history.location); 26 | if (redirectPath) { 27 | push(redirectPath); 28 | return; 29 | } 30 | 31 | push("/dashboard"); 32 | } else { 33 | // eslint-disable-next-line no-throw-literal 34 | throw { message: "Something went wrong" }; 35 | } 36 | } catch (error) { 37 | throw error; 38 | } 39 | } 40 | 41 | /** 42 | * Logout the user from the application 43 | * Steps: 44 | * 1) Remove user token 45 | * 2) Reset the user into the User store. TODO: THIS ONE SHOULD BE COMPLETED 46 | * 3) Redirect user to login page 47 | */ 48 | async function logout() { 49 | await LogoutUser(); 50 | 51 | remove("token"); 52 | 53 | dispatch({ type: "RESET" }); 54 | 55 | push(`/auth/login?redirect=${getRedirectPath()}`); 56 | } 57 | 58 | return { 59 | login, 60 | logout, 61 | }; 62 | } 63 | 64 | export default useUser; 65 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | import { ConfigProvider } from "antd"; 6 | import enUS from "antd/es/locale-provider/en_US"; 7 | 8 | import * as serviceWorker from "./serviceWorker"; 9 | 10 | import "./styles/index.scss"; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | document.getElementById("root") as HTMLElement, 17 | ); 18 | 19 | if (process.env.NODE_ENV === "production") { 20 | serviceWorker.register({ 21 | onUpdate() { 22 | const newUpdateEvent = new Event("newContentAvailble"); 23 | 24 | window.dispatchEvent(newUpdateEvent); 25 | }, 26 | }); 27 | } else { 28 | serviceWorker.unregister(); 29 | } 30 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | readonly NODE_ENV: "development" | "production" | "test"; 8 | readonly PUBLIC_URL: string; 9 | } 10 | } 11 | 12 | declare module "*.bmp" { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module "*.gif" { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module "*.jpg" { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module "*.jpeg" { 28 | const src: string; 29 | export default src; 30 | } 31 | 32 | declare module "*.png" { 33 | const src: string; 34 | export default src; 35 | } 36 | 37 | declare module "*.webp" { 38 | const src: string; 39 | export default src; 40 | } 41 | 42 | declare module "*.svg" { 43 | import * as React from "react"; 44 | 45 | export const ReactComponent: React.FunctionComponent>; 46 | 47 | const src: string; 48 | export default src; 49 | } 50 | 51 | declare module "*.module.css" { 52 | const classes: { readonly [key: string]: string }; 53 | export default classes; 54 | } 55 | 56 | declare module "*.module.scss" { 57 | const classes: { readonly [key: string]: string }; 58 | export default classes; 59 | } 60 | 61 | declare module "*.module.sass" { 62 | const classes: { readonly [key: string]: string }; 63 | export default classes; 64 | } 65 | -------------------------------------------------------------------------------- /src/screens/Auth/components/Main/index.module.scss: -------------------------------------------------------------------------------- 1 | .auth-wrapper { 2 | width: 100%; 3 | height: 100vh; 4 | align-items: stretch; 5 | &__auth-right { 6 | margin-top: 30px; 7 | margin-bottom: 30px; 8 | } 9 | 10 | &__auth-left[class~="ant-col"] { 11 | background-image: linear-gradient(20deg, var(--blue), var(--green-light)); 12 | min-height: 100vh; 13 | } 14 | &__auth-right[class~="ant-col"] { 15 | min-height: 100vh; 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | padding: 50px 0; 20 | } 21 | &__auth-header { 22 | text-align: center; 23 | margin-bottom: 30px; 24 | 25 | &__logo { 26 | width: 145px; 27 | height: 100px; 28 | background-image: linear-gradient(20deg, var(--blue), var(--green-light)); 29 | margin: 0 auto; 30 | margin-bottom: 30px; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/screens/Auth/components/Main/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // UI Frameworks 3 | import { Col, Row, Typography } from "antd"; 4 | // Shared components 5 | import PageTitle from "components/PageTitle"; 6 | // Utilities and hooks 7 | import cs from "classnames"; 8 | // Styles 9 | import styles from "./index.module.scss"; 10 | import Maybe from "components/Maybe"; 11 | 12 | interface IProps { 13 | title?: string; 14 | children: React.ReactNode; 15 | } 16 | 17 | const { Title } = Typography; 18 | const Main: React.FC = (props: IProps) => { 19 | return ( 20 | 21 | 28 | 29 |
30 |
31 | 32 | {props.title} 33 | 34 |
35 |
{props.children}
36 | 37 | 38 |
39 | ); 40 | }; 41 | Main.defaultProps = { 42 | title: "Authentication", 43 | }; 44 | 45 | export default Main; 46 | -------------------------------------------------------------------------------- /src/screens/Auth/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Switch, Redirect } from "react-router-dom"; 3 | 4 | import PublicRoute from "components/PublicRoute"; 5 | 6 | import Login from "./screens/Login"; 7 | 8 | const Auth: React.FC = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default Auth; 18 | -------------------------------------------------------------------------------- /src/screens/Auth/screens/Login/index.module.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | div[class~="ant-form-item-label"] { 3 | text-align: right; 4 | margin-right: 4px; 5 | 6 | label[class~="ant-form-item-required"]::before { 7 | display: none; 8 | } 9 | } 10 | &__forget-pass { 11 | float: left; 12 | } 13 | &__register-btn { 14 | text-align: center; 15 | color: var(--gray); 16 | margin-top: 15px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/screens/Auth/screens/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | // UI Frameworks 3 | import { Form, Input, Button } from "antd"; 4 | import { FormComponentProps } from "antd/lib/form"; 5 | // Local shared components 6 | import Main from "screens/Auth/components/Main"; 7 | // Services 8 | import showNotify from "helpers/notify"; 9 | // Styles 10 | import styles from "./index.module.scss"; 11 | import useUser from "hooks/useUser"; 12 | 13 | const Login: React.FC = props => { 14 | const user = useUser(); 15 | const [loading, setLoading] = React.useState(false); 16 | const { getFieldDecorator } = props.form; 17 | 18 | function handleSubmit(e: React.FormEvent) { 19 | e.preventDefault(); 20 | 21 | props.form.validateFields(async (err, values) => { 22 | if (!err) { 23 | try { 24 | setLoading(true); 25 | await user.login(values); 26 | showNotify({ text: "Logged-in successfully", duration: 3000 }); 27 | setLoading(false); 28 | } catch (error) { 29 | setLoading(false); 30 | showNotify({ text: "Something went wrong! check email and password", duration: 3000 }); 31 | 32 | throw error; 33 | } 34 | } 35 | }); 36 | } 37 | 38 | return ( 39 |
40 |
handleSubmit(e)}> 41 | 42 | {getFieldDecorator("email", { 43 | rules: [ 44 | { 45 | required: true, 46 | message: "Enter email", 47 | transform: value => (value ? value.trim() : value), 48 | }, 49 | ], 50 | })()} 51 | 52 | 53 | {getFieldDecorator("password", { 54 | rules: [ 55 | { 56 | required: true, 57 | transform: value => (value ? value.trim() : value), 58 | message: "Enter password", 59 | }, 60 | ], 61 | })()} 62 | 63 | 64 | 67 | 68 |
69 |
70 | ); 71 | }; 72 | 73 | export default Form.create({ name: "login" })(Login); 74 | -------------------------------------------------------------------------------- /src/screens/Dashboard/components/Actor/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // UI Frameworks 3 | import { Card } from "antd"; 4 | 5 | const { Meta } = Card; 6 | 7 | interface IProps { 8 | name: string; 9 | className?: string; 10 | dob: string; 11 | image: string; 12 | } 13 | 14 | const Actor: React.FC = props => { 15 | return ( 16 | }> 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Actor; 23 | -------------------------------------------------------------------------------- /src/screens/Dashboard/index.module.scss: -------------------------------------------------------------------------------- 1 | .dashboard { 2 | padding: 50px 0; 3 | 4 | &__actors { 5 | transition: all 0.2s; 6 | 7 | &:hover { 8 | transform: scale(1.1); 9 | z-index: 2; 10 | cursor: pointer; 11 | box-shadow: 0 0 25px 2px rgba(0, 0, 0, 0.3); 12 | } 13 | } 14 | 15 | &__signoutBtn { 16 | position: fixed !important; 17 | left: 30px; 18 | bottom: 30px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/screens/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | // UI Framework and Global Components 3 | import { Row, Col, Modal, Button } from "antd"; 4 | import { Switch, Route } from "react-router"; 5 | // Shared components 6 | import PageTitle from "components/PageTitle"; 7 | // Local components 8 | import Fame from "./screens/Fame"; 9 | import Actor from "./components/Actor"; 10 | // Utilities and hooks 11 | import * as R from "ramda"; 12 | import cs from "classnames"; 13 | import useUser from "hooks/useUser"; 14 | import { push } from "helpers/history"; 15 | import { fetchFames, IFame } from "helpers/endpoints"; 16 | import { useAppDispatch, useAppState } from "state/index.app"; 17 | // Styles 18 | import styles from "./index.module.scss"; 19 | 20 | const { confirm } = Modal; 21 | 22 | const Dashboard: React.FC = () => { 23 | const dispatch = useAppDispatch(); 24 | const state = useAppState(); 25 | const user = useUser(); 26 | 27 | React.useEffect(() => { 28 | // @ts-ignore 29 | if (state.fames.length === 0) { 30 | (async () => { 31 | try { 32 | const res = await fetchFames(); 33 | const fames = res.data.list; 34 | 35 | dispatch({ type: "ADD_FAMES", payload: { fames } }); 36 | } catch (error) { 37 | throw error; 38 | } 39 | })(); 40 | } 41 | // eslint-disable-next-line react-hooks/exhaustive-deps 42 | }, []); 43 | 44 | function showConfirm() { 45 | confirm({ 46 | title: "Are you sure you want to sign-out?", 47 | async onOk() { 48 | await user.logout(); 49 | }, 50 | }); 51 | } 52 | 53 | return ( 54 |
55 | 56 | 57 | 58 | 59 | {R.map(fame => { 60 | return ( 61 | push(`/dashboard/fame/${fame.id}`)} 63 | key={fame.id} 64 | span={8} 65 | style={{ marginBottom: 30 }} 66 | > 67 | 73 | 74 | ); 75 | })(state.fames)} 76 | 77 | 78 | 79 | 80 | 83 |
84 | ); 85 | }; 86 | 87 | export default Dashboard; 88 | -------------------------------------------------------------------------------- /src/screens/Dashboard/screens/Fame/index.module.scss: -------------------------------------------------------------------------------- 1 | .fame { 2 | margin-top: 50px; 3 | 4 | &__back { 5 | margin-bottom: 20px; 6 | } 7 | 8 | &__content { 9 | text-align: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/screens/Dashboard/screens/Fame/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | import React, { useEffect, useState } from "react"; 3 | // UI Frameworks 4 | import { Icon, Typography, Avatar } from "antd"; 5 | // Hooks 6 | import { useParams } from "react-router"; 7 | import { useAppState } from "state/index.app"; 8 | // Utilities 9 | import { IFame } from "helpers/endpoints"; 10 | // Styles 11 | import styles from "./index.module.scss"; 12 | import showNotify from "helpers/notify"; 13 | import { Link } from "react-router-dom"; 14 | import { push } from "helpers/history"; 15 | 16 | const { Title, Text } = Typography; 17 | 18 | const Fame = () => { 19 | const params = useParams<{ id: string }>(); 20 | const state = useAppState(); 21 | const [fame, setFame] = useState>({}); 22 | 23 | useEffect(() => { 24 | const fame = state.fames.find(({ id }) => id === params.id); 25 | if (fame) { 26 | setFame(fame); 27 | } else { 28 | push("/dashboard"); 29 | showNotify({ text: "There is no fame" }); 30 | } 31 | // eslint-disable-next-line 32 | }, []); 33 | 34 | return ( 35 |
36 |
37 | 38 | 39 | 40 |
41 |
42 | {fame.name} 43 | Birthday: {fame.dob} 44 |
45 |
46 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default Fame; 53 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/), 19 | ); 20 | 21 | interface IConfig { 22 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 23 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 24 | } 25 | 26 | export function register(config?: IConfig) { 27 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 28 | // The URL constructor is available in all browsers that support SW. 29 | const publicUrl = new URL((process as { env: { [key: string]: string } }).env.PUBLIC_URL, window.location.href); 30 | if (publicUrl.origin !== window.location.origin) { 31 | // Our service worker won't work if PUBLIC_URL is on a different origin 32 | // from what our page is served on. This might happen if a CDN is used to 33 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 34 | return; 35 | } 36 | 37 | window.addEventListener("load", () => { 38 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 39 | 40 | if (isLocalhost) { 41 | // This is running on localhost. Let's check if a service worker still exists or not. 42 | checkValidServiceWorker(swUrl, config); 43 | 44 | // Add some additional logging to localhost, pointing developers to the 45 | // service worker/PWA documentation. 46 | navigator.serviceWorker.ready.then(() => { 47 | console.log( 48 | "This web app is being served cache-first by a service " + 49 | "worker. To learn more, visit https://bit.ly/CRA-PWA", 50 | ); 51 | }); 52 | } else { 53 | // Is not localhost. Just register service worker 54 | registerValidSW(swUrl, config); 55 | } 56 | }); 57 | } 58 | } 59 | 60 | function registerValidSW(swUrl: string, config?: IConfig) { 61 | navigator.serviceWorker 62 | .register(swUrl) 63 | .then(registration => { 64 | registration.onupdatefound = () => { 65 | const installingWorker = registration.installing; 66 | if (installingWorker == null) { 67 | return; 68 | } 69 | installingWorker.onstatechange = () => { 70 | if (installingWorker.state === "installed") { 71 | if (navigator.serviceWorker.controller) { 72 | // At this point, the updated precached content has been fetched, 73 | // but the previous service worker will still serve the older 74 | // content until all client tabs are closed. 75 | console.log( 76 | "New content is available and will be used when all " + 77 | "tabs for this page are closed. See https://bit.ly/CRA-PWA.", 78 | ); 79 | 80 | // Execute callback 81 | if (config && config.onUpdate) { 82 | config.onUpdate(registration); 83 | } 84 | } else { 85 | // At this point, everything has been precached. 86 | // It's the perfect time to display a 87 | // "Content is cached for offline use." message. 88 | console.log("Content is cached for offline use."); 89 | 90 | // Execute callback 91 | if (config && config.onSuccess) { 92 | config.onSuccess(registration); 93 | } 94 | } 95 | } 96 | }; 97 | }; 98 | }) 99 | .catch(error => { 100 | console.error("Error during service worker registration:", error); 101 | }); 102 | } 103 | 104 | function checkValidServiceWorker(swUrl: string, config?: IConfig) { 105 | // Check if the service worker can be found. If it can't reload the page. 106 | fetch(swUrl) 107 | .then(response => { 108 | // Ensure service worker exists, and that we really are getting a JS file. 109 | const contentType = response.headers.get("content-type"); 110 | if (response.status === 404 || (contentType != null && contentType.indexOf("javascript") === -1)) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log("No internet connection found. App is running in offline mode."); 124 | }); 125 | } 126 | 127 | export function unregister() { 128 | if ("serviceWorker" in navigator) { 129 | navigator.serviceWorker.ready.then(registration => { 130 | registration.unregister(); 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/react/cleanup-after-each"; 2 | -------------------------------------------------------------------------------- /src/state/index.app.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import { AppAction, appInitialState, AppState } from "./reducer.app"; 3 | 4 | // Create contexts 5 | export const AppStateContext = createContext(appInitialState as AppState); 6 | export const AppDispatchContext = createContext((() => 0) as React.Dispatch); 7 | 8 | // Use contexts 9 | export const useAppDispatch = () => useContext(AppDispatchContext); 10 | export const useAppState = () => { 11 | const state: AppState = useContext(AppStateContext); 12 | 13 | return state; 14 | }; 15 | -------------------------------------------------------------------------------- /src/state/reducer.app.ts: -------------------------------------------------------------------------------- 1 | import * as R from "ramda"; 2 | import { IFame } from "helpers/endpoints"; 3 | 4 | type AppActionType = "LOGGED_IN" | "ADD_FAMES" | "RESET"; 5 | export interface AppAction { 6 | type: AppActionType; 7 | payload?: any; 8 | } 9 | 10 | export interface AppState { 11 | fames: IFame[]; 12 | isLoggedIn: boolean; 13 | } 14 | export const appInitialState = { 15 | isLoggedIn: false, 16 | fames: [], 17 | }; 18 | export const appReducer = (state: AppState, action: AppAction): AppState => { 19 | const updateState = R.merge(state); 20 | switch (action.type) { 21 | case "LOGGED_IN": 22 | return updateState({ 23 | isLoggedIn: true, 24 | }); 25 | case "ADD_FAMES": 26 | return updateState({ 27 | fames: action.payload.fames, 28 | }); 29 | case "RESET": 30 | return updateState({ 31 | fames: [], 32 | isLoggedIn: false, 33 | }); 34 | default: 35 | return state; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/styles/_text-colors.scss: -------------------------------------------------------------------------------- 1 | .grad-blue-purple { 2 | display: inline-block; 3 | 4 | background: -webkit-linear-gradient(left, #3f9dec, #a637ca); 5 | background: linear-gradient(to right, #3f9dec, #a637ca); 6 | -webkit-background-clip: text; 7 | 8 | -webkit-text-fill-color: transparent; 9 | } 10 | 11 | .grad-purple-pink { 12 | display: inline-block; 13 | 14 | background: -webkit-linear-gradient(left, #6758c5, #e072c3); 15 | background: linear-gradient(to right, #6758c5, #e072c3); 16 | -webkit-background-clip: text; 17 | 18 | -webkit-text-fill-color: transparent; 19 | } 20 | 21 | .grad-pink-purple { 22 | display: inline-block; 23 | 24 | background: -webkit-linear-gradient(right, #6758c5, #e072c3); 25 | background: linear-gradient(to left, #6758c5, #e072c3); 26 | -webkit-background-clip: text; 27 | 28 | -webkit-text-fill-color: transparent; 29 | } 30 | 31 | .grad-peach-purple { 32 | display: inline-block; 33 | 34 | background: -webkit-linear-gradient(left, #f98ea6, #9966d7); 35 | background: linear-gradient(to right, #f98ea6, #9966d7); 36 | -webkit-background-clip: text; 37 | 38 | -webkit-text-fill-color: transparent; 39 | } 40 | 41 | .grad-blue-indigo, 42 | .ant-divider-inner-text { 43 | display: inline-block; 44 | 45 | background: -webkit-linear-gradient(left, #5fb4e0, #4e4ecf); 46 | background: linear-gradient(to right, #5fb4e0, #4e4ecf); 47 | -webkit-background-clip: text; 48 | 49 | -webkit-text-fill-color: transparent; 50 | } 51 | 52 | .grad-indigo { 53 | display: inline-block; 54 | 55 | background: -webkit-linear-gradient(left, #7b7eda, #6c71c0); 56 | background: -webkit-linear-gradient(left, #6c71c0, #7b7eda); 57 | background: linear-gradient(to right, #6c71c0, #7b7eda); 58 | -webkit-background-clip: text; 59 | 60 | -webkit-text-fill-color: transparent; 61 | } 62 | 63 | .grad-teal-blue { 64 | display: inline-block; 65 | 66 | background: -webkit-linear-gradient(left, #00b5c0, #0091d9); 67 | background: linear-gradient(to right, #00b5c0, #0091d9); 68 | -webkit-background-clip: text; 69 | 70 | -webkit-text-fill-color: transparent; 71 | } 72 | 73 | .grad-orange-pink { 74 | display: inline-block; 75 | 76 | background: -webkit-linear-gradient(left, #fe9ba0, #e072c3); 77 | background: linear-gradient(to right, #fe9ba0, #e072c3); 78 | -webkit-background-clip: text; 79 | 80 | -webkit-text-fill-color: transparent; 81 | } 82 | -------------------------------------------------------------------------------- /src/styles/_vars.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // Green family 3 | --green: #377459; 4 | --green-light: #35a56e; 5 | // Blue family 6 | --blue: #293f84; 7 | --blue-dark: #132852; 8 | --blue-light: #2e73ae; 9 | --info: #17a2b8; 10 | // Gray family 11 | --gray: #6d6d70; 12 | --gray-light: #e9e9f0; 13 | // Font family 14 | --font-family: "Fira Sans", "Sans Serif", monospace; 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.css"; 2 | @import "vars"; 3 | @import "text-colors"; 4 | @import url("https://fonts.googleapis.com/css?family=Fira+Sans:100,200,300,400,500,600,700&subset=cyrillic"); 5 | 6 | ul { 7 | padding-right: 0; 8 | } 9 | .container { 10 | margin-right: auto; 11 | margin-left: auto; 12 | padding-left: 20px; 13 | padding-right: 20px; 14 | 15 | @media (min-width: 768px) { 16 | width: 750px; 17 | padding-left: 25px; 18 | padding-right: 25px; 19 | } 20 | 21 | @media (min-width: 992px) { 22 | width: 970px; 23 | } 24 | 25 | @media (min-width: 1200px) { 26 | width: 1100px; 27 | } 28 | } 29 | .snakbarify { 30 | font-family: var(--font-family); 31 | cursor: default; 32 | user-select: none; 33 | 34 | @media screen and (min-width: 640px) { 35 | border-radius: 30px; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: var(--font-family); 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | cursor: default; 7 | background-color: #f9f9f9; 8 | user-select: none; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "outDir": "build/", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "sourceMap": true, 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": false, 17 | "preserveConstEnums": false, 18 | "noEmit": true, 19 | "jsx": "react", 20 | "experimentalDecorators": true, 21 | "importHelpers": true, 22 | "noEmitHelpers": true, 23 | "noImplicitReturns": true, 24 | "noUnusedLocals": true 25 | }, 26 | "extends": "./jsconfig.path.json" 27 | } 28 | --------------------------------------------------------------------------------