initial commit
This commit is contained in:
38
.babelrc.js
Normal file
38
.babelrc.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const Environment = {
|
||||
development: "development",
|
||||
production: "production",
|
||||
};
|
||||
|
||||
module.exports = (api) => {
|
||||
const presets = [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: "usage",
|
||||
debug: false,
|
||||
corejs: 3,
|
||||
},
|
||||
],
|
||||
"@babel/preset-react",
|
||||
];
|
||||
const plugins = [
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
];
|
||||
|
||||
const environment = api.cache.using(() => {
|
||||
const env = process.env.NODE_ENV ?? Environment.production;
|
||||
return env;
|
||||
});
|
||||
console.log(`Current environment: '${environment}'`);
|
||||
switch (environment) {
|
||||
case Environment.development:
|
||||
plugins.push("react-refresh/babel");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
presets,
|
||||
plugins,
|
||||
};
|
||||
};
|
||||
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 4 versions
|
||||
not ie < 9
|
||||
not dead
|
||||
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
2
.env.development
Normal file
2
.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
API_PATH=http://localhost:5000
|
||||
PORT=5001
|
||||
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
API_PATH=http://localhost:5000
|
||||
PORT=5001
|
||||
1
.env.nonprod
Normal file
1
.env.nonprod
Normal file
@@ -0,0 +1 @@
|
||||
API_PATH=
|
||||
1
.env.prelive
Normal file
1
.env.prelive
Normal file
@@ -0,0 +1 @@
|
||||
API_PATH=
|
||||
1
.env.production
Normal file
1
.env.production
Normal file
@@ -0,0 +1 @@
|
||||
API_PATH=
|
||||
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
123
.eslintrc.js
Normal file
123
.eslintrc.js
Normal file
@@ -0,0 +1,123 @@
|
||||
module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: [
|
||||
"@typescript-eslint",
|
||||
"react",
|
||||
"react-hooks",
|
||||
"jsx-a11y",
|
||||
"prettier",
|
||||
"import",
|
||||
"simple-import-sort",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
jest: true,
|
||||
},
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"airbnb-typescript",
|
||||
"plugin:import/typescript",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaVersion: 2021,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"react/jsx-filename-extension": ["warn", { extensions: [".tsx"] }],
|
||||
"react/prop-types": 0,
|
||||
indent: 0,
|
||||
"@typescript-eslint/indent": 0, // prettier is used instead
|
||||
quotes: 0,
|
||||
"@typescript-eslint/quotes": ["error", "double"],
|
||||
"import/no-unresolved": "error",
|
||||
"react/jsx-props-no-spreading": 0,
|
||||
"react/require-default-props": 0,
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
selector: ["variable", "function"],
|
||||
format: ["camelCase", "PascalCase", "UPPER_CASE"],
|
||||
leadingUnderscore: "allowSingleOrDouble",
|
||||
},
|
||||
{
|
||||
selector: "variable",
|
||||
types: ["boolean"],
|
||||
format: ["camelCase", "StrictPascalCase"],
|
||||
prefix: ["is", "should", "has", "can", "did", "will"],
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
vars: "all",
|
||||
args: "after-used",
|
||||
argsIgnorePattern: "^_.*$",
|
||||
varsIgnorePattern: "^_.*$",
|
||||
},
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
patterns: [
|
||||
"@material-ui/*/*/*",
|
||||
"!@material-ui/core/test-utils/*",
|
||||
"!@material-ui/pickers/constants/*",
|
||||
],
|
||||
},
|
||||
],
|
||||
"react/prefer-stateless-function": 2,
|
||||
"import/prefer-default-export": 0,
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
devDependencies: ["**/*.test.js", "**/*.spec.js", "./webpack/**/*"],
|
||||
},
|
||||
],
|
||||
// slow rules that aren't relevant
|
||||
"@typescript-eslint/no-implied-eval": 0,
|
||||
"react/jsx-no-bind": 0,
|
||||
"import/order": 0,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
},
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"],
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.tsx"],
|
||||
rules: {
|
||||
"simple-import-sort/imports": [
|
||||
"error",
|
||||
{
|
||||
groups: [
|
||||
["^react", "^@?\\w"],
|
||||
["^(@|@company|@ui|components|utils|config|vendored-lib)(/.*|$)"],
|
||||
["^\\.\\.(?!/?$)", "^\\.\\./?$"],
|
||||
["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
|
||||
["^.+\\.s?css$"],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
dist
|
||||
|
||||
node_modules
|
||||
|
||||
*.log
|
||||
.swp
|
||||
|
||||
.env
|
||||
6
.postcssrc.js
Normal file
6
.postcssrc.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
"postcss-preset-env",
|
||||
"autoprefixer",
|
||||
],
|
||||
};
|
||||
36
.prettierrc.js
Normal file
36
.prettierrc.js
Normal file
@@ -0,0 +1,36 @@
|
||||
module.exports = {
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
singleQuote: false,
|
||||
trailingComma: "all",
|
||||
semi: true,
|
||||
arrowParens: "always",
|
||||
endOfLine: "lf",
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.ts"],
|
||||
options: {
|
||||
parser: "typescript",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.tsx"],
|
||||
options: {
|
||||
parser: "typescript",
|
||||
jsxSingleQuote: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["*.scss"],
|
||||
options: {
|
||||
parser: "scss",
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["*.json"],
|
||||
options: {
|
||||
parser: "json",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
21
deploy/demo/Dockerfile
Normal file
21
deploy/demo/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
### STAGE 1: Build ###
|
||||
|
||||
# We label our stage as ‘builder’
|
||||
FROM node:16 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn install
|
||||
RUN yarn build:nonprod
|
||||
|
||||
# runner container
|
||||
# - nginx, to serve static built React app
|
||||
|
||||
# nginx state for serving content
|
||||
FROM nginx:stable-alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY deploy/nonprod/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
17
deploy/demo/nginx/nginx.conf
Normal file
17
deploy/demo/nginx/nginx.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
server {
|
||||
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
21
deploy/prelive/Dockerfile
Normal file
21
deploy/prelive/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
### STAGE 1: Build ###
|
||||
|
||||
# We label our stage as ‘builder’
|
||||
FROM node:16 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn install
|
||||
RUN yarn build:prelive
|
||||
|
||||
# runner container
|
||||
# - nginx, to serve static built React app
|
||||
|
||||
# nginx state for serving content
|
||||
FROM nginx:stable-alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY deploy/prelive/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
17
deploy/prelive/nginx/nginx.conf
Normal file
17
deploy/prelive/nginx/nginx.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
server {
|
||||
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
21
deploy/production/Dockerfile
Normal file
21
deploy/production/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
### STAGE 1: Build ###
|
||||
|
||||
# We label our stage as ‘builder’
|
||||
FROM node:16 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn install
|
||||
RUN yarn build:live
|
||||
|
||||
# runner container
|
||||
# - nginx, to serve static built React app
|
||||
|
||||
# nginx state for serving content
|
||||
FROM nginx:stable-alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY deploy/production/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
17
deploy/production/nginx/nginx.conf
Normal file
17
deploy/production/nginx/nginx.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
server {
|
||||
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
107
package.json
Executable file
107
package.json
Executable file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"name": "react-template-esbuild",
|
||||
"version": "1.0.0",
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"main": "webpack.config.babel.js",
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"yarn": "^1.22.5"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "yarn clean",
|
||||
"bundle": "webpack --config webpack.config.babel.js",
|
||||
"build": "cross-env NODE_ENV=production yarn bundle",
|
||||
"build:nonprod": "cross-env ENV_CONFIGURATION=nonprod yarn build",
|
||||
"build:prelive": "cross-env ENV_CONFIGURATION=prelive yarn build",
|
||||
"build:live": "cross-env ENV_CONFIGURATION=production yarn build",
|
||||
"start": "cross-env WEBPACK_IS_DEV_SERVER=true NODE_ENV=development webpack serve --config webpack.config.babel.js",
|
||||
"clean": "rimraf dist",
|
||||
"lint": "eslint src",
|
||||
"profile": "cross-env NODE_ENV=production webpack --profile --json --config webpack.config.babel.js > ./dist/profile.json",
|
||||
"perf:size": "yarn profile && webpack-bundle-analyzer ./dist/profile.json",
|
||||
"perf:lint": "cross-env TIMING=1 yarn lint",
|
||||
"perf:build": "cross-env MEASURE=1 yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@date-io/date-fns": "1.3.13",
|
||||
"@hookform/resolvers": "2.8.3",
|
||||
"@types/i18n-js": "3.8.2",
|
||||
"@types/js-cookie": "3.0.0",
|
||||
"@types/react": "17.0.34",
|
||||
"@types/react-dom": "17.0.11",
|
||||
"@types/react-router-dom": "5.3.2",
|
||||
"@types/sanitize-html": "2.5.0",
|
||||
"classnames": "2.3.1",
|
||||
"date-fns": "2.25.0",
|
||||
"history": "5.1.0",
|
||||
"html-react-parser": "1.4.0",
|
||||
"i18n-js": "3.8.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-error-boundary": "3.1.4",
|
||||
"react-hook-form": "7.18.0",
|
||||
"react-hot-loader": "4.13.0",
|
||||
"react-loader-spinner": "5.1.4",
|
||||
"react-router-dom": "next",
|
||||
"react-skeleton-css": "1.1.0",
|
||||
"sanitize-html": "2.5.3",
|
||||
"swr": "1.0.1",
|
||||
"use-async-effect": "2.2.3",
|
||||
"yup": "0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.16.0",
|
||||
"@babel/eslint-parser": "7.16.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.16.0",
|
||||
"@babel/preset-react": "7.16.0",
|
||||
"@babel/register": "7.16.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.2.0",
|
||||
"@typescript-eslint/parser": "5.2.0",
|
||||
"autoprefixer": "10.4.0",
|
||||
"babel-plugin-import": "1.13.3",
|
||||
"copy-webpack-plugin": "9.0.1",
|
||||
"core-js": "3.19.1",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "6.5.1",
|
||||
"dotenv-webpack": "7.0.3",
|
||||
"esbuild-loader": "2.16.0",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-airbnb-base": "14.2.1",
|
||||
"eslint-config-airbnb-typescript": "14.0.1",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-import-resolver-alias": "1.1.2",
|
||||
"eslint-import-resolver-typescript": "2.5.0",
|
||||
"eslint-plugin-import": "2.25.3",
|
||||
"eslint-plugin-jsx-a11y": "6.5.1",
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"eslint-plugin-react": "7.27.0",
|
||||
"eslint-plugin-react-hooks": "4.3.0",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"eslint-webpack-plugin": "3.1.0",
|
||||
"fast-sass-loader": "2.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "6.4.0",
|
||||
"html-loader": "3.0.1",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"mini-css-extract-plugin": "2.4.4",
|
||||
"path": "0.12.7",
|
||||
"postcss": "8.3.11",
|
||||
"postcss-loader": "6.2.0",
|
||||
"postcss-preset-env": "6.7.0",
|
||||
"prettier": "2.4.1",
|
||||
"react-refresh": "0.11.0",
|
||||
"rimraf": "3.0.2",
|
||||
"sass": "1.43.4",
|
||||
"speed-measure-webpack-plugin": "1.5.0",
|
||||
"style-loader": "3.3.1",
|
||||
"tsconfig-paths-webpack-plugin": "3.5.1",
|
||||
"typescript": "4.4.4",
|
||||
"webpack": "5.63.0",
|
||||
"webpack-bundle-analyzer": "4.5.0",
|
||||
"webpack-cli": "4.9.1",
|
||||
"webpack-dev-server": "4.4.0",
|
||||
"webpack-merge": "5.8.0"
|
||||
}
|
||||
}
|
||||
4
src/@types/declarations.d.ts
vendored
Normal file
4
src/@types/declarations.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.scss" {
|
||||
const content: { [className: string]: string };
|
||||
export = content;
|
||||
}
|
||||
14
src/api/types.ts
Normal file
14
src/api/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export enum ContentType {
|
||||
APPLICATION_JSON = "application/json",
|
||||
}
|
||||
|
||||
export enum RequestMethod {
|
||||
GET = "GET",
|
||||
POST = "POST",
|
||||
PUT = "PUT",
|
||||
DELETE = "DELETE",
|
||||
}
|
||||
|
||||
export enum RequestHeader {
|
||||
CONTENT_TYPE = "Content-Type",
|
||||
}
|
||||
40
src/api/util.ts
Normal file
40
src/api/util.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ContentType, RequestHeader, RequestMethod } from "./types";
|
||||
|
||||
const getAuthenticationHeaders = () => ({});
|
||||
|
||||
const apiPath = process.env.API_PATH;
|
||||
const getApiPath = (api: string): string => `${apiPath}/lg/${api}`;
|
||||
|
||||
const swrFetcher = (path: string, init?: RequestInit): Promise<Response> =>
|
||||
fetch(getApiPath(path), {
|
||||
...init,
|
||||
headers: {
|
||||
[RequestHeader.CONTENT_TYPE]: ContentType.APPLICATION_JSON,
|
||||
...getAuthenticationHeaders(),
|
||||
},
|
||||
}).then(async (res) => {
|
||||
let text = await res.json();
|
||||
if (typeof text === "string") {
|
||||
// eslint-disable-next-line @typescript-eslint/quotes
|
||||
text = text.replace(`b\"`, "").replace(`\\n\"`, "");
|
||||
}
|
||||
return text;
|
||||
});
|
||||
|
||||
const post = (path: string): Promise<Response> =>
|
||||
fetch(getApiPath(path), {
|
||||
method: RequestMethod.POST,
|
||||
headers: {
|
||||
[RequestHeader.CONTENT_TYPE]: ContentType.APPLICATION_JSON,
|
||||
...getAuthenticationHeaders(),
|
||||
},
|
||||
}).then(async (res) => {
|
||||
let text = await res.json();
|
||||
if (typeof text === "string") {
|
||||
// eslint-disable-next-line @typescript-eslint/quotes
|
||||
text = text.replace(`b\"`, "").replace(`\\n\"`, "");
|
||||
}
|
||||
return text;
|
||||
});
|
||||
|
||||
export { post, swrFetcher };
|
||||
BIN
src/assets/favicon.ico
Normal file
BIN
src/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
0
src/assets/images/.gitkeep
Normal file
0
src/assets/images/.gitkeep
Normal file
0
src/assets/svg/.gitkeep
Normal file
0
src/assets/svg/.gitkeep
Normal file
34
src/common/ComponentErrorBoundary.tsx
Normal file
34
src/common/ComponentErrorBoundary.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const ErrorFallback: React.FC<FallbackProps> = ({
|
||||
error,
|
||||
resetErrorBoundary,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
|
||||
const [initialLocation] = useState(location.pathname);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== initialLocation || !error) {
|
||||
resetErrorBoundary();
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error occurred", error);
|
||||
}
|
||||
}, [error, location.pathname]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong!</h2>
|
||||
<h4>Please try again later.</h4>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ComponentErrorBoundary: React.FC = ({ children }) => (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>{children}</ErrorBoundary>
|
||||
);
|
||||
|
||||
export default ComponentErrorBoundary;
|
||||
23
src/components/App.tsx
Normal file
23
src/components/App.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { SWRConfig } from "swr";
|
||||
|
||||
import { swrFetcher } from "@/api/util";
|
||||
|
||||
import AppRouter from "./AppRouter";
|
||||
|
||||
const App: React.FC = () => (
|
||||
<BrowserRouter>
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: swrFetcher,
|
||||
suspense: true,
|
||||
dedupingInterval: 0,
|
||||
}}
|
||||
>
|
||||
<AppRouter />
|
||||
</SWRConfig>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
export default App;
|
||||
34
src/components/AppRouter.tsx
Normal file
34
src/components/AppRouter.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
|
||||
import LayoutWrapper from "@/components/LayoutWrapper";
|
||||
import routes from "@/constants/routes";
|
||||
|
||||
import PrivateRoute from "./PrivateRoute";
|
||||
|
||||
const AppRouter: React.FC = () => (
|
||||
<Routes>
|
||||
{routes.public.map((route) => (
|
||||
<Route key={route.path} path={route.path} element={<route.Component />} />
|
||||
))}
|
||||
<Route path="/" element={<LayoutWrapper />}>
|
||||
{routes.private.map(({ path, Component, subroutes, redirect }) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<PrivateRoute Component={Component} redirect={redirect} />}
|
||||
>
|
||||
{subroutes?.map((subroute) => (
|
||||
<Route
|
||||
key={subroute.path}
|
||||
path={subroute.path}
|
||||
element={<PrivateRoute Component={subroute.Component} />}
|
||||
/>
|
||||
)) || null}
|
||||
</Route>
|
||||
))}
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export default AppRouter;
|
||||
15
src/components/LayoutWrapper.tsx
Normal file
15
src/components/LayoutWrapper.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { useOutlet } from "react-router-dom";
|
||||
|
||||
import ComponentErrorBoundary from "@/common/ComponentErrorBoundary";
|
||||
|
||||
const LayoutWrapper: React.FC = () => {
|
||||
const outlet = useOutlet();
|
||||
return (
|
||||
<div className="container">
|
||||
<ComponentErrorBoundary>{outlet}</ComponentErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutWrapper;
|
||||
25
src/components/PrivateRoute.tsx
Normal file
25
src/components/PrivateRoute.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
const PrivateRoute: React.FC<{
|
||||
Component: React.FC;
|
||||
redirect?: string;
|
||||
}> = ({ Component, redirect }) => {
|
||||
const isAuthenticated = true;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
// navigate("/login");
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
if (isAuthenticated) {
|
||||
if (redirect) {
|
||||
return <Navigate to={redirect} />;
|
||||
}
|
||||
return <Component />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
5
src/constants/navigation.ts
Normal file
5
src/constants/navigation.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const NavigationPath = {
|
||||
Home: "/",
|
||||
};
|
||||
|
||||
export default NavigationPath;
|
||||
29
src/constants/routes.ts
Normal file
29
src/constants/routes.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
|
||||
import Homepage from "@/pages/home/Homepage";
|
||||
|
||||
import NavigationPath from "./navigation";
|
||||
|
||||
interface NavigationRoute {
|
||||
path: string;
|
||||
Component: React.FC;
|
||||
subroutes?: NavigationRoute[];
|
||||
redirect?: string;
|
||||
}
|
||||
|
||||
interface NavigationRoutes {
|
||||
public: NavigationRoute[];
|
||||
private: NavigationRoute[];
|
||||
}
|
||||
|
||||
const routes: NavigationRoutes = {
|
||||
public: [
|
||||
{
|
||||
path: NavigationPath.Home,
|
||||
Component: Homepage,
|
||||
},
|
||||
],
|
||||
private: [],
|
||||
};
|
||||
|
||||
export default routes;
|
||||
14
src/index.html
Normal file
14
src/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="theme-color" content="#FFF">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="version" content="1.0">
|
||||
<title>REPLACE_TITLE</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="./assets/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" />
|
||||
</body>
|
||||
</html>
|
||||
10
src/index.tsx
Normal file
10
src/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import App from "./components/App";
|
||||
|
||||
import "react-skeleton-css/styles/skeleton.2.0.4.css";
|
||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||
import "./styles/styles.scss";
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
||||
1
src/locale/en.json
Normal file
1
src/locale/en.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
src/locale/et.json
Normal file
1
src/locale/et.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
203
src/pages/home/Homepage.tsx
Normal file
203
src/pages/home/Homepage.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { Suspense } from "react";
|
||||
import classNames from "classnames";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { post } from "@/api/util";
|
||||
|
||||
import LoadingPage from "./LoadingPage";
|
||||
import {
|
||||
IAudioStatus,
|
||||
IConfigs,
|
||||
ISoftwareInfo,
|
||||
ISystemInfo,
|
||||
ITVState,
|
||||
} from "./webos-types";
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
const Wrapper: React.FC = ({ children }) => (
|
||||
<div className="container">{children}</div>
|
||||
);
|
||||
|
||||
const toTitleCase = (value?: string) =>
|
||||
value
|
||||
?.toLowerCase()
|
||||
.replaceAll("_", " ")
|
||||
.replace(/[^-’\s]+/g, (match) =>
|
||||
match.replace(/^./, (first) => first.toUpperCase()),
|
||||
) || "";
|
||||
|
||||
const turnScreenOn = async () => {
|
||||
await post("screen-on");
|
||||
};
|
||||
const turnScreenOff = async () => {
|
||||
await post("screen-off");
|
||||
};
|
||||
|
||||
const mute = async () => {
|
||||
await post("mute");
|
||||
};
|
||||
const unmute = async () => {
|
||||
await post("unmute");
|
||||
};
|
||||
|
||||
const getFormattedJson = function <T extends object | string>(
|
||||
badPythonJson?: string,
|
||||
): T | null {
|
||||
if (!badPythonJson) {
|
||||
return null;
|
||||
}
|
||||
const fixed = badPythonJson
|
||||
// eslint-disable-next-line @typescript-eslint/quotes
|
||||
// .replaceAll("'", '"')
|
||||
// .replaceAll("\n", "")
|
||||
.replaceAll(" True", " true")
|
||||
.replaceAll(" False", " false");
|
||||
console.log("JSON parse values", {
|
||||
fixed,
|
||||
badPythonJson,
|
||||
});
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { returnValue, subscribed, ...result } = JSON.parse(fixed);
|
||||
return Object.keys(result).length === 1
|
||||
? result[Object.keys(result)[0]]
|
||||
: result;
|
||||
} catch (e) {
|
||||
console.error("Bad response", e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getAudioScenario = (scenario?: string) => {
|
||||
switch (scenario) {
|
||||
case "mastervolume_ext_speaker_bt":
|
||||
return "Bluetooth";
|
||||
case "mastervolume_tv_speaker":
|
||||
return "Internal TV speaker";
|
||||
case "mastervolume_headphone":
|
||||
return "Wired headphones";
|
||||
default:
|
||||
return scenario;
|
||||
}
|
||||
};
|
||||
|
||||
const MAX_TABLES_NESTING = 3;
|
||||
|
||||
const ObjectTable: React.FC<{ values?: object | null; nesting?: number }> = ({
|
||||
values,
|
||||
nesting = 0,
|
||||
}) => {
|
||||
if (!values || nesting > MAX_TABLES_NESTING) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="small-table-wrapper">
|
||||
<table className={classNames(["u-full-width", "small-table"])}>
|
||||
<tbody>
|
||||
{Object.entries(values).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td>{key.includes(".") ? key : toTitleCase(key)}</td>
|
||||
<td>
|
||||
{typeof value === "object" ? (
|
||||
<ObjectTable values={value} nesting={nesting + 1} />
|
||||
) : (
|
||||
String(value)
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HomepageContent: React.FC = () => {
|
||||
const { data: tvState } = useSWR<ITVState>("get-state");
|
||||
const { data: audioStatus } = useSWR<IAudioStatus>("get-audio-status");
|
||||
const { data: softwareInfo } = useSWR<ISoftwareInfo>("get-software-info");
|
||||
const { data: systemInfo } = useSWR<ISystemInfo>("get-system-info");
|
||||
const { data: configs } = useSWR<IConfigs>("get-configs");
|
||||
const { data: apps } = useSWR<IConfigs>("get-apps");
|
||||
|
||||
return (
|
||||
<div className={classNames(["container", "homepage-content"])}>
|
||||
<div className="row">
|
||||
<div className={classNames(["row", "buttons-row"])}>
|
||||
<button className="button-primary" onClick={turnScreenOn}>
|
||||
TV screen on
|
||||
</button>
|
||||
<button className="button-primary" onClick={turnScreenOff}>
|
||||
TV screen off
|
||||
</button>
|
||||
</div>
|
||||
<div className={classNames(["row", "buttons-row"])}>
|
||||
{(!audioStatus || !audioStatus.mute) && (
|
||||
<button className="button-primary" onClick={mute}>
|
||||
Mute
|
||||
</button>
|
||||
)}
|
||||
{(!audioStatus || audioStatus.mute) && (
|
||||
<button className="button-primary" onClick={unmute}>
|
||||
Unmute
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<table className={classNames(["u-full-width", "properties-table"])}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>TV state</td>
|
||||
<td>{tvState?.state}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Audio status</td>
|
||||
<td>
|
||||
{
|
||||
<ObjectTable
|
||||
values={{
|
||||
...audioStatus,
|
||||
scenario: getAudioScenario(audioStatus?.scenario),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Software info</td>
|
||||
<td>{<ObjectTable values={softwareInfo} />}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>System info</td>
|
||||
<td>{<ObjectTable values={systemInfo} />}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Configs</td>
|
||||
<td>{<ObjectTable values={configs} />}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Apps</td>
|
||||
<td>{<ObjectTable values={apps} />}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Homepage: React.FC = () => (
|
||||
<Suspense fallback={<LoadingPage />}>
|
||||
<HomepageContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
export default Homepage;
|
||||
18
src/pages/home/LoadingPage.tsx
Normal file
18
src/pages/home/LoadingPage.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { Triangle } from "react-loader-spinner";
|
||||
|
||||
const LoadingPage: React.FC = () => (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="eight columns">
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "center", marginTop: 24 }}
|
||||
>
|
||||
<Triangle height="100" width="100" color="grey" ariaLabel="loading" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default LoadingPage;
|
||||
34
src/pages/home/styles.css
Normal file
34
src/pages/home/styles.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.properties-table td:first-child {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.buttons-row {
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.homepage-content {
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.container {
|
||||
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
|
||||
}
|
||||
.container > .row {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
table thead td {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.small-table tbody td:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
.small-table-wrapper {
|
||||
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
|
||||
padding: 16px;
|
||||
}
|
||||
170
src/pages/home/webos-types.ts
Normal file
170
src/pages/home/webos-types.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
interface Configs {
|
||||
"tv.model.supportMovingSpeaker": boolean;
|
||||
"tv.model.tunerSleepMode": boolean;
|
||||
"tv.model.supportMotionDetectSensor": boolean;
|
||||
"tv.model.micomType": string;
|
||||
"tv.model.bplCurrentType": string;
|
||||
"tv.model.twinTV": boolean;
|
||||
"tv.model.audioIndex": number;
|
||||
"tv.model.supportNewPowerSave": boolean;
|
||||
"tv.model.supportAutoStoreMode": boolean;
|
||||
"tv.model.supportMhl": boolean;
|
||||
"tv.model.motionProMode": string;
|
||||
"tv.model.adaptDimDefault": string;
|
||||
"tv.model.supportTvLink": boolean;
|
||||
"tv.model.supportC2": boolean;
|
||||
"tv.model.supportAppStore": boolean;
|
||||
"tv.model.hwSettingGroup": string;
|
||||
"tv.model.analogDemodTypeSub": string;
|
||||
"tv.model.supportNetflix": boolean;
|
||||
"tv.model.supportIsf": boolean;
|
||||
"tv.model.supportZram": boolean;
|
||||
"tv.model.supportWarmStandbySoc": boolean;
|
||||
"tv.model.displayResolution": number;
|
||||
"tv.model.supportScreenRollerMotor": boolean;
|
||||
"tv.model.supportDvrReady": boolean;
|
||||
"tv.model.maxInputNumberTv": number;
|
||||
"tv.model.transitionOpt": string;
|
||||
"tv.model.panelLedBarType": string;
|
||||
"tv.model.motionRemoconType": string;
|
||||
"tv.model.supportMarlinAp": boolean;
|
||||
"tv.model.panelGamutType": string;
|
||||
"tv.model.moduleBackLightType": string;
|
||||
"tv.model.supportEsnAp": boolean;
|
||||
"tv.model.supportPip": boolean;
|
||||
"tv.model.analogDemodType": string;
|
||||
"tv.model.sPqlConfig_ePipType_NOTMAPPED": number;
|
||||
"tv.model.cameraType": string;
|
||||
"tv.model.digitalDemod": string;
|
||||
"tv.model.soundModeType": string;
|
||||
"tv.model.eventBoardType": string;
|
||||
"tv.model.displayLvdsType": string;
|
||||
"tv.model.videoIndex": number;
|
||||
"tv.model.zramSize": number;
|
||||
"tv.model.pwm2Duty": string;
|
||||
"tv.model.supportEmmcRecord": boolean;
|
||||
"tv.model.supportLocalDimming": boolean;
|
||||
"tv.model.audioPwVolt": string;
|
||||
"tv.model.whiteBalanceNpoint": number;
|
||||
"tv.model.supportDolbyVisionHDR": boolean;
|
||||
"tv.model.supportLocalDimmingOsdMenu": boolean;
|
||||
"tv.model.tconPmicType": string;
|
||||
"tv.model.supportEpa": boolean;
|
||||
"tv.model.supportWol": boolean;
|
||||
"tv.model.adjustVCOM": boolean;
|
||||
"tv.model.supportDimming": boolean;
|
||||
"tv.model.supportWidevineAp": boolean;
|
||||
"tv.model.supportMirrorMode": boolean;
|
||||
"tv.model.supportGameMode": boolean;
|
||||
"tv.model.supportOledTconOrbit": boolean;
|
||||
"tv.model.edidType": string;
|
||||
"tv.model.sPqlConfig_eDisplayModule_NOTMAPPED": number;
|
||||
"tv.model.pipType": string;
|
||||
"tv.model.magicSpaceSound": boolean;
|
||||
"tv.model.maxInputNumberUsb": number;
|
||||
"tv.model.digitalEye": string;
|
||||
"tv.model.motionEyeCareIndex": string;
|
||||
"tv.model.supportInternalAdcSoc": boolean;
|
||||
"tv.model.toolType": string;
|
||||
"tv.model.modeSelect": string;
|
||||
"tv.model.reserved": boolean;
|
||||
"tv.model.logoLight": boolean;
|
||||
"tv.model.supportThx": boolean;
|
||||
"tv.model.pwmFreqType": string;
|
||||
"tv.model.supportCiEcpAp": boolean;
|
||||
"tv.model.lvdsBits": string;
|
||||
"tv.model.sdpProductionMode": boolean;
|
||||
"tv.model.supportOledJB": boolean;
|
||||
"tv.model.supportHDR": boolean;
|
||||
"tv.model.supportWiSA": boolean;
|
||||
"tv.model.otaId": string;
|
||||
"tv.model.hdmiSwitchType": string;
|
||||
"tv.model.instantBoot": string;
|
||||
"tv.model.usb1": string;
|
||||
"tv.model.usb2": string;
|
||||
"tv.model.wifiType": string;
|
||||
"tv.model.supportAtvAvDvr": boolean;
|
||||
"tv.model.usb3": string;
|
||||
"tv.model.satDigitalDemod": string;
|
||||
"tv.model.scanningBl": boolean;
|
||||
"tv.model.supportMacAp": boolean;
|
||||
"tv.model.audioSpkWatt": string;
|
||||
"tv.model.osdResolution": number;
|
||||
"tv.model.continentIndx": number;
|
||||
"tv.model.supportTemp8K": boolean;
|
||||
"tv.model.maxTvPathNumber": number;
|
||||
"tv.model.maxPwmType": string;
|
||||
"tv.model.supportDtcpKey": boolean;
|
||||
"tv.model.supportHeadPhone": boolean;
|
||||
"tv.model.moduleInchType": string;
|
||||
"tv.model.languageCountrySel": string;
|
||||
"tv.model.support21YLcon": boolean;
|
||||
"tv.model.3dOrderType": string;
|
||||
"tv.model.supportHdmi2ExtEdid": boolean;
|
||||
"tv.model.supportExternalSpeaker": boolean;
|
||||
"tv.model.supportLuminanceUp": boolean;
|
||||
"tv.model.supportDualView": boolean;
|
||||
"tv.model.defaultStdBacklight": number;
|
||||
"tv.model.ampChipType": string;
|
||||
"tv.model.supportAudioLineOut": boolean;
|
||||
"tv.model.pdpModuleId": number;
|
||||
"tv.model.eModeSelect": boolean;
|
||||
"tv.model.supportFanControl": boolean;
|
||||
"tv.model.supportBNO": boolean;
|
||||
"tv.model.localKeyType": string;
|
||||
"tv.model.hardwareVersion": number;
|
||||
"tv.model.supportPsuPowerBoard": boolean;
|
||||
"tv.model.instantBootDefaultValue": boolean;
|
||||
"tv.model.cellType": string;
|
||||
"tv.model.sysType": string;
|
||||
"tv.model.eyeCurveDerivation": string;
|
||||
"tv.model.moduleMakerType": string;
|
||||
"tv.model.digitalDemodSub": string;
|
||||
"tv.model.supportCiAp": boolean;
|
||||
"tv.model.supportCompRcaCommon": boolean;
|
||||
"tv.model.supportOledOffRsQuickStart": boolean;
|
||||
"tv.model.displayType": string;
|
||||
"tv.model.3dSupportType": string;
|
||||
"tv.model.maxInputNumberAutoAv": number;
|
||||
}
|
||||
|
||||
export interface IConfigs {
|
||||
subscribed: boolean;
|
||||
configs: Configs;
|
||||
}
|
||||
|
||||
export interface ISystemInfo {
|
||||
receiverType: string;
|
||||
modelName: string;
|
||||
programMode: string;
|
||||
features: {
|
||||
"3d": boolean;
|
||||
dvr: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISoftwareInfo {
|
||||
product_name: string;
|
||||
model_name: string;
|
||||
sw_type: string;
|
||||
major_ver: string;
|
||||
minor_ver: string;
|
||||
country: string;
|
||||
country_group: string;
|
||||
device_id: string;
|
||||
auth_flag: string;
|
||||
ignore_disable: string;
|
||||
eco_info: string;
|
||||
config_key: string;
|
||||
language_code: string;
|
||||
}
|
||||
|
||||
export interface IAudioStatus {
|
||||
scenario: string;
|
||||
volume: number;
|
||||
mute: boolean;
|
||||
}
|
||||
|
||||
export interface ITVState {
|
||||
state: string;
|
||||
}
|
||||
74
src/styles/styles.scss
Normal file
74
src/styles/styles.scss
Normal file
@@ -0,0 +1,74 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;1,400;1,500&display=swap');
|
||||
|
||||
body {
|
||||
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
background: #FBFCFD;
|
||||
color: #000;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
|
||||
button,
|
||||
input {
|
||||
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
|
||||
button,
|
||||
button > span {
|
||||
letter-spacing: 0.6px;
|
||||
}
|
||||
|
||||
// hides chrome input color
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover textarea:-webkit-autofill:focus,
|
||||
select:-webkit-autofill,
|
||||
select:-webkit-autofill:hover,
|
||||
select:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0px 1000px #ffffff inset !important;
|
||||
}
|
||||
|
||||
body {
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
border-radius: 4px;
|
||||
background: #E6E8EC;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background: #C3C4C6;
|
||||
&:hover {
|
||||
background: #75787A;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/theme/index.ts
Normal file
37
src/theme/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
const theme = {
|
||||
color: {
|
||||
white: "#FFFFFF",
|
||||
},
|
||||
borderRadius: {
|
||||
s: 4,
|
||||
m: 8,
|
||||
l: 16,
|
||||
},
|
||||
font: {
|
||||
size: {
|
||||
xxs: 12,
|
||||
xs: 14,
|
||||
s: 16,
|
||||
m: 18,
|
||||
l: 24,
|
||||
xl: 36,
|
||||
xxl: 48,
|
||||
},
|
||||
lineHeight: {
|
||||
s: 1.2,
|
||||
m: 1.4,
|
||||
l: 1.7,
|
||||
},
|
||||
letterSpacing: {
|
||||
s: "0.15px",
|
||||
m: "0.32px",
|
||||
l: "0.4px",
|
||||
},
|
||||
},
|
||||
sizes: {
|
||||
maxWidthValue: 1440,
|
||||
maxWidth: "@media screen and (min-width: 1440px)",
|
||||
},
|
||||
};
|
||||
|
||||
export default theme;
|
||||
28
src/theme/scrollbar.ts
Normal file
28
src/theme/scrollbar.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
const scrollbarStyles = {
|
||||
"&::-webkit-scrollbar": {
|
||||
width: 8,
|
||||
borderRadius: 4,
|
||||
background: "#E6E8EC",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
borderRadius: 4,
|
||||
background: "#C3C4C6",
|
||||
"&:hover": {
|
||||
background: "#75787A",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const horizontalScrollBar = {
|
||||
"&::-webkit-scrollbar": {
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
background: "#E6E8EC",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: "#C3C4C6",
|
||||
},
|
||||
};
|
||||
|
||||
export default scrollbarStyles;
|
||||
export { horizontalScrollBar };
|
||||
11
tsconfig.eslint.json
Normal file
11
tsconfig.eslint.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"./webpack/**/*",
|
||||
"./.eslintrc.js",
|
||||
"./.babelrc.js",
|
||||
"./.prettierrc.js",
|
||||
"./.postcssrc.js",
|
||||
"./webpack.config.babel.js"
|
||||
]
|
||||
}
|
||||
34
tsconfig.json
Normal file
34
tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"module": "esnext",
|
||||
"target": "es6",
|
||||
"lib": ["esnext", "dom"],
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": "./",
|
||||
"rootDir": "./src",
|
||||
"typeRoots": [
|
||||
"./src/@types",
|
||||
"node_modules/@types"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"importsNotUsedAsValues": "remove",
|
||||
"incremental": false,
|
||||
"skipLibCheck": false,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["**/node_modules"]
|
||||
}
|
||||
8
webpack.config.babel.js
Normal file
8
webpack.config.babel.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import merge from "webpack-merge";
|
||||
|
||||
import baseConfig from "./webpack/base";
|
||||
import config from "./webpack/config";
|
||||
import devConfig from "./webpack/dev";
|
||||
import prodConfig from "./webpack/prod";
|
||||
|
||||
export default merge(baseConfig, config.isProd ? prodConfig : devConfig);
|
||||
46
webpack/base.js
Executable file
46
webpack/base.js
Executable file
@@ -0,0 +1,46 @@
|
||||
import { join } from "path";
|
||||
import SpeedMeasurePlugin from "speed-measure-webpack-plugin";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
|
||||
import config from "./config";
|
||||
import { basePlugins } from "./options/plugins";
|
||||
import rules from "./options/rules";
|
||||
|
||||
const { devServer } = config;
|
||||
console.log("dev server", JSON.stringify(devServer));
|
||||
const baseConfig = {
|
||||
context: __dirname,
|
||||
entry: {
|
||||
main: ["../src/index.tsx"],
|
||||
},
|
||||
output: {
|
||||
path: join(config.webpack.rootDir, "dist"),
|
||||
publicPath: devServer.isDevServer ? devServer.url : "./",
|
||||
filename: devServer.isDevServer
|
||||
? "[name].[fullhash].js"
|
||||
: "[name].[contenthash].js",
|
||||
},
|
||||
module: { rules },
|
||||
plugins: basePlugins,
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js", ".jsx"],
|
||||
plugins: [new TsconfigPathsPlugin({})],
|
||||
},
|
||||
optimization: {
|
||||
runtimeChunk: {
|
||||
name: "runtime",
|
||||
},
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
commons: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: "vendor",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default process.env.MEASURE
|
||||
? new SpeedMeasurePlugin().wrap(baseConfig)
|
||||
: baseConfig;
|
||||
43
webpack/config.js
Normal file
43
webpack/config.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { join } from "path";
|
||||
|
||||
const port = process.env.PORT || 5005;
|
||||
const devServerHost = "127.0.0.1";
|
||||
const devServerUrl = `http://${devServerHost}:${port}/`;
|
||||
|
||||
const Environment = {
|
||||
development: "development",
|
||||
production: "production",
|
||||
};
|
||||
const environment = process.env.NODE_ENV || Environment.production;
|
||||
|
||||
const EnvironmentConfig = {
|
||||
development: "development",
|
||||
nonprod: "nonprod",
|
||||
prelive: "prelive",
|
||||
production: "production",
|
||||
};
|
||||
const environmentConfiguration = process.env.ENV_CONFIGURATION || EnvironmentConfig.development;
|
||||
|
||||
const isProd = environmentConfiguration === EnvironmentConfig.production;
|
||||
const isPrelive = environmentConfiguration === EnvironmentConfig.prelive;
|
||||
const isNonprod = environmentConfiguration === EnvironmentConfig.nonprod;
|
||||
const isDev = environment === Environment.development
|
||||
&& environmentConfiguration === EnvironmentConfig.development;
|
||||
|
||||
export default {
|
||||
environment,
|
||||
environmentConfiguration,
|
||||
isProd,
|
||||
isNonprod,
|
||||
isDev,
|
||||
isPrelive,
|
||||
devServer: {
|
||||
isDevServer: process.env.WEBPACK_IS_DEV_SERVER === "true",
|
||||
host: devServerHost,
|
||||
url: devServerUrl,
|
||||
},
|
||||
webpack: {
|
||||
rootDir: join(__dirname, "../"),
|
||||
},
|
||||
};
|
||||
export { EnvironmentConfig };
|
||||
21
webpack/dev.js
Normal file
21
webpack/dev.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import config from "./config";
|
||||
import { devPlugins } from "./options/plugins";
|
||||
|
||||
export default {
|
||||
target: "web",
|
||||
mode: "development",
|
||||
devtool: "eval-cheap-source-map",
|
||||
devServer: {
|
||||
port: process.env.PORT || 5005,
|
||||
host: config.devServer.host,
|
||||
historyApiFallback: true,
|
||||
hot: true,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
client: {
|
||||
overlay: false,
|
||||
},
|
||||
},
|
||||
plugins: devPlugins,
|
||||
};
|
||||
66
webpack/options/plugins.js
Normal file
66
webpack/options/plugins.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
|
||||
import CopyPlugin from "copy-webpack-plugin";
|
||||
import Dotenv from "dotenv-webpack";
|
||||
import ESLintPlugin from "eslint-webpack-plugin";
|
||||
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import { join } from "path";
|
||||
import { ProgressPlugin } from "webpack";
|
||||
|
||||
import config, { EnvironmentConfig } from "../config";
|
||||
|
||||
const basePlugins = [
|
||||
new HtmlWebpackPlugin({
|
||||
publicPath: "/",
|
||||
template: join(config.webpack.rootDir, "src/index.html"),
|
||||
}),
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: {
|
||||
configFile: join(config.webpack.rootDir, "tsconfig.json"),
|
||||
mode: "write-references",
|
||||
},
|
||||
}),
|
||||
new ESLintPlugin({
|
||||
context: join(config.webpack.rootDir, "src"),
|
||||
extensions: ["ts", "tsx"],
|
||||
failOnError: !config.isDev,
|
||||
lintDirtyModulesOnly: true,
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: join(config.webpack.rootDir, "src/assets"),
|
||||
to: "assets",
|
||||
globOptions: {
|
||||
dot: false,
|
||||
ignore: ["**/*.tsx", "**/*.ico"],
|
||||
},
|
||||
noErrorOnMissing: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
new Dotenv({
|
||||
path: (() => {
|
||||
let path = "./.env";
|
||||
if (config.isProd) {
|
||||
path = "./.env.production";
|
||||
} else if (config.isPrelive) {
|
||||
path = "./.env.prelive";
|
||||
} else if (config.isNonprod) {
|
||||
path = "./.env.nonprod";
|
||||
}
|
||||
console.log(`Using '${path}' for build variables.`)
|
||||
return path;
|
||||
})(),
|
||||
}),
|
||||
];
|
||||
|
||||
const devPlugins = [
|
||||
new ProgressPlugin(),
|
||||
!config.isProd &&
|
||||
new ReactRefreshWebpackPlugin({
|
||||
overlay: false,
|
||||
}),
|
||||
].filter((_) => _);
|
||||
|
||||
export { basePlugins, devPlugins };
|
||||
56
webpack/options/rules.js
Normal file
56
webpack/options/rules.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
|
||||
import config from "../config";
|
||||
|
||||
const styleRules = [
|
||||
{
|
||||
loader: config.isProd ? MiniCssExtractPlugin.loader : "style-loader",
|
||||
options: {
|
||||
esModule: false,
|
||||
},
|
||||
},
|
||||
{ loader: "css-loader" },
|
||||
{ loader: "postcss-loader" },
|
||||
];
|
||||
|
||||
export default [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "esbuild-loader",
|
||||
options: {
|
||||
loader: "tsx",
|
||||
target: "es2015",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(gif|png|jpg|jpeg|webp)$/i,
|
||||
exclude: /node_modules/,
|
||||
type: "asset/resource",
|
||||
},
|
||||
{
|
||||
test: /\.(woff(2)?|eot|ttf|otf|)$/,
|
||||
exclude: /node_modules/,
|
||||
type: "asset/inline",
|
||||
},
|
||||
{
|
||||
test: /\.s([ca])ss$/,
|
||||
use: [
|
||||
...styleRules,
|
||||
{ loader: "fast-sass-loader" },
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: styleRules,
|
||||
},
|
||||
];
|
||||
28
webpack/prod.js
Normal file
28
webpack/prod.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ESBuildMinifyPlugin } from "esbuild-loader";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
|
||||
export default {
|
||||
target: "web",
|
||||
mode: "production",
|
||||
devtool: false,
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new ESBuildMinifyPlugin({
|
||||
target: "es2015",
|
||||
css: true,
|
||||
legalComments: "eof",
|
||||
sourcemap: false,
|
||||
minifyWhitespace: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "[name].[contenthash].css",
|
||||
chunkFilename: "[id].[contenthash].css",
|
||||
}),
|
||||
],
|
||||
performance: {
|
||||
hints: false,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user