├── .gitignore ├── README.md ├── craco.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── app.tsx ├── assets │ └── .gitignore ├── components │ ├── common │ │ ├── ordinary │ │ │ └── Table │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ ├── simple │ │ │ └── Align │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ ├── smart │ │ │ └── DragAndDrop │ │ │ │ └── index.tsx │ │ └── ui │ │ │ └── Image │ │ │ ├── index.tsx │ │ │ └── styled.ts │ ├── containers │ │ └── .gitignore │ ├── pages │ │ ├── Main │ │ │ └── index.tsx │ │ └── Product │ │ │ └── index.tsx │ └── routes │ │ └── index.tsx ├── core │ ├── api.ts │ ├── config │ │ ├── api.config.ts │ │ ├── index.ts │ │ └── routes.config.ts │ ├── helpers │ │ ├── index.ts │ │ └── schema.helpers.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useAPI.ts │ │ └── useLocalStore.ts │ ├── models │ │ ├── index.ts │ │ └── product.models.ts │ ├── services │ │ ├── index.ts │ │ └── product.service.ts │ ├── store │ │ ├── index.tsx │ │ └── segments │ │ │ └── app.ts │ ├── theme │ │ └── index.ts │ ├── types │ │ ├── index.ts │ │ └── styled.d.ts │ └── utils │ │ └── index.ts ├── index.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts └── styles │ ├── common.ts │ └── index.ts ├── tsconfig.json ├── tsconfig.paths.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-rest-api-typescript-boilerplate 2 | 3 | I will be glad to any suggestions and corrections, for I am constantly trying to improve the structure of my projects :) 4 | 5 | My email for contact - phoenix.ofdarkness@mail.ru 6 | 7 | ## Pages 8 | 9 | This folder contains the components that form the pages. (Can use Containers, Smart, Ordinary, Simple, and UI components in their implementation) 10 | 11 | ## Containers 12 | 13 | Container components that can use services and custom hooks are stored in this folder. (They may use Smart, Ordinary, Simple and UI components in their implementation.) 14 | 15 | ## Components 16 | 17 | ### Smart 18 | 19 | This folder contains all smart components, which can use both the repository and custom hooks, but which cannot use services. (Can use in their implementation Ordinary, Simple and UI components) 20 | 21 | ### Ordinary 22 | 23 | This folder stores all the complex components that can't use the repository or custom hooks. (Can use Simple and UI components in their implementation) 24 | 25 | ### Simple 26 | 27 | This folder stores all auxiliary, silly components, which must not use any custom hooks or repository. (Can use UI components in their implementation) 28 | 29 | ### UI 30 | 31 | This folder contains all components that replace the native components (for example: button, label, radio, select, h1, h2, h3, and so on). These components may contain some logic, using hooks and storage (for example in the Select component) 32 | 33 | ## Core 34 | 35 | This folder contains all of the logic of the project. From services to config. 36 | 37 | ### Config 38 | 39 | This folder contains configuration files of example (for example: URI of servers or some limits/patterns/routes) 40 | 41 | ### Constants 42 | 43 | There are constants in this folder (for example: messages, units, mime-types) 44 | 45 | ### Hooks 46 | 47 | This folder contains all your hooks. 48 | 49 | ### Models 50 | 51 | In this folder there are all your models which come from API server. Try not to specify attached models in descriptions of the models themselves, it is better to do it in description of the response type of corresponding service. 52 | 53 | ### Schemes 54 | 55 | This folder contains your table schemes or form schemes. 56 | 57 | ### Services 58 | 59 | This folder stores your services for API-server communication. 60 | 61 | ### Store 62 | 63 | This folder stores (if used with MobX) store context and global store schemes. 64 | 65 | ### Theme (for styled-components) 66 | 67 | This folder stores your project's theme(s). 68 | 69 | ### Types 70 | 71 | This folder stores all domain types 72 | 73 | ### Utils 74 | 75 | This folder stores all auxiliary functions 76 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoAlias = require("craco-alias"); 2 | 3 | module.exports = { 4 | plugins: [ 5 | { 6 | plugin: CracoAlias, 7 | options: { 8 | source: "tsconfig", 9 | baseUrl: "./src", 10 | tsConfigPath: "./tsconfig.paths.json", 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^6.4.3", 7 | "axios": "^0.21.1", 8 | "mobx": "^6.1.6", 9 | "mobx-react-lite": "^3.2.0", 10 | "polished": "^4.1.0", 11 | "react": "^17.0.1", 12 | "react-dom": "^17.0.1", 13 | "react-router": "^5.2.0", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "4.0.2", 16 | "styled-components": "^5.2.1", 17 | "web-vitals": "^1.0.1" 18 | }, 19 | "scripts": { 20 | "start": "craco start", 21 | "build": "craco build" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "@testing-library/jest-dom": "^5.11.4", 43 | "@testing-library/react": "^11.1.0", 44 | "@testing-library/user-event": "^12.1.10", 45 | "@types/jest": "^26.0.15", 46 | "@types/node": "^12.0.0", 47 | "@types/react": "^17.0.0", 48 | "@types/react-dom": "^17.0.0", 49 | "@types/react-helmet": "^6.1.0", 50 | "@types/react-input-mask": "^3.0.0", 51 | "@types/react-router": "^5.1.11", 52 | "@types/react-router-dom": "^5.1.7", 53 | "@types/styled-components": "^5.1.7", 54 | "craco-alias": "^3.0.1", 55 | "typescript": "^4.1.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukkk3/react-rest-api-typescript-boilerplate/3ac2fbc793e22a9a7d0cf196bda6790bd3b1cca4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | React App 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukkk3/react-rest-api-typescript-boilerplate/3ac2fbc793e22a9a7d0cf196bda6790bd3b1cca4/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukkk3/react-rest-api-typescript-boilerplate/3ac2fbc793e22a9a7d0cf196bda6790bd3b1cca4/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import React from "react"; 3 | 4 | import { Routes } from "@components/routes"; 5 | 6 | export const App: React.FC = () => { 7 | return ; 8 | }; 9 | -------------------------------------------------------------------------------- /src/assets/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukkk3/react-rest-api-typescript-boilerplate/3ac2fbc793e22a9a7d0cf196bda6790bd3b1cca4/src/assets/.gitignore -------------------------------------------------------------------------------- /src/components/common/ordinary/Table/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | 3 | import { Align } from "@components/common/simple/Align"; 4 | 5 | import * as S from "./styled"; 6 | 7 | export interface Props { 8 | rows: R[]; 9 | columns: C; 10 | templateColumns?: (number | null | string)[]; 11 | prepareColumns?: (columns: C) => any[]; 12 | prepareRows?: (row: R) => any[]; 13 | renderRows?: (props: { 14 | row: R; 15 | index: number; 16 | Row: typeof S.Row; 17 | Cell: typeof S.Cell; 18 | CellContent: typeof S.CellContent; 19 | }) => React.ReactNode; 20 | renderColumns?: (props: { 21 | columns: C; 22 | Cell: typeof S.Cell; 23 | CellContent: typeof S.CellContent; 24 | }) => React.ReactNode; 25 | header?: React.ReactNode; 26 | footer?: React.ReactNode; 27 | } 28 | 29 | export const Table = ({ 30 | rows, 31 | columns, 32 | header, 33 | footer, 34 | renderRows, 35 | renderColumns, 36 | prepareRows, 37 | prepareColumns, 38 | templateColumns, 39 | }: Props) => { 40 | const columnKeys = useMemo(() => Object.keys(columns || {}), [columns]); 41 | const preparedTemplateColumns = useMemo( 42 | () => (templateColumns ? templateColumns : Array(columnKeys.length).fill(1)), 43 | [templateColumns, columnKeys] 44 | ); 45 | 46 | return ( 47 | 48 | {prepareColumns || renderColumns ? ( 49 | 50 | {header ? {header} : null} 51 | 52 | {renderColumns 53 | ? renderColumns({ 54 | columns: columns, 55 | Cell: S.Cell, 56 | CellContent: S.CellContent, 57 | }) 58 | : prepareColumns 59 | ? prepareColumns(columns).map((column, index) => ( 60 | 61 | 62 |

