[1]) =>
41 | testRender(ui, { wrapper: Wrapper, ...options });
42 |
43 | export { history };
44 |
45 | export const mockAllApi = () => {
46 | mockApi(
47 | Object.keys(api).reduce((acc, key) => {
48 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
49 | acc[key as keyof typeof acc] = [] as any;
50 |
51 | return acc;
52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
53 | }, {} as any)
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/modules/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | "airbnb-typescript",
4 | "airbnb/hooks",
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/eslint-recommended",
7 | "plugin:@typescript-eslint/recommended",
8 | "plugin:eslint-comments/recommended",
9 | "plugin:jest/recommended",
10 | "plugin:react/recommended",
11 | "plugin:import/warnings",
12 | "plugin:import/typescript",
13 | "prettier",
14 | ],
15 | plugins: ["simple-import-sort", "import", "jest", "unused-imports", "i18next", "sonarjs"],
16 | env: {
17 | browser: true,
18 | es6: true,
19 | jest: true,
20 | },
21 | parser: "@typescript-eslint/parser",
22 | parserOptions: {
23 | sourceType: "module",
24 | project: "./tsconfig.json",
25 | },
26 | rules: {
27 | "no-plusplus": "off",
28 | "func-names": "off",
29 | "eslint-disable-next-line no-param-reassign": "off",
30 | "no-param-reassign": [2, { props: false }],
31 | "simple-import-sort/imports": "error",
32 | "simple-import-sort/exports": "error",
33 | "import/first": "error",
34 | "import/newline-after-import": "error",
35 | "import/no-duplicates": "error",
36 | "consistent-return": "off",
37 | "@typescript-eslint/no-implied-eval": "off",
38 | "import/no-named-as-default-member": "off",
39 | "import/no-cycle": "off",
40 | "sort-imports": "off",
41 | "import/order": "off",
42 | "linebreak-style": "off",
43 | "react/no-unknown-property": ["error", { ignore: ["css"] }],
44 | "react/prop-types": "off",
45 | "react/jsx-props-no-spreading": "off",
46 | "jsx-a11y/anchor-is-valid": "off",
47 | "import/prefer-default-export": "off",
48 | "react/display-name": "off",
49 | "no-void": "off",
50 | "jest/no-disabled-tests": "off",
51 | "react/react-in-jsx-scope": "off",
52 | "react/require-default-props": "off",
53 | "import/no-named-as-default": "off",
54 | "jsx-a11y/no-static-element-interactions": "off",
55 | "jsx-a11y/click-events-have-key-events": "off",
56 | "@typescript-eslint/no-non-null-assertion": "off",
57 | "@typescript-eslint/explicit-module-boundary-types": "off",
58 | "import/no-useless-path-segments": [
59 | "error",
60 | {
61 | noUselessIndex: true,
62 | },
63 | ],
64 | "no-restricted-imports": [
65 | "error",
66 | {
67 | paths: [
68 | "antd",
69 | "lib/swagger/generated",
70 | {
71 | name: "react",
72 | importNames: ["FC"],
73 | message: "Write Function Components as regular functions with props.",
74 | },
75 | {
76 | name: "react-hook-form",
77 | importNames: ["useForm"],
78 | message: "Import from components/Form",
79 | },
80 | {
81 | name: "@emotion/css",
82 | importNames: ["css"],
83 | message: "Import from @emotion/react.",
84 | },
85 | ],
86 | patterns: [
87 | "antd/*",
88 | "rc-table/*",
89 | "components/Form/*",
90 | "models/*",
91 | "components/Table/*",
92 | "components/Calendar/*",
93 | "lib/api/responses",
94 | "../../*",
95 | ],
96 | },
97 | ],
98 | "@typescript-eslint/no-explicit-any": "error",
99 | "padding-line-between-statements": [
100 | "error",
101 | { blankLine: "always", prev: "*", next: "return" },
102 | { blankLine: "always", prev: ["const", "let", "var"], next: "*" },
103 | { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"] },
104 | ],
105 | "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
106 | "@typescript-eslint/array-type": ["error", { default: "array-simple" }],
107 | "@typescript-eslint/prefer-ts-expect-error": "error",
108 | "@typescript-eslint/prefer-as-const": "error",
109 | "@typescript-eslint/no-unused-vars": [
110 | "warn",
111 | { vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_" },
112 | ],
113 | "react/destructuring-assignment": "off",
114 | "react/forbid-dom-props": ["error", { forbid: ["style"] }],
115 | "react/jsx-sort-props": [
116 | "error",
117 | {
118 | callbacksLast: true,
119 | shorthandFirst: true,
120 | ignoreCase: true,
121 | reservedFirst: true,
122 | },
123 | ],
124 | "react/jsx-no-useless-fragment": "error",
125 | "react/jsx-fragments": "error",
126 | "no-nested-ternary": "off",
127 | "react/function-component-definition": [
128 | "error",
129 | { namedComponents: "arrow-function", unnamedComponents: "arrow-function" },
130 | ],
131 |
132 | "@typescript-eslint/ban-ts-comment": [
133 | "error",
134 | { "ts-expect-error": "allow-with-description", minimumDescriptionLength: 5 },
135 | ],
136 | "spaced-comment": ["error", "always", { markers: ["#region"], exceptions: ["#endregion"] }],
137 |
138 | "no-console": "warn",
139 | "eslint-comments/no-unlimited-disable": "error",
140 | "eslint-comments/disable-enable-pair": "off",
141 | "import/no-extraneous-dependencies": ["error", { devDependencies: ["**/*.spec.ts*"] }],
142 | "arrow-body-style": "off",
143 | "prefer-arrow-callback": "off",
144 | },
145 | };
146 |
--------------------------------------------------------------------------------
/src/modules/CV.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 |
3 | import type { Theme } from "../components/theme";
4 | import { css } from "@emotion/react";
5 |
6 | const styles = {
7 | container: (theme: Theme) =>
8 | css({
9 | display: "flex",
10 | height: "100vh",
11 | background: "linear-gradient(rgb(241, 135, 79) 0%, rgb(194, 58, 134) 100%)",
12 | //
13 | }),
14 | };
15 |
16 | export const CV = () => {
17 | const { t } = useTranslation("common");
18 |
19 | return //
;
20 | };
21 |
--------------------------------------------------------------------------------
/src/modules/ErrorFallback.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import type { FallbackProps } from "react-error-boundary";
3 |
4 | import { Button } from "../components/Button";
5 | import { isAxiosError } from "../lib/api/errors";
6 |
7 | const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
8 | let message = Something went wrong.
;
9 |
10 | if (isAxiosError(error) && Number(error.response?.status) === 403) {
11 | message = (
12 |
13 |
You don't have permission to access this page.
14 |
15 | );
16 |
17 | return {message}
;
18 | }
19 |
20 | return isAxiosError(error) ? (
21 |
22 |
Something went wrong:
23 |
{error.message}
24 |
25 |
26 | ) : (
27 | {message}
28 | );
29 | };
30 |
31 | export default ErrorFallback;
32 |
--------------------------------------------------------------------------------
/src/modules/layouts/MainLayout.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { ErrorBoundary } from "react-error-boundary";
3 | import { QueryErrorResetBoundary } from "react-query";
4 |
5 | import { Loader } from "../../components/Loader";
6 | import type { ChildrenProps } from "../../routes/types";
7 | import ErrorFallback from "../ErrorFallback";
8 |
9 | export const MainLayout = ({ children }: ChildrenProps) => {
10 | return (
11 |
12 | {({ reset }) => (
13 |
14 | }>
15 | {children}
16 |
17 |
18 | )}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/public/en/cv.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorPashko/react-ts-skeleton/072d4cac68ef213a4a78ef7b6fd4bec710f2cb79/src/public/en/cv.json
--------------------------------------------------------------------------------
/src/public/en/index.js:
--------------------------------------------------------------------------------
1 | export { default as validation } from "./en/cv.json";
2 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | // eslint-disable-next-line @typescript-eslint/triple-slash-reference
3 | ///
4 | ///
5 | ///
6 | ///
7 | ///
8 |
9 | declare namespace NodeJS {
10 | interface ProcessEnv {
11 | readonly NODE_ENV: "development" | "production" | "test";
12 | readonly PUBLIC_URL: string;
13 | }
14 | }
15 |
16 | declare module "*.avif" {
17 | const src: string;
18 | export default src;
19 | }
20 |
21 | declare module "*.bmp" {
22 | const src: string;
23 | export default src;
24 | }
25 |
26 | declare module "*.gif" {
27 | const src: string;
28 | export default src;
29 | }
30 |
31 | declare module "*.jpg" {
32 | const src: string;
33 | export default src;
34 | }
35 |
36 | declare module "*.jpeg" {
37 | const src: string;
38 | export default src;
39 | }
40 |
41 | declare module "*.png" {
42 | const src: string;
43 | export default src;
44 | }
45 |
46 | declare module "*.webp" {
47 | const src: string;
48 | export default src;
49 | }
50 |
51 | declare module "*.svg" {
52 | // eslint-disable-next-line no-restricted-imports
53 | import type * as React from "react";
54 |
55 | export const ReactComponent: React.FunctionComponent & { title?: string }>;
56 |
57 | const src: string;
58 | export default src;
59 | }
60 |
61 | declare module "*.module.css" {
62 | const classes: { readonly [key: string]: string };
63 | export default classes;
64 | }
65 |
66 | declare module "*.module.scss" {
67 | const classes: { readonly [key: string]: string };
68 | export default classes;
69 | }
70 |
71 | declare module "*.module.sass" {
72 | const classes: { readonly [key: string]: string };
73 | export default classes;
74 | }
75 |
--------------------------------------------------------------------------------
/src/react-i18next.d.ts:
--------------------------------------------------------------------------------
1 | import "react-i18next";
2 |
3 | // import type { i18n, StringMap, TFunctionKeys, TFunctionResult, TOptions } from "i18next";
4 | import type * as translations from "locales";
5 |
6 | declare module "react-i18next" {
7 | type DefaultResources = typeof translations["en"];
8 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
9 | interface Resources extends DefaultResources {}
10 | }
11 |
12 | // react-i18next versions higher than 11.11.0
13 | declare module "react-i18next" {
14 | interface CustomTypeOptions {
15 | defaultNS: typeof defaultNS;
16 | resources: typeof translations["en"];
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/routes/IndexRedirect.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Navigate } from "react-router-dom";
3 |
4 | import { NavigationUrls } from "./useNavigation";
5 |
6 | const IndexRedirect = () => ;
7 |
8 | export default IndexRedirect;
9 |
--------------------------------------------------------------------------------
/src/routes/PrivateRoute.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from "mobx-react-lite";
2 | import { useTranslation } from "react-i18next";
3 |
4 | import { useStore } from "../stores/Store";
5 | import type { RouteProps } from "./types";
6 |
7 | export const PrivateRoute = observer(({ component: Component, layout: Layout }: RouteProps) => {
8 | const { user } = useStore();
9 | const { t } = useTranslation("common");
10 |
11 | if (!user) {
12 | return {t("forbidden") as string}
;
13 | }
14 |
15 | return (
16 |
17 |
18 |
19 | );
20 | });
21 |
--------------------------------------------------------------------------------
/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from "mobx-react-lite";
2 | import { Route, Routes as BaseRoutes } from "react-router-dom";
3 |
4 | import { CV } from "../modules/CV";
5 | import { MainLayout } from "../modules/layouts/MainLayout";
6 | import IndexRedirect from "./IndexRedirect";
7 | import { PrivateRoute } from "./PrivateRoute";
8 | import { NavigationUrls } from "./useNavigation";
9 |
10 | const Routes = observer(() => (
11 |
12 | } path="/" />
13 | } path={NavigationUrls.home} />
14 |
15 | ));
16 |
17 | export default Routes;
18 |
--------------------------------------------------------------------------------
/src/routes/types.ts:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 |
3 | export type ChildrenProps = { children?: ReactNode };
4 | export type BasicComponent = (props: ChildrenProps) => JSX.Element;
5 | export type RouteProps = {
6 | component: BasicComponent;
7 | layout: BasicComponent;
8 | visible?: boolean;
9 | };
10 |
--------------------------------------------------------------------------------
/src/routes/useNavigation.ts:
--------------------------------------------------------------------------------
1 | import { useNavigate } from "react-router";
2 |
3 | export enum NavigationUrls {
4 | "home" = "/home",
5 | }
6 |
7 | export const useNavigation = () => {
8 | const navigate = useNavigate();
9 |
10 | return {
11 | goHome: () => navigate(NavigationUrls.home),
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/stores/Store.ts:
--------------------------------------------------------------------------------
1 | import { cast, flow, types } from "mobx-state-tree";
2 |
3 | import { api } from "../lib/api/api";
4 | import type { User } from "../lib/api/models";
5 | import { asyncSuspense } from "../lib/asyncSuspense";
6 | import { sleep } from "../lib/sleep";
7 | import { UserStore } from "./UserStore";
8 |
9 | export const Store = types
10 | .model("Store", {
11 | user: types.maybeNull(UserStore),
12 | })
13 | .actions((self) => ({
14 | setUser: (user: User | undefined) => {
15 | self.user = cast(user);
16 | },
17 | }))
18 | .actions((self) => ({
19 | loadUser: flow(function* () {
20 | // imitate real work with BE
21 | yield sleep(1000);
22 | const user: User = yield api.getUserById(333);
23 |
24 | if (user) {
25 | self.setUser(user);
26 | }
27 | }),
28 | }));
29 |
30 | const store = Store.create({});
31 |
32 | export const useStore = () => store;
33 |
34 | export const initializeStore = asyncSuspense(async () => {
35 | await store.loadUser();
36 |
37 | return store;
38 | }, "store");
39 |
--------------------------------------------------------------------------------
/src/stores/UserStore.ts:
--------------------------------------------------------------------------------
1 | import { types } from "mobx-state-tree";
2 |
3 | export const UserStore = types
4 | .model("User", {
5 | id: types.number,
6 | })
7 | .views((self) => ({
8 | get isAuthenticated() {
9 | return !!self.id;
10 | },
11 | }));
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "noImplicitAny": true,
6 | "noImplicitThis": true,
7 | "strictNullChecks": true,
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "strict": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react-jsx"
21 | },
22 | "include": ["src", "public/locales", "src/modules/.eslintrc.js"]
23 | }
24 |
--------------------------------------------------------------------------------