initial commit

This commit is contained in:
Karli
2025-09-18 19:02:11 +03:00
commit f8a7da141f
55 changed files with 8461 additions and 0 deletions

38
.babelrc.js Normal file
View 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
View File

@@ -0,0 +1,4 @@
> 1%
last 4 versions
not ie < 9
not dead

12
.editorconfig Normal file
View 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
View File

@@ -0,0 +1,2 @@
API_PATH=http://localhost:5000
PORT=5001

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
API_PATH=http://localhost:5000
PORT=5001

1
.env.nonprod Normal file
View File

@@ -0,0 +1 @@
API_PATH=

1
.env.prelive Normal file
View File

@@ -0,0 +1 @@
API_PATH=

1
.env.production Normal file
View File

@@ -0,0 +1 @@
API_PATH=

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist

123
.eslintrc.js Normal file
View 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
View File

@@ -0,0 +1,11 @@
.idea
.vscode
dist
node_modules
*.log
.swp
.env

6
.postcssrc.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: [
"postcss-preset-env",
"autoprefixer",
],
};

36
.prettierrc.js Normal file
View 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",
},
},
],
};

1
README.md Normal file
View File

@@ -0,0 +1 @@
# React ESBuild template

21
deploy/demo/Dockerfile Normal file
View 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;"]

View 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
View 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;"]

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

View 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;"]

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

@@ -0,0 +1,4 @@
declare module "*.scss" {
const content: { [className: string]: string };
export = content;
}

14
src/api/types.ts Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

View File

0
src/assets/svg/.gitkeep Normal file
View File

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

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

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

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

View File

@@ -0,0 +1,5 @@
const NavigationPath = {
Home: "/",
};
export default NavigationPath;

29
src/constants/routes.ts Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
{}

1
src/locale/et.json Normal file
View File

@@ -0,0 +1 @@
{}

203
src/pages/home/Homepage.tsx Normal file
View 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;

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

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

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

6874
yarn.lock Normal file

File diff suppressed because it is too large Load Diff