{column}

63 |
64 |
65 | )) 66 | : null} 67 |
68 |
69 | ) : null} 70 | {prepareRows || renderRows ? ( 71 | 72 | {rows.map((row, rowIndex) => 73 | renderRows ? ( 74 | renderRows({ 75 | row: row, 76 | index: rowIndex, 77 | Row: S.Row, 78 | Cell: S.Cell, 79 | CellContent: S.CellContent, 80 | }) 81 | ) : prepareRows ? ( 82 | 83 | {prepareRows(row).map((cell, cellIndex) => ( 84 | 85 | 86 | {cell} 87 | 88 | 89 | ))} 90 | 91 | ) : null 92 | )} 93 | 94 | ) : null} 95 | {footer ? {footer} : null} 96 |
97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /src/components/common/ordinary/Table/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | function unitsToGridTemplate(units: (string | null | number)[]) { 4 | return units 5 | .map((cell) => { 6 | switch (true) { 7 | case typeof cell === "number": 8 | return `${cell}fr`; 9 | case typeof cell === "string": 10 | return cell; 11 | default: 12 | return "auto"; 13 | } 14 | }) 15 | .join(" "); 16 | } 17 | 18 | export const Table = styled.div``; 19 | 20 | export const Cell = styled.div` 21 | padding: 2rem; 22 | overflow: hidden; 23 | position: relative; 24 | `; 25 | 26 | export const CellContent = styled.div` 27 | width: 100%; 28 | `; 29 | 30 | interface RowProps { 31 | $isHead?: boolean; 32 | $isSelected?: boolean; 33 | $templateColumns: (number | string | null)[]; 34 | } 35 | 36 | export const Row = styled.div` 37 | display: grid; 38 | grid-template-columns: ${(props) => unitsToGridTemplate(props.$templateColumns)}; 39 | transition: background 0.1s linear; 40 | `; 41 | 42 | export const HeaderGroup = styled.div` 43 | border-bottom: 1px solid ${(props) => props.theme.colors.complex.border}; 44 | `; 45 | 46 | export const FooterGroup = styled.div` 47 | border-top: 1px solid ${(props) => props.theme.colors.complex.border}; 48 | `; 49 | 50 | export const Body = styled.div` 51 | overflow: hidden; 52 | 53 | ${Row}:not(:first-child) { 54 | border-top: 1px solid ${(props) => props.theme.colors.complex.border}; 55 | } 56 | `; 57 | 58 | export const Head = styled.div` 59 | ${Row} { 60 | border-bottom: 1px solid ${(props) => props.theme.colors.complex.border}; 61 | 62 | ${Cell} { 63 | font-size: ${(props) => props.theme.sizes.common.fontSizeSecondary}; 64 | text-transform: uppercase; 65 | } 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /src/components/common/simple/Align/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | 3 | import * as S from "./styled"; 4 | 5 | export const Align: React.FC = memo(({ children, axis, isAdaptable = false }) => { 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | }); 12 | 13 | export interface Props { 14 | axis: ("y" | "x")[] | "x" | "y"; 15 | children?: React.ReactNode; 16 | isAdaptable?: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/common/simple/Align/styled.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | const notAdaptableMixin = css` 4 | width: 100%; 5 | height: 100%; 6 | max-height: 100%; 7 | max-width: 100%; 8 | `; 9 | 10 | interface AlignProps { 11 | $axis: ("y" | "x")[] | "x" | "y"; 12 | $isAdaptable?: boolean; 13 | } 14 | 15 | const adaptableMixin = css` 16 | width: ${(props) => !props.$axis.includes("x") && "100%"}; 17 | height: ${(props) => !props.$axis.includes("y") && "100%"}; 18 | min-width: ${(props) => props.$axis.includes("x") && "100%"}; 19 | min-height: ${(props) => props.$axis.includes("y") && "100%"}; 20 | `; 21 | 22 | export const Align = styled.div` 23 | display: flex; 24 | flex-grow: 1; 25 | justify-content: ${(props) => (props.$axis.includes("x") ? "center" : "start")}; 26 | align-items: ${(props) => (props.$axis.includes("y") ? "center" : "start")}; 27 | ${(props) => (props.$isAdaptable ? adaptableMixin : notAdaptableMixin)}; 28 | `; 29 | -------------------------------------------------------------------------------- /src/components/common/smart/DragAndDrop/index.tsx: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src/components/common/ui/Image/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | 3 | import * as S from "./styled"; 4 | 5 | export interface Props extends React.ComponentProps<"img"> { 6 | sources?: { 7 | src: string; 8 | media?: string; 9 | }[]; 10 | } 11 | 12 | export const Image: React.FC = memo(({ sources = [], alt = "", src = "", ...rest }) => { 13 | return ( 14 | 15 | 16 | {sources.length > 0 17 | ? sources.map(({ src, media }, index) => ( 18 | 19 | )) 20 | : null} 21 | 22 | 23 | 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/common/ui/Image/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Image = styled.div` 4 | position: relative; 5 | overflow: hidden; 6 | `; 7 | 8 | export const Picture = styled.picture` 9 | display: block; 10 | max-width: 100%; 11 | max-height: 100%; 12 | width: 100%; 13 | height: 100%; 14 | `; 15 | 16 | export const NativeImage = styled.img` 17 | max-width: 100%; 18 | max-height: 100%; 19 | `; 20 | -------------------------------------------------------------------------------- /src/components/containers/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukkk3/react-rest-api-typescript-boilerplate/3ac2fbc793e22a9a7d0cf196bda6790bd3b1cca4/src/components/containers/.gitignore -------------------------------------------------------------------------------- /src/components/pages/Main/index.tsx: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src/components/pages/Product/index.tsx: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src/components/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Switch, Route } from "react-router-dom"; 2 | 3 | // import { Main } from "../pages/Main"; 4 | // import {Product} from "../pages/Product"; 5 | 6 | import { routesConfig } from "@core/config"; 7 | 8 | export const Routes: React.FC = () => { 9 | return ( 10 | 11 | 12 | {/*
*/} 13 | 14 | 15 | {/* */} 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/core/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { apiConfig } from "@core/config"; 3 | 4 | const api = axios.create({ 5 | baseURL: apiConfig.serverURI, 6 | }); 7 | 8 | api.interceptors.request.use((req) => { 9 | return { 10 | ...req, 11 | baseURL: apiConfig.serverURI, 12 | }; 13 | }); 14 | 15 | export { api }; 16 | -------------------------------------------------------------------------------- /src/core/config/api.config.ts: -------------------------------------------------------------------------------- 1 | export const serverURI = "http://localhost:8080"; 2 | -------------------------------------------------------------------------------- /src/core/config/index.ts: -------------------------------------------------------------------------------- 1 | export * as apiConfig from "./api.config"; 2 | export * as routesConfig from "./routes.config"; 3 | -------------------------------------------------------------------------------- /src/core/config/routes.config.ts: -------------------------------------------------------------------------------- 1 | import { serverURI } from "./api.config"; 2 | 3 | export const productAPIRoutes = { 4 | findOne: (id: string | number) => `${serverURI}/product/${id}`, 5 | }; 6 | 7 | export const productBrowserRoutes = { 8 | findOne: (to: string | number = ":id") => `/product/${to}`, 9 | }; 10 | -------------------------------------------------------------------------------- /src/core/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * as schemaHelpers from "./schema.helpers"; 2 | -------------------------------------------------------------------------------- /src/core/helpers/schema.helpers.ts: -------------------------------------------------------------------------------- 1 | import { camelCaseToPascalCase } from "@core/utils"; 2 | import type { Schema, SchemaBase } from "@core/types"; 3 | 4 | export function generateStoreSchema(object: T) { 5 | const entries = Object.entries(object) as any[][]; 6 | 7 | const propertiesSetters = entries 8 | .filter(([_, value]) => typeof value !== "function") 9 | .flatMap(([key, value]) => [ 10 | [ 11 | `set${camelCaseToPascalCase(key)}`, 12 | function (this: any, value: any) { 13 | this[key] = value; 14 | }, 15 | ], 16 | [ 17 | `reset${camelCaseToPascalCase(key)}`, 18 | function (this: any) { 19 | this[key] = value; 20 | }, 21 | ], 22 | ]); 23 | 24 | return Object.fromEntries([...entries, ...propertiesSetters]) as Schema.Store; 25 | } 26 | -------------------------------------------------------------------------------- /src/core/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useAPI } from "./useAPI"; 2 | export { useLocalStore } from "./useLocalStore"; 3 | -------------------------------------------------------------------------------- /src/core/hooks/useAPI.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { useCallback, useEffect } from "react"; 3 | 4 | import { useLocalStore } from "@core/hooks"; 5 | import type { API, Take } from "@core/types"; 6 | 7 | function useAPI< 8 | F extends API.Service.Function>, 9 | R extends Take.FromServiceFunction.Response, 10 | P extends Parameters 11 | >({ service, isPendingAfterMount = false, ignoreHTTPErrors = false }: Options) { 12 | const localStore = useLocalStore({ isPending: isPendingAfterMount }); 13 | 14 | const call = useCallback( 15 | async (...params: P): Promise => { 16 | localStore.setIsPending(true); 17 | 18 | try { 19 | const { data } = await service(...params); 20 | const { result } = data; 21 | 22 | return result; 23 | } catch (error) { 24 | if (ignoreHTTPErrors === false) { 25 | console.error(error); 26 | } 27 | 28 | throw error; 29 | } finally { 30 | localStore.setIsPending(false); 31 | } 32 | }, 33 | [service, ignoreHTTPErrors] 34 | ); 35 | 36 | const isPending = useCallback(() => { 37 | return localStore.isPending; 38 | }, []); 39 | 40 | useEffect(() => { 41 | localStore.setIsPending(isPendingAfterMount); 42 | }, [isPendingAfterMount]); 43 | 44 | return { 45 | call, 46 | isPending, 47 | }; 48 | } 49 | 50 | export { useAPI }; 51 | export interface Options { 52 | service: F; 53 | isPendingAfterMount?: boolean; 54 | ignoreHTTPErrors?: boolean; 55 | } 56 | -------------------------------------------------------------------------------- /src/core/hooks/useLocalStore.ts: -------------------------------------------------------------------------------- 1 | import { useLocalObservable } from "mobx-react-lite"; 2 | 3 | import { schemaHelpers } from "@core/helpers"; 4 | import type { SchemaBase } from "@core/types"; 5 | 6 | export function useLocalStore(object: T) { 7 | return useLocalObservable(() => schemaHelpers.generateStoreSchema(object)); 8 | } 9 | -------------------------------------------------------------------------------- /src/core/models/index.ts: -------------------------------------------------------------------------------- 1 | export * as ProductModels from "./product.models"; 2 | -------------------------------------------------------------------------------- /src/core/models/product.models.ts: -------------------------------------------------------------------------------- 1 | export interface Data { 2 | id: number; 3 | name: string; 4 | price: number; 5 | info: { 6 | description: string; 7 | note: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/core/services/index.ts: -------------------------------------------------------------------------------- 1 | export * as productService from "./product.service"; 2 | -------------------------------------------------------------------------------- /src/core/services/product.service.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@core/api"; 2 | import { routesConfig } from "@core/config"; 3 | import type { ProductModels } from "@core/models"; 4 | import type { API } from "@core/types"; 5 | 6 | export function getOne(id: number) { 7 | return api.get>( 8 | routesConfig.productAPIRoutes.findOne(id) 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/core/store/index.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { useLocalObservable } from "mobx-react-lite"; 3 | 4 | import { app } from "./segments/app"; 5 | 6 | export const combinedStore = { app }; 7 | export const storeContext = createContext(combinedStore); 8 | export function StoreProvider({ children }: { children: React.ReactNode }) { 9 | const store = useLocalObservable(() => combinedStore); 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /src/core/store/segments/app.ts: -------------------------------------------------------------------------------- 1 | import { schemaHelpers } from "@core/helpers"; 2 | 3 | export const app = schemaHelpers.createStoreSchema({ 4 | userID: null as null | number, 5 | }); 6 | -------------------------------------------------------------------------------- /src/core/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { transparentize } from "polished"; 2 | 3 | export const theme = { 4 | colors: { 5 | common: { 6 | primary: "#2648f1", 7 | intense: "#151e27", 8 | orange: "#fdb549", 9 | yellow: "#ffe243", 10 | white: "white", 11 | }, 12 | complex: { 13 | border: transparentize(0.85, "#626b73"), 14 | translucentGrey: transparentize(0.95, "#626b73"), 15 | }, 16 | }, 17 | sizes: { 18 | html: { 19 | font: "10px", 20 | }, 21 | common: { 22 | borderRadius: ".3rem", 23 | fontSizePrimary: "1.3rem", 24 | fontSizeSecondary: "1.2rem", 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/core/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosResponse } from "axios"; 2 | 3 | export namespace API { 4 | export namespace Service { 5 | export namespace Response { 6 | export type Upsert = Response; 7 | export type FindOne = Response; 8 | export type FindMany = Response<{ 9 | rows: T[]; 10 | totalRowCount: number; 11 | totalPageCount: number; 12 | }>; 13 | } 14 | export type Function, U extends any[] = any[]> = ( 15 | ...params: U 16 | ) => Promise>; 17 | } 18 | export namespace Request { 19 | export type Filter = { 20 | columnName: string; 21 | operator: "equal" | "like" | "not" | "startsWith" | "endsWith"; 22 | value: string | number | number[] | string[]; 23 | }; 24 | } 25 | export type Response = { 26 | status: number; 27 | result: T; 28 | }; 29 | export type Token = string; 30 | } 31 | 32 | export namespace Schema { 33 | type GetActionKeys = KeysMatching any>; 34 | 35 | type StoreDefaultActions = { 36 | [K in Exclude> as `set${CamelCaseToPascalCase}`]: ( 37 | value: T[K] 38 | ) => void; 39 | } & { 40 | [K in Exclude> as `reset${CamelCaseToPascalCase}`]: () => void; 41 | }; 42 | 43 | export type Store = T & 44 | StoreDefaultActions & { 45 | [K in GetActionKeys]: T[K]; 46 | }; 47 | } 48 | 49 | export namespace SchemaBase { 50 | export type Store = { [s: string]: any }; 51 | } 52 | 53 | export namespace Take { 54 | export namespace FromServiceFunction { 55 | export type Response = T extends API.Service.Function ? R : never; 56 | export type ResponseModel = T extends API.Service.Function< 57 | API.Response 58 | > 59 | ? R 60 | : never; 61 | } 62 | } 63 | 64 | export type CamelCaseToPascalCase = T extends `${infer FirstLetter}${infer _Rest}` 65 | ? `${Capitalize}${_Rest}` 66 | : T; 67 | 68 | export type PascalCaseToCamelCase = T extends `${infer FirstLetter}${infer _Rest}` 69 | ? `${Uncapitalize}${_Rest}` 70 | : T; 71 | 72 | export type WithSet = `set${T}`; 73 | export type WithoutSet = T extends `set${infer _Rest}` ? _Rest : T; 74 | 75 | export type KeysMatching = { 76 | [K in keyof T]-?: T[K] extends V ? K : never; 77 | }[keyof T]; 78 | -------------------------------------------------------------------------------- /src/core/types/styled.d.ts: -------------------------------------------------------------------------------- 1 | import "styled-components"; 2 | import { theme } from "../theme"; 3 | 4 | declare module "styled-components" { 5 | type Theme = typeof theme; 6 | 7 | export interface DefaultTheme extends Theme {} 8 | } 9 | -------------------------------------------------------------------------------- /src/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function isAPISupported(api: string) { 2 | return api in window; 3 | } 4 | 5 | export function isCSSSupports(propertyName: string | string[], value: string) { 6 | if (CSS && CSS.supports) { 7 | return Array.isArray(propertyName) 8 | ? propertyName.every((propertyName) => CSS.supports(propertyName, value)) 9 | : CSS.supports(propertyName, value); 10 | } 11 | 12 | return false; 13 | } 14 | 15 | export function safelyParseJSON | any[]>(string: string) { 16 | try { 17 | return JSON.parse(string) as T; 18 | } catch (e) { 19 | return null; 20 | } 21 | } 22 | 23 | export function camelCaseToPascalCase(string: string) { 24 | const [firstLetter, ...rest] = string; 25 | return `${firstLetter.toUpperCase()}${rest.join("")}`; 26 | } 27 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import { ThemeProvider } from "styled-components"; 5 | 6 | import { StoreProvider } from "@core/store"; 7 | import { theme } from "@core/theme"; 8 | 9 | import { CommonStyles } from "@styles"; 10 | 11 | import { App } from "./app"; 12 | import reportWebVitals from "./reportWebVitals"; 13 | 14 | const appElement = document.getElementById("app"); 15 | 16 | ReactDOM.render( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | , 27 | appElement 28 | ); 29 | 30 | reportWebVitals(); 31 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /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/styles/common.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | export const CommonStyles = createGlobalStyle` 4 | html { 5 | font-size: ${(props) => props.theme.sizes.html.font}; 6 | } 7 | body { 8 | overflow: hidden; 9 | overflow-y: auto; 10 | margin: 0; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | font-style: normal; 14 | font-variant-ligatures: normal; 15 | font-variant-caps: normal; 16 | font-variant-numeric: normal; 17 | font-variant-east-asian: normal; 18 | font-weight: 600; 19 | height: 100vh; 20 | font-stretch: normal; 21 | font-size: ${(props) => props.theme.sizes.common.fontSizePrimary}; 22 | color: ${(props) => props.theme.colors.common.intense}; 23 | } 24 | * { 25 | margin: 0; 26 | padding: 0; 27 | outline: none; 28 | color: inherit; 29 | background: initial; 30 | box-sizing: border-box; 31 | border: initial; 32 | font-family: inherit; 33 | font-weight: inherit; 34 | cursor: inherit; 35 | text-align: inherit; 36 | font-size: inherit; 37 | text-decoration: initial; 38 | word-wrap: break-word; 39 | text-transform: inherit; 40 | } 41 | #app { 42 | height: 100%; 43 | } 44 | img { 45 | max-width: 100%; 46 | max-height: 100%; 47 | } 48 | input, 49 | textarea, 50 | select { 51 | border: initial; 52 | padding: initial; 53 | background: initial; 54 | } 55 | button { 56 | cursor: pointer; 57 | } 58 | svg { 59 | width: 100%; 60 | height: 100%; 61 | fill: inherit; 62 | stroke: inherit; 63 | stroke-width: inherit; 64 | } 65 | svg g, 66 | svg path { 67 | stroke: inherit; 68 | fill: inherit; 69 | stroke-width: inherit; 70 | } 71 | a { 72 | cursor: pointer; 73 | } 74 | .noselect { 75 | user-select: none; 76 | } 77 | `; 78 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | export { CommonStyles } from "./common"; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext", 8 | "ES2019.Object" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "downlevelIteration": true, 19 | "isolatedModules": true, 20 | "moduleResolution": "node", 21 | "resolveJsonModule": true, 22 | "strictNullChecks": true, 23 | "noEmit": true, 24 | "keyofStringsOnly": true, 25 | "jsx": "react-jsx" 26 | }, 27 | "extends": "./tsconfig.paths.json", 28 | "include": [ 29 | "src", 30 | "src/core/types" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "paths": { 5 | "@components/*": ["components/*"], 6 | "@assets/*": ["assets/*"], 7 | "@styles": ["styles"], 8 | "@core/*": ["core/*"] 9 | } 10 | } 11 | } 12 | --------------------------------------------------------------------------------