├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── addons.js ├── manager.ts ├── tsconfig.json └── webpack.config.js ├── .yarnrc ├── Dockerfile ├── README.md ├── babel.config.js ├── commitlint.config.js ├── docker-compose.yml ├── docker └── envoy │ └── Dockerfile ├── dockerfile.debug ├── envoy.yaml ├── lerna.json ├── nx.json ├── package.json ├── packages ├── admin │ ├── .babelrc │ ├── api │ │ ├── ioc.ts │ │ ├── protos │ │ │ └── rpc │ │ │ │ ├── RpcServiceClientPb.ts │ │ │ │ ├── rpc_pb.d.ts │ │ │ │ ├── rpc_pb.js │ │ │ │ ├── subscription_plan_pb.d.ts │ │ │ │ └── subscription_plan_pb.js │ │ └── subscription_plans │ │ │ ├── index.ts │ │ │ └── subscription_plans.ts │ ├── components │ │ ├── DynamicTranslations │ │ │ ├── Component.tsx │ │ │ └── index.ts │ │ ├── Header │ │ │ ├── Component.tsx │ │ │ ├── index.ts │ │ │ └── menu.ts │ │ ├── SwitchButton │ │ │ ├── Component.tsx │ │ │ └── index.ts │ │ ├── SwitchLink │ │ │ ├── Component.tsx │ │ │ └── index.ts │ │ ├── Title │ │ │ ├── Component.tsx │ │ │ └── index.ts │ │ ├── sidebar │ │ │ └── menu.ts │ │ └── styles.ts │ ├── i18n.config.ts │ ├── index.d.ts │ ├── layout │ │ └── main.tsx │ ├── modules │ │ └── subscription_plans │ │ │ ├── epics.ts │ │ │ ├── reducer.ts │ │ │ └── types.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── now.json │ ├── package.json │ ├── pages │ │ ├── [language] │ │ │ ├── index.tsx │ │ │ └── login.tsx │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── index.tsx │ │ ├── login.tsx │ │ └── subscription_plans │ │ │ ├── add.tsx │ │ │ └── index.tsx │ ├── public │ │ ├── images │ │ │ └── register_bg_2.png │ │ ├── rosetta.png │ │ └── translations │ │ │ ├── components │ │ │ ├── DynamicTranslations │ │ │ │ ├── ar.json │ │ │ │ └── en.json │ │ │ ├── Header │ │ │ │ ├── ar.json │ │ │ │ └── en.json │ │ │ ├── RosettaImage │ │ │ │ ├── ar.json │ │ │ │ └── en.json │ │ │ ├── SwitchButton │ │ │ │ ├── ar.json │ │ │ │ └── en.json │ │ │ ├── SwitchLink │ │ │ │ ├── ar.json │ │ │ │ └── en.json │ │ │ └── Title │ │ │ │ ├── ar.json │ │ │ │ └── en.json │ │ │ └── pages │ │ │ └── [language] │ │ │ ├── dynamic │ │ │ ├── ar.json │ │ │ └── en.json │ │ │ ├── index │ │ │ ├── ar.json │ │ │ └── en.json │ │ │ ├── login │ │ │ ├── ar.json │ │ │ └── en.json │ │ │ └── ssr │ │ │ ├── ar.json │ │ │ └── en.json │ ├── store.ts │ ├── tsconfig.json │ ├── tsconfig.spec.json │ ├── types │ │ ├── css.d.ts │ │ └── json.ts │ └── utils │ │ └── i18n.tsx ├── backend │ ├── package.json │ ├── src │ │ ├── app.module.ts │ │ ├── assets │ │ │ └── README.md │ │ ├── codegen │ │ │ ├── rpc.d.ts │ │ │ └── rpc.js │ │ ├── config │ │ │ └── config.module.ts │ │ ├── dispatcher.ts │ │ ├── generate-typings.ts │ │ ├── graphql.schema.ts │ │ ├── helpers │ │ │ └── NestJsLogger.ts │ │ ├── main.ts │ │ ├── service │ │ │ ├── inversify.config.ts │ │ │ ├── service.module.ts │ │ │ └── services.app.ts │ │ ├── shared │ │ │ ├── base.graphql │ │ │ ├── exception-filter │ │ │ │ └── http-exception.filter.ts │ │ │ └── interceptor │ │ │ │ ├── exception.interceptor.ts │ │ │ │ ├── logging.interceptor.ts │ │ │ │ └── timeout.interceptor.ts │ │ └── subscription_plans │ │ │ ├── subcription_plan.graphql │ │ │ ├── subscription_plan.controller.ts │ │ │ ├── subscription_plan.module.ts │ │ │ └── subscription_plan.resolver.ts │ ├── tools │ │ └── gen.sh │ ├── tsconfig.app.json │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── common-backend │ ├── babel.config.js │ ├── package.json │ ├── src │ │ ├── IService.ts │ │ ├── config │ │ │ ├── config.service.ts │ │ │ └── index.ts │ │ ├── database │ │ │ ├── database_service.abstract.ts │ │ │ └── index.ts │ │ ├── env.ts │ │ ├── errors.generator.ts │ │ ├── errors.messages.ts │ │ ├── helpers │ │ │ ├── Log.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── common.interfaces.ts │ │ │ └── index.ts │ │ ├── inversify.config.ts │ │ ├── services │ │ │ ├── base.service.ts │ │ │ └── index.ts │ │ └── types │ │ │ ├── bulk.types.ts │ │ │ ├── index.ts │ │ │ └── sort.types.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── subscription │ ├── babel.config.js │ ├── package.json │ ├── src │ │ ├── database │ │ │ └── database.service.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ └── subscription_plan.interface.ts │ │ ├── inversify.config.ts │ │ ├── message │ │ │ └── error.message.ts │ │ ├── model │ │ │ └── subscription_plan.model.ts │ │ ├── services.app.ts │ │ ├── subscription_plan.seed.ts │ │ ├── subscription_plan.service.spec.ts │ │ ├── subscription_plan.service.ts │ │ ├── types │ │ │ └── subscription_plan.types.ts │ │ └── validators │ │ │ ├── subscription_plan.create.yup.ts │ │ │ ├── subscription_plan.filter.yup.ts │ │ │ └── subscription_plan.update.yup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── ui │ ├── .storybook │ ├── addons.js │ ├── config.js │ ├── tsconfig.json │ └── webpack.config.js │ ├── package.json │ ├── src │ ├── components │ │ ├── backdrop │ │ │ ├── Backdrop.tsx │ │ │ └── index.ts │ │ ├── background │ │ │ ├── Background.tsx │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── variants │ │ │ │ └── BackgroundProvider.tsx │ │ ├── button │ │ │ ├── Button.tsx │ │ │ ├── index.ts │ │ │ ├── style.tsx │ │ │ ├── types.ts │ │ │ └── variants │ │ │ │ ├── LinkButton.tsx │ │ │ │ ├── StandardButton.tsx │ │ │ │ └── index.ts │ │ ├── dashboard │ │ │ ├── Dashboard.tsx │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── dropdown │ │ │ ├── Dropdown.tsx │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── header │ │ │ ├── Header.tsx │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── hooks │ │ │ └── useWindowSize.ts │ │ ├── icon │ │ │ ├── Icon.tsx │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── input │ │ │ ├── index.ts │ │ │ └── input.tsx │ │ ├── notification │ │ │ ├── index.ts │ │ │ ├── notification.provider.tsx │ │ │ ├── notification.tsx │ │ │ ├── types.ts │ │ │ └── useNotify.tsx │ │ ├── sidebar │ │ │ ├── Sidebar.tsx │ │ │ ├── index.ts │ │ │ ├── internals │ │ │ │ └── SidebarContent.tsx │ │ │ └── types.ts │ │ ├── table │ │ │ ├── index.ts │ │ │ └── table.tsx │ │ ├── theme-switcher │ │ │ ├── ThemeSwitcher.tsx │ │ │ └── index.ts │ │ └── theme │ │ │ ├── themes.ts │ │ │ └── types.ts │ ├── context │ │ ├── sidebar.context.tsx │ │ └── theme.context.tsx │ ├── index.ts │ ├── style.css │ └── types.ts │ ├── stories │ ├── background.stories.tsx │ ├── button.stories.tsx │ ├── dashboard.stories.tsx │ ├── data │ │ ├── icons.ts │ │ └── table.ts │ ├── dropdown.stories.tsx │ ├── header.stories.tsx │ ├── input.stories.tsx │ ├── notification.stories.tsx │ ├── sidebar.stories.tsx │ ├── table.stories.tsx │ └── theme-switcher.stories.tsx │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsdx.config.js ├── patches └── nestjs-flub+0.2.0.patch ├── postcss.config.js ├── protos └── rpc │ ├── rpc.proto │ └── subscription_plan.proto ├── tailwind.config.js ├── tools ├── schematics │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.json ├── workspace.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | *.log 5 | packages/**/dist 6 | packages/**/node_modules 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig file: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 4 11 | max_line_length = 80 12 | 13 | [*.{Dockerfile,dart,html,js,jsx,json,scss,tf,ts,tsx,vim,yaml,yml}] 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .next 4 | *.d.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb', 4 | 'prettier', 5 | 'prettier/react', 6 | 'plugin:import/errors', 7 | 'plugin:import/warnings', 8 | 'plugin:import/typescript', 9 | ], 10 | parser: 'babel-eslint', 11 | parserOptions: { 12 | ecmaVersion: 2020, 13 | // Can I remove these now? 14 | ecmaFeatures: { 15 | impliedStrict: true, 16 | classes: true, 17 | }, 18 | }, 19 | env: { 20 | browser: true, 21 | node: true, 22 | jquery: true, 23 | jest: true, 24 | }, 25 | rules: { 26 | 'no-debugger': 0, 27 | 'no-alert': 0, 28 | 'no-await-in-loop': 0, 29 | 'no-return-assign': ['error', 'except-parens'], 30 | 'no-restricted-syntax': [ 31 | 2, 32 | 'ForInStatement', 33 | 'LabeledStatement', 34 | 'WithStatement', 35 | ], 36 | 'no-unused-vars': [ 37 | 1, 38 | { 39 | ignoreRestSiblings: true, 40 | argsIgnorePattern: 'res|next|^err', 41 | }, 42 | ], 43 | 'prefer-const': [ 44 | 'error', 45 | { 46 | destructuring: 'all', 47 | }, 48 | ], 49 | 'arrow-body-style': [2, 'as-needed'], 50 | 'no-unused-expressions': [ 51 | 2, 52 | { 53 | allowTaggedTemplates: true, 54 | }, 55 | ], 56 | 'no-param-reassign': [ 57 | 2, 58 | { 59 | props: false, 60 | }, 61 | ], 62 | 'no-console': 0, 63 | 'import/prefer-default-export': 0, 64 | import: 0, 65 | 'func-names': 0, 66 | 'space-before-function-paren': 0, 67 | 'comma-dangle': 0, 68 | 'max-len': 0, 69 | 'import/extensions': 0, 70 | 'no-underscore-dangle': 0, 71 | 'consistent-return': 0, 72 | 'react/display-name': 1, 73 | 'react/no-array-index-key': 0, 74 | 'react/react-in-jsx-scope': 0, 75 | 'react/prefer-stateless-function': 0, 76 | 'react/forbid-prop-types': 0, 77 | 'react/no-unescaped-entities': 0, 78 | 'jsx-a11y/accessible-emoji': 0, 79 | 'react/require-default-props': 0, 80 | 'react/jsx-filename-extension': [ 81 | 1, 82 | { 83 | extensions: ['.js', '.jsx'], 84 | }, 85 | ], 86 | radix: 0, 87 | 'no-shadow': [ 88 | 2, 89 | { 90 | hoist: 'all', 91 | allow: ['resolve', 'reject', 'done', 'next', 'err', 'error'], 92 | }, 93 | ], 94 | quotes: [ 95 | 2, 96 | 'single', 97 | { 98 | avoidEscape: true, 99 | allowTemplateLiterals: true, 100 | }, 101 | ], 102 | 'prettier/prettier': [ 103 | 'error', 104 | { 105 | trailingComma: 'es5', 106 | singleQuote: true, 107 | printWidth: 80, 108 | }, 109 | ], 110 | 'jsx-a11y/href-no-hash': 'off', 111 | 'jsx-a11y/anchor-is-valid': [ 112 | 'warn', 113 | { 114 | aspects: ['invalidHref'], 115 | }, 116 | ], 117 | 'react-hooks/rules-of-hooks': 'error', 118 | 'react-hooks/exhaustive-deps': 'warn', 119 | }, 120 | plugins: ['html', 'prettier', 'react-hooks', 'simple-import-sort'], 121 | }; 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | .next 5 | build 6 | base 7 | free.bcdapps.client.certificate 8 | .env 9 | .vscode -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": true, 6 | "arrowParens": "avoid", 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register'; 2 | import '@storybook/addon-actions'; 3 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons'; 2 | 3 | addons.setConfig({ 4 | panelPosition: 'bottom', 5 | //theme, 6 | previewTabs: { 7 | 'storybook/docs/panel': { index: -1 }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "exclude": ["../**/*.spec.ts"], 4 | // "include": ["../**/*"] 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // Export a function. Accept the base config as the only param. 2 | module.exports = async ({ config, mode }) => { 3 | // `mode` has a value of 'DEVELOPMENT' or 'PRODUCTION' 4 | // You can change the configuration based on that. 5 | // 'PRODUCTION' is used when building the static version of storybook. 6 | 7 | // Make whatever fine-grained changes you need 8 | 9 | // Return the altered config 10 | return config; 11 | }; 12 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry: "https://registry.yarnpkg.com" 2 | "@bcdapps:registry" "http://192.168.1.8:4873/" 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | ARG BUILD_USER=build 3 | ARG APPLICATION_USER=application 4 | ARG NODE_VERSION=12.16.1 5 | 6 | FROM node:${NODE_VERSION}-alpine as os 7 | 8 | ARG BUILD_USER 9 | ENV buildUser=$BUILD_USER 10 | 11 | RUN addgroup -S appgroup && adduser -S $buildUser -G appgroup 12 | USER $buildUser 13 | 14 | 15 | 16 | WORKDIR /home/$buildUser 17 | 18 | FROM os AS install 19 | COPY package*.json ./ 20 | ADD .yarnrc ./ 21 | COPY yarn.lock ./ 22 | COPY babel.config.js ./ 23 | COPY patches/nestjs-flub+0.2.0.patch ./patches/nestjs-flub+0.2.0.patch 24 | RUN yarn install --only=production 25 | RUN cp -R node_modules /tmp/production_modules 26 | RUN yarn install --frozen-lockfile 27 | COPY . . 28 | 29 | 30 | FROM install AS build 31 | USER root 32 | WORKDIR /home/$buildUser 33 | RUN yarn build:backend 34 | 35 | 36 | FROM node:${NODE_VERSION}-alpine 37 | 38 | ARG APPLICATION_USER 39 | ARG BUILD_USER 40 | ENV applicationUser=$APPLICATION_USER 41 | ENV buildUser=$BUILD_USER 42 | RUN apk add --update --no-cache bash 43 | RUN addgroup -S appgroup && adduser -S $applicationUser -G appgroup 44 | 45 | 46 | WORKDIR /home/$applicationUser 47 | 48 | COPY --from=install /tmp/production_modules node_modules 49 | COPY --from=build /home/$buildUser/dist dist 50 | 51 | COPY free.bcdapps.client.certificate free.bcdapps.client.certificate 52 | COPY protos protos 53 | RUN mkdir -p packages/backend/src/graphql 54 | COPY packages/backend/src/**/*.graphql packages/backend/src/graphql/ 55 | 56 | EXPOSE 3000 57 | CMD ["node", "dist/packages/backend/main.js"] 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Full Stack starter kit 2 | 3 | Full-Stack starter kit is your starter monorepo for building apps both in **NodeJs** and **React**. 4 | Monorepo is setup using the dev tool [nx.dev](https://nx.dev/). We can create multiple apps and libraries via nx.dev and use them in different applications and publish npm packages. 5 | 6 | # Tables of Content 7 | 8 | - Getting started 9 | - Built with 10 | - Packages 11 | - :rocket: [Admin](#Admin) 12 | - :rocket: [Backend](#Backend) 13 | - :package: [Common-Backend](#Common-Backend) 14 | - :package: [Subscription](#Subscription) 15 | - :package: [UI](#UI) 16 | 17 | ## Getting Started 18 | 19 | Instruction to run the app 20 | > **Note:** Sample instructions 21 | 22 | ## Built With 23 | 24 | - nx.dev 25 | - nestjs 26 | - next.js 27 | - tsdx 28 | - inversify 29 | - commitizen 30 | - commit-lint 31 | - husky 32 | - prettier 33 | - eslint 34 | - cz-ccgls 35 | - docker-compose 36 | - envoy (grpc proxy) 37 | - verdaccio (private package repository) 38 | 39 | ## Packages 40 | 41 | Library consist of 5 packages 42 | 43 | ### :rocket:[Admin](https://github.com/asimashfaq/fullstack-starter/tree/master/packages/admin) 44 | Admin panel dashboard 45 | 46 | ```mermaid 47 | graph LR 48 | A[Admin] --> C(UI) 49 | ``` 50 | #### Features 51 | 52 | - Internalization 53 | - Grpc 54 | - Server sider rendering 55 | - Theme Switcher 56 | 57 | #### Build With 58 | 59 | - NextJs 60 | - Typescript 61 | - StyledComponents 62 | - Tailwindcss 63 | - Redux-Toolkit 64 | - Redux-Observable 65 | - Redux-Epics 66 | - Redux Dev Tools 67 | - Epics 68 | - Rxjs 69 | - Typesafe-Actions 70 | - Redux-Logger 71 | - Postcss 72 | 73 | ### :rocket:[Backend](https://github.com/asimashfaq/fullstack-starter/tree/master/packages/backend) 74 | 75 | Backend Server provide api interface in **Graphql** and **Grpc**. Use **Inversify.js** (a dependency injection tool) to inject the (business logic layer + database layer) into the app imported via libraries or packages. 76 | 77 | In this monorepo we import the **Subscription** library which implemented the `Services` and `Database` Interface provided by the **Common-Backend**. User can interact with Subscription Library via Grphc and Graphql interface provided in the Backend application via NestJS farmework. 78 | 79 | ```mermaid 80 | graph LR 81 | A[Backend] --> C(Subscription) 82 | C --> D(Common-Backend) 83 | A --> D 84 | ``` 85 | 86 | #### Apis 87 | - Subscription 88 | #### Build With 89 | - Typescript 90 | - NestJS 91 | - Inversify 92 | - Grpc-Tools 93 | - Pbts 94 | 95 | ### :package:[Common-Backend](https://github.com/asimashfaq/fullstack-starter/tree/master/packages/backend) 96 | Provide Generic interface and types for 97 | - Database 98 | - Config 99 | - Service 100 | - Pagination 101 | - Errors 102 | - Logger 103 | 104 | Also implemented the common functions that can be used in verious libraries/app build using common-backend. 105 | In our case its used in **Subscription** Library and in **Backend** app. 106 | 107 | #### Build With 108 | 109 | - Typescript 110 | - Inversify 111 | - Tsdx 112 | - Bunyan 113 | 114 | ### :package:[Subscription](https://github.com/asimashfaq/fullstack-starter/blob/master/packages/subscription) 115 | Implement the Subscription Service base on the Service interface provided by `Common-Backend`. 116 | Implement all the business logic , validation and database layer in it. Also Implement the Database Service base on interface provided by `common-backend` It save infromation in the **RavenDB**. 117 | 118 | > Note: Also implement the unit testing . You can run the project as the standalone library without having any kind of transport layer. 119 | 120 | #### Features 121 | 122 | - Add 123 | - Update 124 | - Delete 125 | - List 126 | 127 | #### Build With 128 | 129 | - Typescript 130 | - Raven DB 131 | - Inversify 132 | - Tsdx 133 | - Yup 134 | 135 | 136 | ### :package:[UI](https://github.com/asimashfaq/fullstack-starter/blob/master/packages/ui) 137 | UI is the library of different UI elements build from sacratch or write wrappers on top of 3rd parties libraries. 138 | We can use UI elements in different apps and built our UI on top of it. 139 | 140 | #### UI Element and 3rd Party Wrappers 141 | - Background 142 | - Button 143 | - Dashboard 144 | - Dropdown (popper.js) 145 | - Header 146 | - Hooks 147 | - Icon 148 | - Input (formik) 149 | - Notifiaction 150 | - Table (react-table) 151 | - Sidebar 152 | - Theme-Switcher 153 | 154 | #### Build With 155 | 156 | - Typescript 157 | - Storybook 158 | - React 159 | - Tsdx 160 | - Yup 161 | - Framer-Motion 162 | - ahooks 163 | - Styled-Components 164 | - popper.js 165 | - formik 166 | - react-icons 167 | - react-table 168 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const { BABEL_ENV } = process.env; 2 | const isCommonJS = BABEL_ENV !== undefined && BABEL_ENV === 'cjs'; 3 | const isESM = BABEL_ENV !== undefined && BABEL_ENV === 'esm'; 4 | 5 | module.exports = function(api) { 6 | api.cache(true); 7 | 8 | const presets = [ 9 | [ 10 | '@babel/env', 11 | { 12 | loose: true, 13 | modules: isCommonJS ? 'commonjs' : false, 14 | targets: { 15 | esmodules: isESM ? true : undefined, 16 | }, 17 | }, 18 | ], 19 | '@babel/preset-typescript', 20 | '@babel/preset-react', 21 | ]; 22 | 23 | const plugins = [ 24 | '@babel/plugin-proposal-class-properties', 25 | '@babel/syntax-dynamic-import', 26 | '@babel/plugin-proposal-object-rest-spread', 27 | ]; 28 | 29 | return { 30 | babelrcRoots: [ 31 | // Keep the root as a root 32 | '.', 33 | // Also consider monorepo packages "root" and load their .babelrc files. 34 | './packages/*', 35 | ], 36 | presets, 37 | plugins, 38 | env: { 39 | test: { 40 | presets: [ 41 | [ 42 | '@babel/env', 43 | { 44 | useBuiltIns: 'usage', 45 | targets: { 46 | browsers: ['> 1%'], 47 | }, 48 | }, 49 | ], 50 | '@babel/typescript', 51 | '@babel/react', 52 | ], 53 | }, 54 | }, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | node-server: 4 | build: ./ 5 | image: node-server 6 | ports: 7 | - 5000:5000 8 | - 3001:3001 9 | environment: 10 | NODE_ENV: development 11 | DB_URI: https://a.free.bcdapps.ravendb.cloud 12 | DB_NAME: sampledatabase 13 | DB_CERTS: free.bcdapps.client.certificate/free.bcdapps.client.certificate.pfx 14 | HOST: 0.0.0.0 15 | PORT: 3001 16 | links: 17 | - verdaccio 18 | depends_on: 19 | - verdaccio 20 | networks: 21 | - backend_network 22 | envoy: 23 | build: ./docker/envoy 24 | image: envoy-grpc-backend 25 | restart: always 26 | ports: 27 | - '8080:8080' 28 | - '9901:9901' 29 | networks: 30 | - backend_network 31 | verdaccio: 32 | image: verdaccio/verdaccio:latest 33 | ports: 34 | - 4873:4873 35 | networks: 36 | - backend_network 37 | networks: 38 | backend_network: 39 | driver: 'bridge' 40 | -------------------------------------------------------------------------------- /docker/envoy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM envoyproxy/envoy:v1.15.0 2 | 3 | COPY envoy.yaml /etc/envoy/envoy.yaml 4 | 5 | CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml -l trace --log-path /tmp/envoy_info.log 6 | -------------------------------------------------------------------------------- /dockerfile.debug: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=12.16.1 2 | 3 | FROM node:${NODE_VERSION}-alpine as os 4 | RUN mkdir -p /home/application 5 | RUN chmod -R 777 /home/application 6 | WORKDIR /home/application 7 | ADD package*.json ./ 8 | ADD yarn.lock ./ 9 | ADD .yarnrc ./ 10 | 11 | COPY babel.config.js ./ 12 | COPY patches/nestjs-flub+0.2.0.patch ./patches/nestjs-flub+0.2.0.patch 13 | RUN yarn install 14 | COPY . . 15 | RUN yarn build:backend 16 | 17 | COPY free.bcdapps.client.certificate free.bcdapps.client.certificate 18 | 19 | EXPOSE 3000 20 | CMD ["node", "dist/packages/backend/main.js"] 21 | -------------------------------------------------------------------------------- /envoy.yaml: -------------------------------------------------------------------------------- 1 | 2 | admin: 3 | access_log_path: /tmp/admin_access.log 4 | address: 5 | socket_address: { address: 0.0.0.0, port_value: 9901 } 6 | 7 | static_resources: 8 | listeners: 9 | - name: listener_0 10 | address: 11 | socket_address: { address: 0.0.0.0, port_value: 8080 } 12 | filter_chains: 13 | - filters: 14 | - name: envoy.filters.network.http_connection_manager 15 | typed_config: 16 | "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager 17 | codec_type: auto 18 | stat_prefix: ingress_http 19 | route_config: 20 | name: local_route 21 | virtual_hosts: 22 | - name: local_service 23 | domains: ["*"] 24 | routes: 25 | - match: { prefix: "/" } 26 | route: 27 | cluster: echo_service 28 | max_grpc_timeout: 0s 29 | cors: 30 | allow_origin_string_match: 31 | - prefix: "*" 32 | allow_methods: GET, PUT, DELETE, POST, OPTIONS 33 | allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout 34 | max_age: "1728000" 35 | expose_headers: custom-header-1,grpc-status,grpc-message 36 | http_filters: 37 | - name: envoy.filters.http.grpc_web 38 | - name: envoy.filters.http.cors 39 | - name: envoy.filters.http.router 40 | clusters: 41 | - name: echo_service 42 | connect_timeout: 0.25s 43 | type: logical_dns 44 | http2_protocol_options: {} 45 | lb_policy: round_robin 46 | load_assignment: 47 | cluster_name: cluster_0 48 | endpoints: 49 | - lb_endpoints: 50 | - endpoint: 51 | address: 52 | socket_address: 53 | address: node-server 54 | port_value: 5000 55 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "independent", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true, 6 | "stream":true, 7 | "registry": "https://registry.npmjs.org/", 8 | "command": { 9 | "publish": { 10 | "conventionalCommits": true, 11 | "message": "chore(release): publish", 12 | "ignoreChanges": ["**/stories/**", "**/tests/**"], 13 | "allowBranch": "master" 14 | }, 15 | "version": { 16 | "ignoreChanges": ["**/stories/**", "**/tests/**"] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "bcdapps", 3 | "affected": { 4 | "defaultBase": "master" 5 | }, 6 | "implicitDependencies": { 7 | "workspace.json": "*", 8 | "package.json": { 9 | "dependencies": "*", 10 | "devDependencies": "*" 11 | }, 12 | "tsconfig.json": "*", 13 | "tslint.json": "*", 14 | "nx.json": "*" 15 | }, 16 | "tasksRunnerOptions": { 17 | "default": { 18 | "runner": "@nrwl/workspace/tasks-runners/default", 19 | "options": { 20 | "cacheableOperations": [ 21 | "build", 22 | "lint", 23 | "test", 24 | "e2e", 25 | "build-storybook" 26 | ] 27 | } 28 | } 29 | }, 30 | "projects": { 31 | "admin": { 32 | "tags": [] 33 | }, 34 | "admin-e2e": { 35 | "tags": [], 36 | "implicitDependencies": ["admin"] 37 | }, 38 | "ui": { 39 | "tags": [] 40 | }, 41 | "common-backend": { 42 | "tags": [] 43 | }, 44 | "backend": { 45 | "tags": [], 46 | "implicitDependencies": ["common-backend", "subscription"] 47 | }, 48 | "subscription": { 49 | "tags": [], 50 | "implicitDependencies": ["common-backend"] 51 | } 52 | }, 53 | "workspaceLayout": { 54 | "appsDir": "packages", 55 | "libsDir": "packages" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/admin/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel", 5 | { 6 | "preset-env": { 7 | "targets": { 8 | "browsers": [ 9 | "> 0.25%, not dead" 10 | ] 11 | } 12 | } 13 | } 14 | ], 15 | ], 16 | "plugins": [ 17 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 18 | 19 | [ 20 | "styled-components", 21 | { 22 | "ssr": true, 23 | "displayName": true, 24 | "preprocess": false 25 | } 26 | ], 27 | ], 28 | } 29 | -------------------------------------------------------------------------------- /packages/admin/api/ioc.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'inversify'; 2 | import { SubscriptionPlan } from './protos/rpc/subscription_plan_pb'; 3 | import { 4 | IProvider, 5 | SubscriptionPlanProvider, 6 | } from './subscription_plans/index'; 7 | 8 | export const container = new Container(); 9 | container 10 | .bind>('subscriptionPlanProvider') 11 | .to(SubscriptionPlanProvider); 12 | -------------------------------------------------------------------------------- /packages/admin/api/protos/rpc/rpc_pb.d.ts: -------------------------------------------------------------------------------- 1 | import * as jspb from "google-protobuf" 2 | 3 | import * as rpc_subscription_plan_pb from './subscription_plan_pb'; 4 | 5 | export class Empty extends jspb.Message { 6 | serializeBinary(): Uint8Array; 7 | toObject(includeInstance?: boolean): Empty.AsObject; 8 | static toObject(includeInstance: boolean, msg: Empty): Empty.AsObject; 9 | static serializeBinaryToWriter(message: Empty, writer: jspb.BinaryWriter): void; 10 | static deserializeBinary(bytes: Uint8Array): Empty; 11 | static deserializeBinaryFromReader(message: Empty, reader: jspb.BinaryReader): Empty; 12 | } 13 | 14 | export namespace Empty { 15 | export type AsObject = { 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /packages/admin/api/protos/rpc/rpc_pb.js: -------------------------------------------------------------------------------- 1 | // source: rpc/rpc.proto 2 | /** 3 | * @fileoverview 4 | * @enhanceable 5 | * @suppress {messageConventions} JS Compiler reports an error if a variable or 6 | * field starts with 'MSG_' and isn't a translatable message. 7 | * @public 8 | */ 9 | // GENERATED CODE -- DO NOT EDIT! 10 | 11 | var jspb = require('google-protobuf'); 12 | var goog = jspb; 13 | var global = Function('return this')(); 14 | 15 | var rpc_subscription_plan_pb = require('../rpc/subscription_plan_pb.js'); 16 | goog.object.extend(proto, rpc_subscription_plan_pb); 17 | goog.exportSymbol('proto.rpc.Empty', null, global); 18 | /** 19 | * Generated by JsPbCodeGenerator. 20 | * @param {Array=} opt_data Optional initial data array, typically from a 21 | * server response, or constructed directly in Javascript. The array is used 22 | * in place and becomes part of the constructed object. It is not cloned. 23 | * If no data is provided, the constructed object will be empty, but still 24 | * valid. 25 | * @extends {jspb.Message} 26 | * @constructor 27 | */ 28 | proto.rpc.Empty = function(opt_data) { 29 | jspb.Message.initialize(this, opt_data, 0, -1, null, null); 30 | }; 31 | goog.inherits(proto.rpc.Empty, jspb.Message); 32 | if (goog.DEBUG && !COMPILED) { 33 | /** 34 | * @public 35 | * @override 36 | */ 37 | proto.rpc.Empty.displayName = 'proto.rpc.Empty'; 38 | } 39 | 40 | 41 | 42 | if (jspb.Message.GENERATE_TO_OBJECT) { 43 | /** 44 | * Creates an object representation of this proto. 45 | * Field names that are reserved in JavaScript and will be renamed to pb_name. 46 | * Optional fields that are not set will be set to undefined. 47 | * To access a reserved field use, foo.pb_, eg, foo.pb_default. 48 | * For the list of reserved names please see: 49 | * net/proto2/compiler/js/internal/generator.cc#kKeyword. 50 | * @param {boolean=} opt_includeInstance Deprecated. whether to include the 51 | * JSPB instance for transitional soy proto support: 52 | * http://goto/soy-param-migration 53 | * @return {!Object} 54 | */ 55 | proto.rpc.Empty.prototype.toObject = function(opt_includeInstance) { 56 | return proto.rpc.Empty.toObject(opt_includeInstance, this); 57 | }; 58 | 59 | 60 | /** 61 | * Static version of the {@see toObject} method. 62 | * @param {boolean|undefined} includeInstance Deprecated. Whether to include 63 | * the JSPB instance for transitional soy proto support: 64 | * http://goto/soy-param-migration 65 | * @param {!proto.rpc.Empty} msg The msg instance to transform. 66 | * @return {!Object} 67 | * @suppress {unusedLocalVariables} f is only used for nested messages 68 | */ 69 | proto.rpc.Empty.toObject = function(includeInstance, msg) { 70 | var f, obj = { 71 | 72 | }; 73 | 74 | if (includeInstance) { 75 | obj.$jspbMessageInstance = msg; 76 | } 77 | return obj; 78 | }; 79 | } 80 | 81 | 82 | /** 83 | * Deserializes binary data (in protobuf wire format). 84 | * @param {jspb.ByteSource} bytes The bytes to deserialize. 85 | * @return {!proto.rpc.Empty} 86 | */ 87 | proto.rpc.Empty.deserializeBinary = function(bytes) { 88 | var reader = new jspb.BinaryReader(bytes); 89 | var msg = new proto.rpc.Empty; 90 | return proto.rpc.Empty.deserializeBinaryFromReader(msg, reader); 91 | }; 92 | 93 | 94 | /** 95 | * Deserializes binary data (in protobuf wire format) from the 96 | * given reader into the given message object. 97 | * @param {!proto.rpc.Empty} msg The message object to deserialize into. 98 | * @param {!jspb.BinaryReader} reader The BinaryReader to use. 99 | * @return {!proto.rpc.Empty} 100 | */ 101 | proto.rpc.Empty.deserializeBinaryFromReader = function(msg, reader) { 102 | while (reader.nextField()) { 103 | if (reader.isEndGroup()) { 104 | break; 105 | } 106 | var field = reader.getFieldNumber(); 107 | switch (field) { 108 | default: 109 | reader.skipField(); 110 | break; 111 | } 112 | } 113 | return msg; 114 | }; 115 | 116 | 117 | /** 118 | * Serializes the message to binary data (in protobuf wire format). 119 | * @return {!Uint8Array} 120 | */ 121 | proto.rpc.Empty.prototype.serializeBinary = function() { 122 | var writer = new jspb.BinaryWriter(); 123 | proto.rpc.Empty.serializeBinaryToWriter(this, writer); 124 | return writer.getResultBuffer(); 125 | }; 126 | 127 | 128 | /** 129 | * Serializes the given message to binary data (in protobuf wire 130 | * format), writing to the given BinaryWriter. 131 | * @param {!proto.rpc.Empty} message 132 | * @param {!jspb.BinaryWriter} writer 133 | * @suppress {unusedLocalVariables} f is only used for nested messages 134 | */ 135 | proto.rpc.Empty.serializeBinaryToWriter = function(message, writer) { 136 | var f = undefined; 137 | }; 138 | 139 | 140 | goog.object.extend(exports, proto.rpc); 141 | -------------------------------------------------------------------------------- /packages/admin/api/subscription_plans/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./subscription_plans"; 2 | -------------------------------------------------------------------------------- /packages/admin/api/subscription_plans/subscription_plans.ts: -------------------------------------------------------------------------------- 1 | import * as grpcWeb from 'grpc-web'; 2 | import { injectable } from 'inversify'; 3 | import { SubscriptionPlanServiceClient } from '../protos/rpc/RpcServiceClientPb'; 4 | import { Empty } from '../protos/rpc/rpc_pb'; 5 | import { 6 | SubscriptionPlan, 7 | SubscriptionPlanResponse, 8 | } from '../protos/rpc/subscription_plan_pb'; 9 | 10 | export interface IProvider { 11 | findAll(): Promise; 12 | } 13 | 14 | @injectable() 15 | export class SubscriptionPlanProvider implements IProvider { 16 | constructor() { 17 | console.log('SubscriptionPlanProvider is initialized'); 18 | } 19 | async findAll(): Promise { 20 | try { 21 | const e = new Empty(); 22 | const a = new SubscriptionPlanServiceClient( 23 | 'http://localhost:8081', 24 | null, 25 | null 26 | ); 27 | return new Promise((resolve, reject) => { 28 | a.findAll( 29 | e, 30 | null, 31 | (err: grpcWeb.Error, response: SubscriptionPlanResponse) => { 32 | if (!err) { 33 | return resolve(response.getEdgesList().map((e) => e.toObject())); 34 | } 35 | reject(err.message); 36 | } 37 | ); 38 | }); 39 | } catch (error) { 40 | return Promise.reject(error); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/admin/components/DynamicTranslations/Component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | useDynamicI18n, 4 | withPrefetchDynamicTranslations, 5 | } from '../../utils/i18n'; 6 | 7 | const TranslationsNeeded = '/components/DynamicTranslations'; 8 | 9 | const Component: React.FC = () => { 10 | const { translations, isLoading, error } = useDynamicI18n(TranslationsNeeded); 11 | 12 | if (error) { 13 | return

{error.toString()}

; 14 | } 15 | 16 | if (isLoading) { 17 | return

Loading translations...

; 18 | } 19 | 20 | return

{translations.name}

; 21 | }; 22 | 23 | export const AllTranslationsNeeded: string[] = [TranslationsNeeded]; 24 | 25 | export default withPrefetchDynamicTranslations(Component, TranslationsNeeded); 26 | -------------------------------------------------------------------------------- /packages/admin/components/DynamicTranslations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Component'; 2 | export { default } from './Component'; 3 | -------------------------------------------------------------------------------- /packages/admin/components/Header/Component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SwitchButton, { 3 | AllTranslationsNeeded as SwitchButtonAllTranslationsNeeded, 4 | } from '../SwitchButton'; 5 | import SwtichLink, { 6 | AllTranslationsNeeded as SwitchLinkAllTranslationsNeeded, 7 | } from '../SwitchLink'; 8 | import { useI18n, Link } from '../../utils/i18n'; 9 | import { JsonMap } from '../../types/json'; 10 | 11 | const TranslationsNeeded = '/components/Header'; 12 | 13 | const Component: React.FC = () => { 14 | const { translations } = useI18n(TranslationsNeeded); 15 | 16 | return ( 17 |
18 | 34 |
35 | ); 36 | }; 37 | 38 | export const AllTranslationsNeeded: string[] = [ 39 | TranslationsNeeded, 40 | ...SwitchButtonAllTranslationsNeeded, 41 | ...SwitchLinkAllTranslationsNeeded, 42 | ]; 43 | 44 | export default Component; 45 | -------------------------------------------------------------------------------- /packages/admin/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Component'; 2 | export { default } from './Component'; 3 | -------------------------------------------------------------------------------- /packages/admin/components/Header/menu.ts: -------------------------------------------------------------------------------- 1 | import { IMenuItemProps } from '@bcdapps/ui/dist/components/sidebar/types'; 2 | 3 | export const HeaderDropDownMenu: IMenuItemProps[] = [ 4 | { 5 | icon: 'HiOutlineUser', 6 | name: 'Profile', 7 | path: '/me', 8 | }, 9 | { 10 | icon: 'HiOutlineCog', 11 | name: 'Settings', 12 | path: '/settings', 13 | }, 14 | { 15 | icon: 'HiOutlineLogout', 16 | name: 'Logout', 17 | path: '/logout', 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /packages/admin/components/SwitchButton/Component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'next/router'; 3 | import { 4 | useI18n, 5 | getI18nAgnosticPathname, 6 | setI18nCookie, 7 | changeDocumentLanguage, 8 | } from '../../utils/i18n'; 9 | 10 | const TranslationsNeeded = '/components/SwitchButton'; 11 | 12 | const Component: React.FC = () => { 13 | const { translations, config } = useI18n(TranslationsNeeded); 14 | 15 | return ( 16 | 30 | ); 31 | }; 32 | 33 | export const AllTranslationsNeeded: string[] = [TranslationsNeeded]; 34 | 35 | export default Component; 36 | -------------------------------------------------------------------------------- /packages/admin/components/SwitchButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Component'; 2 | export { default } from './Component'; 3 | -------------------------------------------------------------------------------- /packages/admin/components/SwitchLink/Component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import { useI18n, Link } from '../../utils/i18n'; 4 | 5 | const TranslationsNeeded = '/components/SwitchLink'; 6 | 7 | const Component: React.FC = () => { 8 | const { translations, config } = useI18n(TranslationsNeeded); 9 | 10 | const router = useRouter(); 11 | 12 | return ( 13 | 17 | {translations.name} 18 | 19 | ); 20 | }; 21 | 22 | export const AllTranslationsNeeded: string[] = [TranslationsNeeded]; 23 | 24 | export default Component; 25 | -------------------------------------------------------------------------------- /packages/admin/components/SwitchLink/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Component'; 2 | export { default } from './Component'; 3 | -------------------------------------------------------------------------------- /packages/admin/components/Title/Component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useI18n } from '../../utils/i18n'; 3 | 4 | const TranslationsNeeded = '/components/Title'; 5 | 6 | const Component: React.FC<{ title: string; subtitle?: string }> = ({ 7 | title,subtitle, 8 | }) => { 9 | const { language, translations, config } = useI18n(TranslationsNeeded); 10 | 11 | return ( 12 |
13 |

{title}

14 |

{subtitle}

15 |
16 | ); 17 | }; 18 | 19 | export const AllTranslationsNeeded: string[] = [TranslationsNeeded]; 20 | 21 | export default Component; 22 | -------------------------------------------------------------------------------- /packages/admin/components/Title/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Component'; 2 | export { default } from './Component'; 3 | -------------------------------------------------------------------------------- /packages/admin/components/sidebar/menu.ts: -------------------------------------------------------------------------------- 1 | import { IMenuItemProps } from '@bcdapps/ui/dist/components/sidebar/types'; 2 | 3 | export const sideBarItems: IMenuItemProps[] = [ 4 | { 5 | path: '/app/dashboard', 6 | icon: 'HiOutlineHome', 7 | name: 'Dashboard', 8 | exact: true, 9 | }, 10 | { 11 | path: '/app/forms', 12 | icon: 'HiOutlineNewspaper', 13 | name: 'Forms', 14 | }, 15 | { 16 | path: '/app/charts', 17 | icon: 'HiOutlineChartPie', 18 | name: 'Charts', 19 | }, 20 | ]; -------------------------------------------------------------------------------- /packages/admin/components/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.main` 4 | color: #000; 5 | width: 100%; 6 | height: 100%; 7 | 8 | align-items: center; 9 | justify-content: center; 10 | `; 11 | 12 | export const Logo = styled.img` 13 | width: 25rem; 14 | margin-bottom: 2rem; 15 | `; 16 | 17 | export const Title = styled.h1` 18 | font-size: 2.5rem; 19 | `; 20 | 21 | export const Description = styled.h2` 22 | font-size: 2rem; 23 | font-weight: 400; 24 | `; 25 | 26 | export const Illustration = styled.img` 27 | margin-top: 3rem; 28 | width: min(30rem, 100%); 29 | `; 30 | -------------------------------------------------------------------------------- /packages/admin/i18n.config.ts: -------------------------------------------------------------------------------- 1 | export interface Language { 2 | name: string; 3 | prefix: string; 4 | direction?: string; 5 | } 6 | 7 | interface Config { 8 | [key: string]: Language; 9 | } 10 | 11 | interface Domains { 12 | development: string; 13 | production: string; 14 | } 15 | 16 | const allLanguages: Config = { 17 | en: { 18 | name: 'English', 19 | prefix: 'en', 20 | }, 21 | ar: { 22 | name: 'العربية', 23 | prefix: 'ar', 24 | direction: 'rtl', 25 | }, 26 | }; 27 | 28 | const defaultLanguage: Language = allLanguages.en; 29 | 30 | const domains: Domains = { 31 | development: 'http://localhost:3000', 32 | production: 'https://next-i18n-dynamic.netlify.app', 33 | }; 34 | 35 | export default { 36 | allLanguages, 37 | defaultLanguage, 38 | domains, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/admin/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg'; 2 | -------------------------------------------------------------------------------- /packages/admin/layout/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SidebarProvider, Dashboard } from '@bcdapps/ui'; 3 | import * as S from '../components/styles'; 4 | import { sideBarItems } from '../components/Sidebar/menu'; 5 | import { HeaderDropDownMenu } from '../components/Header/menu'; 6 | export const Main = ({ children }) => { 7 | return ( 8 | <> 9 | 10 | 11 | 15 | {children} 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/admin/modules/subscription_plans/epics.ts: -------------------------------------------------------------------------------- 1 | import { Epic } from 'redux-observable'; 2 | import { from, Observable, of } from 'rxjs'; 3 | import { catchError, filter, map, switchMap } from 'rxjs/operators'; 4 | import { ActionType } from 'typesafe-actions'; 5 | import { container } from '../../api/ioc'; 6 | import { SubscriptionPlanProvider } from '../../api/subscription_plans'; 7 | import { RootState } from '../../store'; 8 | import { 9 | getSubscriptionPlans, 10 | getSubscriptionPlansFailure, 11 | getSubscriptionPlansSuccess, 12 | } from './reducer'; 13 | 14 | type SourceActions = 15 | | typeof getSubscriptionPlans 16 | | typeof getSubscriptionPlansSuccess 17 | | typeof getSubscriptionPlansFailure; 18 | type Action = ActionType; 19 | const subscriptionPlanService = container.get( 20 | 'subscriptionPlanProvider' 21 | ); 22 | export const getSubscriptionPlansEpic: Epic = ( 23 | action$ 24 | ) => 25 | action$.pipe( 26 | filter(getSubscriptionPlans.match), 27 | switchMap>((action) => 28 | from(subscriptionPlanService.findAll()).pipe( 29 | map((res) => getSubscriptionPlansSuccess(res)), 30 | catchError((error) => of(getSubscriptionPlansFailure(error))) 31 | ) 32 | ) 33 | ); 34 | -------------------------------------------------------------------------------- /packages/admin/modules/subscription_plans/reducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { ISubscriptionPlan } from './types'; 3 | 4 | type SubscriptionPlansState = ISubscriptionPlan; 5 | 6 | const initState: SubscriptionPlansState = { 7 | id: '', 8 | name: '', 9 | slug: '', 10 | code: '', 11 | price: 0, 12 | }; 13 | 14 | const subscriptionPlansSlice = createSlice({ 15 | name: 'subscriptionPlans', 16 | initialState: initState as SubscriptionPlansState, 17 | reducers: { 18 | getSubscriptionPlans: ( 19 | state 20 | //action?: PayloadAction, 21 | ) => { 22 | console.log(`Load Subscription Plan`); 23 | // Empty 24 | }, 25 | getSubscriptionPlansSuccess: ( 26 | state, 27 | action: PayloadAction 28 | ) => { 29 | console.log(`Received Subscription Plan`, action); 30 | const plans = action.payload as ISubscriptionPlan[]; 31 | return { 32 | ...state, 33 | plans, 34 | }; 35 | }, 36 | getSubscriptionPlansFailure: (state, action: PayloadAction) => { 37 | console.log(`Receive error from Subscription Plan`, action); 38 | // Empty 39 | }, 40 | }, 41 | }); 42 | 43 | export const { 44 | getSubscriptionPlans, 45 | getSubscriptionPlansSuccess, 46 | getSubscriptionPlansFailure, 47 | } = subscriptionPlansSlice.actions; 48 | 49 | export type SubscriptionPlansActionsWithPayload = 50 | | typeof getSubscriptionPlans 51 | | typeof getSubscriptionPlansSuccess 52 | | typeof getSubscriptionPlansFailure; 53 | 54 | export default subscriptionPlansSlice.reducer; 55 | -------------------------------------------------------------------------------- /packages/admin/modules/subscription_plans/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Subscription Plan Model 3 | * Manage Subscription plans for your service 4 | * Define its duration for both invoice and trail period 5 | * 6 | * @interface ISubscriptionPlan 7 | */ 8 | export interface ISubscriptionPlan { 9 | id: string; 10 | 11 | /** 12 | * Store the Subscription Name 13 | * 14 | * @type {string} 15 | * @memberof ISubscriptionPlan 16 | */ 17 | name: string; 18 | /** 19 | * Random Unique code generated for the subscription plan 20 | * 21 | * @type {string} 22 | * @memberof ISubscriptionPlan 23 | */ 24 | code?: string; 25 | 26 | /** 27 | * Alias of the Subscription Name with lowercase 28 | * 29 | * @type {string} 30 | * @memberof ISubscriptionPlan 31 | */ 32 | slug?: string; 33 | 34 | /** 35 | * Description of the subscription plan 36 | * 37 | * @type {string} 38 | * @memberof ISubscriptionPlan 39 | */ 40 | description?: string; 41 | 42 | /** 43 | * One time cost of the subscription plan 44 | * 45 | * @type {number} 46 | * @memberof ISubscriptionPlan 47 | */ 48 | price: number; 49 | 50 | /** 51 | * Extra fee for the subscription plan like 52 | * signup_fee or credit_card fee 53 | * 54 | * @type {number} 55 | * @memberof ISubscriptionPlan 56 | */ 57 | extra_fee?: number; 58 | create_at?: Date; 59 | /** 60 | * Subscription period must be between 1 and 30 61 | * 62 | * @type {number} 63 | * @memberof ISubscriptionPlan 64 | */ 65 | invoice_period?: number; 66 | 67 | /** 68 | * Duration must be DAY | WEEK | MONTH | YEAR 69 | * 70 | * @type {SubscriptionDuration} 71 | * @memberof ISubscriptionPlan 72 | */ 73 | invoice_duration?: string; 74 | 75 | /** 76 | * To offer trail period for the subscription 77 | * Trail period must be between 1 and 30 78 | * 79 | * @type {number} 80 | * @memberof ISubscriptionPlan 81 | */ 82 | trail_period?: number; 83 | /** 84 | * Duration must be DAY | WEEK | MONTH | YEAR 85 | * 86 | * @type {SubscriptionDuration} 87 | * @memberof ISubscriptionPlan 88 | */ 89 | trail_duration?: string; 90 | } 91 | -------------------------------------------------------------------------------- /packages/admin/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/admin/next.config.js: -------------------------------------------------------------------------------- 1 | const withCSS = require('@zeit/next-css'); 2 | const withTM = require('next-transpile-modules')(['@bcdapps/ui']); // pass the modules you would like to see transpiled 3 | const path = require('path'); 4 | // https://spectrum.chat/thread/ba3668b1-f0b1-42a6-9c71-d7d9d3b67f04 5 | if (typeof require !== 'undefined') { 6 | require.extensions['.less'] = () => {}; 7 | require.extensions['.css'] = file => {}; 8 | } 9 | 10 | const _cfg = (cfg, extra) => Object.assign(cfg || {}, extra); 11 | 12 | const _css = cfg => withCSS(_cfg(cfg, {})); 13 | const _combine = fns => fns.reduce((result, fn) => fn(result), {}); 14 | 15 | const _wtm = cfg => withTM(_cfg(cfg, {})); 16 | module.exports = _combine([_css, _wtm]); 17 | -------------------------------------------------------------------------------- /packages/admin/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | 4 | "builds": [ 5 | { 6 | "src": "next.config.js", 7 | "use": "@now/next" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/plugin-proposal-decorators": "^7.10.5", 7 | "@babel/preset-react": "^7.10.4", 8 | "@next/bundle-analyzer": "^9.3.5", 9 | "@types/styled-components": "^5.1.2", 10 | "@zeit/next-css": "^1.0.1", 11 | "babel-plugin-inline-react-svg": "^1.1.1", 12 | "babel-plugin-styled-components": "^1.11.1", 13 | "next": "^9.4.4", 14 | "reflect-metadata": "^0.1.13", 15 | "styled-components": "^5.1.1", 16 | "swr": "^0.2.3" 17 | }, 18 | "devDependencies": { 19 | "@babel/plugin-syntax-jsx": "^7.10.4", 20 | "@svgr/webpack": "^5.4.0", 21 | "@types/node": "^14.0.20", 22 | "next-images": "^1.4.1", 23 | "next-transpile-modules": "^4.1.0", 24 | "typescript": "^3.9.5" 25 | }, 26 | "files": [ 27 | "dist" 28 | ], 29 | "scripts": { 30 | "dev": "next dev -p 1234", 31 | "dev:static": "npm run build && npm run export && npm run start:static", 32 | "build": "next build", 33 | "export": "next export", 34 | "build:analyze": "ANALYZE=true next build", 35 | "start": "next start", 36 | "start:static": "http-server ./out", 37 | "prod": "npm run start" 38 | }, 39 | "eslintConfig": { 40 | "extends": "react-app" 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all", 47 | "IE >= 10" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version", 53 | "IE 11" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/admin/pages/[language]/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextPage, GetStaticPaths, GetStaticProps } from 'next'; 3 | import Head from 'next/head'; 4 | import { 5 | getI18nStaticPaths, 6 | withI18n, 7 | getI18nProps, 8 | GetI18nProps, 9 | GetI18nQuery, 10 | useI18n, 11 | } from '../../utils/i18n'; 12 | import { Main } from '../../layout/main'; 13 | const Page: NextPage = () => { 14 | const { translations } = useI18n('/pages/[language]/index'); 15 | 16 | return ( 17 | <> 18 | 19 | 20 | {translations.title} 21 | 22 |
Welcome
23 | 24 | ); 25 | }; 26 | 27 | export const getStaticPaths: GetStaticPaths = async () => ({ 28 | paths: [...getI18nStaticPaths()], 29 | fallback: false, 30 | }); 31 | 32 | export const getStaticProps: GetStaticProps< 33 | GetI18nProps, 34 | GetI18nQuery 35 | > = async ({ params }) => { 36 | return { 37 | props: { 38 | ...(await getI18nProps({ 39 | language: params && (params.language as any), 40 | // The reason we're importing here, is because we can only 41 | // import node modules here and not in any other file. 42 | // More specifically, not outside of getStaticProps and getServerSideProps 43 | fs: await import('promise-fs'), // pass it to import all the translations 44 | })), 45 | }, 46 | }; 47 | }; 48 | 49 | export default withI18n(Page, ''); 50 | -------------------------------------------------------------------------------- /packages/admin/pages/[language]/login.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-associated-control */ 2 | /* eslint-disable max-len */ 3 | import React, { useRef } from 'react'; 4 | import { NextPage, GetStaticPaths, GetStaticProps } from 'next'; 5 | import Head from 'next/head'; 6 | import styled from 'styled-components'; 7 | import { getI18nStaticPaths, withI18n, getI18nProps, GetI18nProps, GetI18nQuery, useI18n } from '../../utils/i18n'; 8 | import { Formik, Form } from 'formik'; 9 | import * as Yup from 'yup'; 10 | import { Input } from '@bcdapps/ui/src/components/input'; 11 | import { Button, StandardButton } from '@bcdapps/ui/src/components/button'; 12 | import { ButtonHandler } from '@bcdapps/ui'; 13 | 14 | const LoginSchema = Yup.object().shape({ 15 | email: Yup.string() 16 | .email('Invalid email address') 17 | .required('Required'), 18 | password: Yup.string().required('Required'), 19 | }); 20 | const Wrapper = styled.div.attrs({ className: 'login-container' })``; 21 | const BackgroundImage = styled.div` 22 | background-image: url(/images/register_bg_2.png); 23 | background-size: 100%; 24 | background-repeat: no-repeat; 25 | `; 26 | const ContentContainer = styled.div``; 27 | const Content = styled.div``; 28 | const LoginContent = styled.div` 29 | overflow: hidden; 30 | `; 31 | 32 | const Login: NextPage = () => { 33 | const { translations } = useI18n('/pages/[language]/login'); 34 | const refButton = useRef(); 35 | 36 | return ( 37 | <> 38 | 39 | 40 | {translations.title} 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 |
49 |
50 |
{translations.title}
51 |
52 |
53 |
54 | { 62 | setTimeout(() => { 63 | resetForm({ 64 | email: '', 65 | password: '', 66 | } as any); 67 | refButton && refButton.current && refButton.current.result(false); 68 | 69 | }, 500); 70 | }} 71 | render={({ values,errors, touched, handleSubmit, resetForm, isValid , isSubmitting}) => ( 72 | <> 73 |
74 | 82 | 90 |
91 |
107 |
108 | 109 | )} 110 | /> 111 |
112 |
113 |
114 |
115 |
116 |
117 | 118 | ); 119 | }; 120 | 121 | export const getStaticPaths: GetStaticPaths = async () => ({ 122 | paths: [...getI18nStaticPaths()], 123 | fallback: false, 124 | }); 125 | 126 | export const getStaticProps: GetStaticProps = async ({ params }) => ({ 127 | props: { 128 | ...(await getI18nProps({ 129 | language: params?.language as string, 130 | fs: await import('promise-fs'), 131 | })), 132 | }, 133 | }); 134 | 135 | export default withI18n(Login, '/login'); 136 | -------------------------------------------------------------------------------- /packages/admin/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | /* eslint-disable react/jsx-props-no-spreading */ 4 | import React from 'react'; 5 | import Head from 'next/head'; 6 | import App, { AppInitialProps } from 'next/app'; 7 | import { Provider } from 'react-redux'; 8 | import '@bcdapps/ui/dist/style.css' 9 | import i18nConfig from '../i18n.config'; 10 | import { 11 | changeDocumentLanguage, 12 | setI18nCookie, 13 | changeDocumentDirection, 14 | } from '../utils/i18n'; 15 | import { store } from '../store'; 16 | import { ThemeProvider } from 'styled-components'; 17 | 18 | const { allLanguages, defaultLanguage } = i18nConfig; 19 | 20 | class MyApp extends App { 21 | componentDidMount(): void { 22 | const { pageProps } = this.props; 23 | 24 | const { language } = pageProps; 25 | const languageObject = allLanguages[language as string]; 26 | if (languageObject) { 27 | // 1. Change the language again in case it was a client-side 28 | // transition (_document.tsx only runs on the server) 29 | changeDocumentLanguage(languageObject.prefix); 30 | 31 | // 2. Update the "preferred-language" cookie 32 | setI18nCookie(languageObject.prefix); 33 | 34 | // 3. Change html's dir 35 | changeDocumentDirection(languageObject.direction || 'ltr'); 36 | } 37 | } 38 | 39 | public render(): React.ReactElement { 40 | const { Component, pageProps } = this.props; 41 | 42 | const { language } = pageProps; 43 | const languageObject = allLanguages[language as string] || defaultLanguage; 44 | const direction = languageObject.direction || 'ltr'; 45 | 46 | // 3. Set the dir div 47 | return ( 48 | <> 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | 57 |
58 |
59 |
60 | 61 | ); 62 | } 63 | } 64 | 65 | export default MyApp; 66 | -------------------------------------------------------------------------------- /packages/admin/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | /* eslint-disable react/jsx-props-no-spreading */ 3 | import React from 'react'; 4 | import Document, { 5 | DocumentInitialProps, 6 | Html, 7 | Head, 8 | Main, 9 | NextScript, 10 | DocumentContext, 11 | } from 'next/document'; 12 | import { ServerStyleSheet } from 'styled-components'; 13 | import { getLanguageFromURL } from '../utils/i18n'; 14 | 15 | export default class MyDocument extends Document { 16 | static async getInitialProps(ctx: DocumentContext) { 17 | const sheet = new ServerStyleSheet(); 18 | const originalRenderPage = ctx.renderPage; 19 | 20 | try { 21 | ctx.renderPage = () => 22 | originalRenderPage({ 23 | enhanceApp: (App: any) => (props: any) => 24 | sheet.collectStyles(), 25 | }); 26 | 27 | const initialProps = await Document.getInitialProps(ctx); 28 | return { 29 | ...initialProps, 30 | styles: ( 31 | <> 32 | {initialProps.styles} 33 | {sheet.getStyleElement()} 34 | 35 | ), 36 | }; 37 | } finally { 38 | sheet.seal(); 39 | } 40 | } 41 | 42 | render(): JSX.Element { 43 | // eslint-disable-next-line no-underscore-dangle 44 | const { page } = this.props.__NEXT_DATA__; 45 | const prefix = getLanguageFromURL(page); 46 | return ( 47 | 48 | 49 | 50 |
51 | 52 | 53 | 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/admin/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import Router from 'next/router'; 4 | 5 | import { GetServerSideProps } from 'next'; 6 | import i18nConfig from '../i18n.config'; 7 | import { 8 | GetI18nProps, 9 | getI18nCookieFromUnparsedCookieHeader, 10 | } from '../utils/i18n'; 11 | 12 | const { defaultLanguage } = i18nConfig; 13 | 14 | const Component: React.FC = ({ language }) => { 15 | React.useEffect(() => { 16 | Router.replace(`/${language}`); 17 | // eslint-disable-next-line react-hooks/exhaustive-deps 18 | }, []); 19 | 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export const getServerSideProps: GetServerSideProps = async ({ 28 | req, 29 | res, 30 | }) => { 31 | const preferredLanguage = getI18nCookieFromUnparsedCookieHeader( 32 | req.headers.cookie || '', 33 | ); 34 | 35 | const acceptLanguageHeader = 36 | (req.headers['accept-language'] as string | undefined) || 37 | (req.headers['Accept-Language'] as string | undefined); 38 | 39 | const acceptLanguageSub = acceptLanguageHeader 40 | ? acceptLanguageHeader.substring(0, 2) 41 | : undefined; 42 | 43 | const acceptLanguage = acceptLanguageSub === 'en' ? acceptLanguageSub : 'ar'; 44 | 45 | // 1st priority: language in cookie 46 | // 2nd priority: accept-language header 47 | // 3rd priority: default language 48 | const finalLanguage = 49 | preferredLanguage || acceptLanguage || defaultLanguage.prefix; 50 | 51 | // https://github.com/vercel/next.js/discussions/14547#discussion-7687 52 | // https://github.com/vercel/next.js/discussions/14890 53 | // https://github.com/vercel/next.js/discussions/11281 54 | if (typeof window === 'undefined') { 55 | res.statusCode = 302; 56 | res.setHeader('Location', `/${finalLanguage}`); 57 | res.end(); 58 | } 59 | 60 | return { 61 | props: { 62 | language: finalLanguage, 63 | translations: {}, 64 | }, 65 | }; 66 | }; 67 | 68 | export default Component; 69 | -------------------------------------------------------------------------------- /packages/admin/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import Router from 'next/router'; 4 | 5 | import { GetServerSideProps } from 'next'; 6 | import i18nConfig from '../i18n.config'; 7 | import { 8 | GetI18nProps, 9 | getI18nCookieFromUnparsedCookieHeader, 10 | } from '../utils/i18n'; 11 | 12 | const { defaultLanguage } = i18nConfig; 13 | 14 | const Component: React.FC = ({ language }) => { 15 | React.useEffect(() => { 16 | Router.replace(`/${language}/login`); 17 | // eslint-disable-next-line react-hooks/exhaustive-deps 18 | }, []); 19 | 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export const getServerSideProps: GetServerSideProps = async ({ 28 | req, 29 | res, 30 | }) => { 31 | const preferredLanguage = getI18nCookieFromUnparsedCookieHeader( 32 | req.headers.cookie || '', 33 | ); 34 | 35 | const acceptLanguageHeader = 36 | (req.headers['accept-language'] as string | undefined) || 37 | (req.headers['Accept-Language'] as string | undefined); 38 | 39 | const acceptLanguageSub = acceptLanguageHeader 40 | ? acceptLanguageHeader.substring(0, 2) 41 | : undefined; 42 | 43 | const acceptLanguage = acceptLanguageSub === 'en' ? acceptLanguageSub : 'ar'; 44 | 45 | // 1st priority: language in cookie 46 | // 2nd priority: accept-language header 47 | // 3rd priority: default language 48 | const finalLanguage = 49 | preferredLanguage || acceptLanguage || defaultLanguage.prefix; 50 | 51 | // https://github.com/vercel/next.js/discussions/14547#discussion-7687 52 | // https://github.com/vercel/next.js/discussions/14890 53 | // https://github.com/vercel/next.js/discussions/11281 54 | if (typeof window === 'undefined') { 55 | res.statusCode = 302; 56 | res.setHeader('Location', `/${finalLanguage}`); 57 | res.end(); 58 | } 59 | 60 | return { 61 | props: { 62 | language: finalLanguage, 63 | translations: {}, 64 | }, 65 | }; 66 | }; 67 | 68 | export default Component; 69 | -------------------------------------------------------------------------------- /packages/admin/pages/subscription_plans/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import Head from 'next/head'; 3 | import Title from '../../components/Title'; 4 | import { Main } from '../../layout/main'; 5 | 6 | import { NextPage } from 'next'; 7 | import { useDispatch } from 'react-redux'; 8 | import { getSubscriptionPlans } from 'packages/admin/modules/subscription_plans/reducer'; 9 | 10 | const Page: NextPage = () => { 11 | const dispatch = useDispatch(); 12 | useEffect(() => { 13 | dispatch(getSubscriptionPlans()); 14 | }, []); 15 | return ( 16 | <> 17 | 18 | 19 | Subscription Plans 20 | 21 |
22 | 23 | </Main> 24 | </> 25 | ); 26 | }; 27 | export default Page; 28 | -------------------------------------------------------------------------------- /packages/admin/public/images/register_bg_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asimashfaq/fullstack-starter/69bc399e482b76251a347968aae9f09c3a4abcdc/packages/admin/public/images/register_bg_2.png -------------------------------------------------------------------------------- /packages/admin/public/rosetta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asimashfaq/fullstack-starter/69bc399e482b76251a347968aae9f09c3a4abcdc/packages/admin/public/rosetta.png -------------------------------------------------------------------------------- /packages/admin/public/translations/components/DynamicTranslations/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ترجمة ديناميكية" 3 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/components/DynamicTranslations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dynamic translations!" 3 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/components/Header/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "/": "الرئيسية", 4 | "ssr": "تحميل السيرفير", 5 | "dynamic": "الديناميكية" 6 | } 7 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/components/Header/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "/": "Home", 4 | "ssr": "SSR", 5 | "dynamic": "Dynamic" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/admin/public/translations/components/RosettaImage/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "alt": "حجر رشيد", 3 | "title": "حجر رشيد" 4 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/components/RosettaImage/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "alt": "The Rosetta Stone", 3 | "title": "The Rosetta Stone" 4 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/components/SwitchButton/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "زر تحويل اللغة" 3 | } 4 | -------------------------------------------------------------------------------- /packages/admin/public/translations/components/SwitchButton/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Switch languages button" 3 | } 4 | -------------------------------------------------------------------------------- /packages/admin/public/translations/components/SwitchLink/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "رابط تحويل اللغة" 3 | } 4 | -------------------------------------------------------------------------------- /packages/admin/public/translations/components/SwitchLink/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Switch languages link" 3 | } 4 | -------------------------------------------------------------------------------- /packages/admin/public/translations/components/Title/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "language_description": "هذه الصفحة معروضة باللغة: ", 3 | "prefix_description": "اختصار اللغة هو: " 4 | } 5 | -------------------------------------------------------------------------------- /packages/admin/public/translations/components/Title/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "language_description": "This page is in: ", 3 | "prefix_description": "Language Prefix is: " 4 | } 5 | -------------------------------------------------------------------------------- /packages/admin/public/translations/pages/[language]/dynamic/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "الديناميكية" 3 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/pages/[language]/dynamic/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Dynamic" 3 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/pages/[language]/index/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "الرئيسية" 3 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/pages/[language]/index/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Home" 3 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/pages/[language]/login/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "الرئيسية" 3 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/pages/[language]/login/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sign in" 3 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/pages/[language]/ssr/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "تحميل السيرفير" 3 | } -------------------------------------------------------------------------------- /packages/admin/public/translations/pages/[language]/ssr/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "SSR" 3 | } -------------------------------------------------------------------------------- /packages/admin/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | combineReducers, 3 | configureStore, 4 | getDefaultMiddleware, 5 | } from '@reduxjs/toolkit'; 6 | import { createLogger } from 'redux-logger'; 7 | import { combineEpics, createEpicMiddleware } from 'redux-observable'; 8 | import { ActionType } from 'typesafe-actions'; 9 | import { getSubscriptionPlansEpic } from './modules/subscription_plans/epics'; 10 | import SubscriptionPlanReducer, { 11 | SubscriptionPlansActionsWithPayload, 12 | } from './modules/subscription_plans/reducer'; 13 | export const rootReducer = combineReducers({ 14 | subscription_plans: SubscriptionPlanReducer, 15 | 16 | // router: connectRouter(history), 17 | }); 18 | type ActionsWithPayloads = SubscriptionPlansActionsWithPayload; 19 | type finalActions = ActionType<ActionsWithPayloads>; 20 | 21 | export type RootState = ReturnType<typeof rootReducer>; 22 | // Configure epics 23 | const epics = combineEpics(getSubscriptionPlansEpic); 24 | const epicMiddleware = createEpicMiddleware< 25 | finalActions, // input actions 26 | finalActions, // output actions 27 | RootState 28 | >(); 29 | 30 | const Logger = createLogger(); 31 | // configure middlewares 32 | const middleware = [ 33 | ...getDefaultMiddleware(), 34 | Logger, 35 | // routerMiddleware(history), 36 | epicMiddleware, 37 | ]; 38 | 39 | function configureAppStore(initialState?: any) { 40 | // create store 41 | return configureStore({ 42 | reducer: rootReducer, 43 | middleware: middleware, 44 | preloadedState: initialState, 45 | devTools: process.env.NODE_ENV !== 'production', 46 | }); 47 | } 48 | 49 | export const store = configureAppStore(); 50 | epicMiddleware.run(epics); 51 | export type AppDispatch = typeof store.dispatch; 52 | -------------------------------------------------------------------------------- /packages/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "include": [ 5 | "**/*.ts", 6 | "**/*.tsx", 7 | "**/*.js", 8 | "**/*.jsx", 9 | ".eslintrc.js" 10 | ], 11 | "exclude": [ 12 | "node_modules", 13 | "dist", 14 | ".next", 15 | "out", 16 | ".babelrc", 17 | "bundles", 18 | "coverage" 19 | ], 20 | "compilerOptions": { 21 | "target": "es5", 22 | "module": "esnext", 23 | "lib": [ 24 | "dom", 25 | "dom.iterable", 26 | "esnext" 27 | ], 28 | "strict": true, 29 | "jsx": "preserve", 30 | "forceConsistentCasingInFileNames": true, 31 | "noEmit": true, 32 | "esModuleInterop": true, 33 | "allowSyntheticDefaultImports": true, 34 | "skipLibCheck": true, 35 | "moduleResolution": "node", 36 | "resolveJsonModule": true, 37 | "isolatedModules": true, 38 | "allowJs": true, 39 | "experimentalDecorators":true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/admin/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "**/*.spec.ts", 11 | "**/*.spec.tsx", 12 | "**/*.spec.js", 13 | "**/*.spec.jsx", 14 | "**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/admin/types/css.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | interface Content { 3 | [className: string]: string; 4 | } 5 | 6 | const content: Content; 7 | export default content; 8 | } 9 | -------------------------------------------------------------------------------- /packages/admin/types/json.ts: -------------------------------------------------------------------------------- 1 | export type AnyJson = boolean | number | string | null | JsonArray | JsonMap; 2 | export interface JsonMap { [key: string]: AnyJson; } 3 | export type JsonArray = Array<AnyJson>; 4 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "prebuild": "rimraf dist && yarn typecheck", 8 | "build": "nest build", 9 | "start": "nest start", 10 | "start:dev": "nest start --watch", 11 | "start:debug": "nest start --debug --watch", 12 | "start:prod": "node dist/main", 13 | "gen:typings": "ts-node src/generate-typings.ts", 14 | "typecheck": "tsc -p tsconfig.json" 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@bcdapps/common-backend'; 2 | import { Module } from '@nestjs/common'; 3 | import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; 4 | import { GraphQLModule } from '@nestjs/graphql'; 5 | import GraphQLJSON, { GraphQLJSONObject } from 'graphql-type-json'; 6 | import _ from 'lodash'; 7 | import { join } from 'path'; 8 | import { ConfigModule } from './config/config.module'; 9 | import { ServicesModule } from './service/service.module'; 10 | import { HttpExceptionFilter } from './shared/exception-filter/http-exception.filter'; 11 | import { TimeoutInterceptor } from './shared/interceptor/timeout.interceptor'; 12 | import { SubscriptionPlanModule } from './subscription_plans/subscription_plan.module'; 13 | @Module({ 14 | imports: [ 15 | ConfigModule, 16 | ServicesModule, 17 | SubscriptionPlanModule, 18 | GraphQLModule.forRootAsync({ 19 | imports: [ConfigModule], 20 | useFactory: (configService: ConfigService) => ({ 21 | include: [], 22 | typePaths: [join(process.cwd(), 'packages/backend/src/**/*.graphql')], 23 | installSubscriptionHandlers: true, 24 | context: ({ raw }) => ({ raw }), 25 | introspection: true, 26 | debug: configService.get('NODE_ENV') === 'development', 27 | resolverValidationOptions: { 28 | requireResolversForResolveType: false, 29 | }, 30 | resolvers: { 31 | JSON: GraphQLJSON, 32 | JSONObject: GraphQLJSONObject, 33 | }, 34 | formatError: error => { 35 | try { 36 | error.message = JSON.parse(error.message); 37 | } catch (e) { 38 | // Empty 39 | } 40 | return { 41 | ...error, 42 | message: error.message, 43 | code: _.get(error, 'extensions.exception.title', 'UNKNOWN'), 44 | locations: error.locations, 45 | path: error.path, 46 | }; 47 | }, 48 | formatResponse: response => response, 49 | }), 50 | inject: [ConfigService], 51 | }), 52 | ], 53 | 54 | controllers: [], 55 | providers: [ 56 | { 57 | provide: APP_INTERCEPTOR, 58 | useClass: TimeoutInterceptor, 59 | }, 60 | { 61 | provide: APP_FILTER, 62 | useClass: HttpExceptionFilter, 63 | }, 64 | ], 65 | }) 66 | export class AppModule {} 67 | -------------------------------------------------------------------------------- /packages/backend/src/assets/README.md: -------------------------------------------------------------------------------- 1 | No assets -------------------------------------------------------------------------------- /packages/backend/src/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@bcdapps/common-backend'; 2 | import { Module } from '@nestjs/common'; 3 | 4 | @Module({ 5 | providers: [ 6 | { 7 | provide: ConfigService, 8 | useValue: new ConfigService(), 9 | }, 10 | ], 11 | exports: [ConfigService], 12 | }) 13 | export class ConfigModule {} 14 | -------------------------------------------------------------------------------- /packages/backend/src/dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@bcdapps/common-backend'; 2 | import { 3 | INestApplicationContext, 4 | Logger, 5 | ValidationPipe, 6 | } from '@nestjs/common'; 7 | import { NestFactory } from '@nestjs/core'; 8 | import { Transport } from '@nestjs/microservices'; 9 | import { useContainer } from 'class-validator'; 10 | import cors from 'cors'; 11 | import { FlubErrorHandler } from 'nestjs-flub'; 12 | import { join } from 'path'; 13 | import { AppModule } from './app.module'; 14 | 15 | /** 16 | * Start and Stop the Application 17 | * @export 18 | * @class AppDispatcher 19 | */ 20 | export class AppDispatcher { 21 | private app: any; 22 | private logger = new Logger(AppDispatcher.name); 23 | private configService: ConfigService; 24 | 25 | /** 26 | * Trigger the server 27 | * @returns {Promise<void>} 28 | * @memberof AppDispatcher 29 | */ 30 | async dispatch(): Promise<void> { 31 | await this.createServer(); 32 | return this.startServer(); 33 | } 34 | 35 | /** 36 | * Stop the Server 37 | * @returns {Promise<void>} 38 | * @memberof AppDispatcher 39 | */ 40 | async shutdown(): Promise<void> { 41 | await this.app.close(); 42 | } 43 | 44 | /** 45 | * `AppModule` Context 46 | * @returns {Promise<INestApplicationContext>} 47 | * @memberof AppDispatcher 48 | */ 49 | // eslint-disable-next-line @typescript-eslint/tslint/config 50 | public getContext(): Promise<INestApplicationContext> { 51 | return NestFactory.createApplicationContext(AppModule); 52 | } 53 | 54 | /** 55 | * Initialize the server 56 | * @private 57 | * @returns {Promise<void>} 58 | * @memberof AppDispatcher 59 | */ 60 | private async createServer(): Promise<void> { 61 | this.app = await NestFactory.create(AppModule); 62 | useContainer(this.app.select(AppModule), { fallbackOnErrors: true }); 63 | this.app.use(cors()); 64 | this.configService = this.app.get(ConfigService); 65 | 66 | if (this.configService.get('NODE_ENV') !== 'production') { 67 | this.app.useGlobalFilters( 68 | new FlubErrorHandler({ theme: 'dark', quote: true }), 69 | ); 70 | } 71 | this.app.useGlobalPipes(new ValidationPipe()); 72 | const protoDir = join(process.cwd(), 'protos'); 73 | // # TODO: Implement secure connection 74 | // const credentials = grpc.ServerCredentials.createSsl(null, [ 75 | // { 76 | // private_key: readFileSync(join(process.cwd(), 'certs/server.key')), 77 | // cert_chain: readFileSync(join(process.cwd(), 'certs/server.crt')), 78 | // }, 79 | // ] as any); 80 | 81 | this.app.connectMicroservice({ 82 | transport: Transport.GRPC, 83 | options: { 84 | // credentials, 85 | url: '0.0.0.0:5000', 86 | package: 'rpc', 87 | protoPath: `${protoDir}/rpc/rpc.proto`, 88 | loader: { 89 | keepCase: true, 90 | longs: Number, 91 | defaults: true, 92 | oneofs: true, 93 | enums: String, 94 | arrays: true, 95 | objects: true, 96 | includeDirs: [protoDir], 97 | }, 98 | }, 99 | }); 100 | 101 | await this.app.startAllMicroservicesAsync(); 102 | } 103 | 104 | /** 105 | * Start the server 106 | * @private 107 | * @returns {Promise<void>} 108 | * @memberof AppDispatcher 109 | */ 110 | private async startServer(): Promise<void> { 111 | const host = this.configService.get('HOST'); 112 | const port = this.configService.get('PORT'); 113 | await this.app.listen(port, host); 114 | this.logger.log( 115 | `😎 Graphql Server is listening http://${host}:${port}/graphql 😎`, 116 | ); 117 | this.logger.log(`😎 Grpc Server is listening http://${host}:5000 😎`); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/backend/src/generate-typings.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-floating-promises */ 2 | import { GraphQLDefinitionsFactory } from '@nestjs/graphql'; 3 | import { join } from 'path'; 4 | 5 | const definitionsFactory = new GraphQLDefinitionsFactory(); 6 | definitionsFactory.generate({ 7 | typePaths: ['./src/**/*.graphql'], 8 | path: join(process.cwd(), 'src/graphql.schema.ts'), 9 | outputAs: 'class', 10 | }); 11 | -------------------------------------------------------------------------------- /packages/backend/src/graphql.schema.ts: -------------------------------------------------------------------------------- 1 | 2 | /** ------------------------------------------------------ 3 | * THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 4 | * ------------------------------------------------------- 5 | */ 6 | 7 | /* tslint:disable */ 8 | /* eslint-disable */ 9 | export enum SubscriptionDuration { 10 | DAY = "DAY", 11 | WEEK = "WEEK", 12 | MONTH = "MONTH", 13 | YEAR = "YEAR" 14 | } 15 | 16 | export class SubscriptionPlanFilter { 17 | id?: string; 18 | } 19 | 20 | export class SubscriptionPlanInput { 21 | name: string; 22 | code: string; 23 | description?: string; 24 | price: number; 25 | extra_fee?: number; 26 | invoice_period: number; 27 | invoice_duration: SubscriptionDuration; 28 | trail_period: number; 29 | trail_duration: SubscriptionDuration; 30 | } 31 | 32 | export class DeleteSubscriptionPlanResponse { 33 | modified?: number; 34 | edges?: SubscriptionPlan[]; 35 | } 36 | 37 | export abstract class IMutation { 38 | abstract createSubscriptionPlan(payload: SubscriptionPlanInput): SubscriptionPlan | Promise<SubscriptionPlan>; 39 | 40 | abstract updateSubscriptionPlan(payload: SubscriptionPlanInput, where: SubscriptionPlanFilter): UpdateSubscriptionPlanResponse | Promise<UpdateSubscriptionPlanResponse>; 41 | 42 | abstract DeleteSubscriptionPlanResponse(where: SubscriptionPlanFilter): DeleteSubscriptionPlanResponse | Promise<DeleteSubscriptionPlanResponse>; 43 | } 44 | 45 | export class Pagination { 46 | total?: number; 47 | has_more?: boolean; 48 | limit?: number; 49 | skip?: number; 50 | } 51 | 52 | export abstract class IQuery { 53 | abstract find_all_subscription_plans(): SubscriptionPlanResponse | Promise<SubscriptionPlanResponse>; 54 | 55 | abstract find_one_subscription_plan(where?: SubscriptionPlanFilter): SubscriptionPlan | Promise<SubscriptionPlan>; 56 | } 57 | 58 | export class SubscriptionPlan { 59 | id?: string; 60 | name?: string; 61 | code?: string; 62 | slug?: string; 63 | description?: string; 64 | price?: number; 65 | extra_fee?: number; 66 | invoice_period?: number; 67 | invoice_duration?: SubscriptionDuration; 68 | trail_period?: number; 69 | trail_duration?: SubscriptionDuration; 70 | } 71 | 72 | export class SubscriptionPlanResponse { 73 | page_info?: Pagination; 74 | edges?: SubscriptionPlan[]; 75 | } 76 | 77 | export class UpdateSubscriptionPlanResponse { 78 | modified?: number; 79 | edges?: SubscriptionPlan[]; 80 | } 81 | 82 | export type date = any; 83 | export type JSON = any; 84 | export type JSONObject = any; 85 | -------------------------------------------------------------------------------- /packages/backend/src/helpers/NestJsLogger.ts: -------------------------------------------------------------------------------- 1 | import { createEverLogger } from '@bcdapps/common-backend'; 2 | import { LoggerService } from '@nestjs/common/services/logger.service'; 3 | 4 | const log = createEverLogger({ name: 'nestjs' }); 5 | 6 | export class EverbieNestJSLogger implements LoggerService { 7 | log(message: string) { 8 | log.info(message); 9 | } 10 | 11 | error(message: string, trace: string) { 12 | log.error(`Message: ${message}. Trace: ${trace}`); 13 | } 14 | 15 | warn(message: string) { 16 | log.warn(message); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/backend/src/main.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-empty */ 2 | /* eslint-disable @typescript-eslint/restrict-plus-operands */ 3 | import { createEverLogger } from '@bcdapps/common-backend'; 4 | import exitHook from 'async-exit-hook'; 5 | import 'dotenv/config'; 6 | import 'reflect-metadata'; 7 | import { AppDispatcher } from './dispatcher'; 8 | 9 | const log = createEverLogger({ name: 'uncaught' }); 10 | 11 | const dispatcher = new AppDispatcher(); 12 | 13 | dispatcher 14 | .dispatch() 15 | .then(() => log.info('Everything up running')) 16 | .catch(e => { 17 | log.error(e.message, e.stack); 18 | process.exit(1); 19 | }); 20 | 21 | exitHook(callback => { 22 | void dispatcher.shutdown().then(() => { 23 | log.info('Graceful shutdown the server'); 24 | callback(); 25 | }); 26 | }); 27 | process.on('uncaughtException', err => { 28 | try { 29 | log.error(err, `Caught exception: ${err}`); 30 | } catch (logWritingErr) { 31 | try { 32 | console.error("Can't write to log!!!!!!"); 33 | console.error(logWritingErr); 34 | } catch (consoleWritingError) {} 35 | } 36 | 37 | console.error(err); 38 | }); 39 | 40 | process.on('unhandledRejection', err => { 41 | try { 42 | log.error(err, `Uncaught rejection: ${err}`); 43 | } catch (logWritingErr) { 44 | try { 45 | console.error("Can't write to log!!!!!!"); 46 | console.error(logWritingErr); 47 | } catch (consoleWritingError) {} 48 | } 49 | 50 | console.error(err); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/backend/src/service/inversify.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/tslint/config */ 2 | import { configBinding, ServiceSymbol } from '@bcdapps/common-backend'; 3 | import { bindingsSubscription } from '@bcdapps/subscription'; 4 | import { Container, ContainerModule, interfaces } from 'inversify'; 5 | import _ from 'lodash'; 6 | import 'reflect-metadata'; 7 | import { ServicesApp } from './services.app'; 8 | 9 | const bindings = new ContainerModule((bind: interfaces.Bind) => { 10 | _.each([], (Service: any) => { 11 | bind(Service) 12 | .to(Service) 13 | .inSingletonScope(); 14 | 15 | bind<any>(ServiceSymbol).toFactory<any>(context => 16 | context.container.get<any>(Service), 17 | ); 18 | }); 19 | bind<ServicesApp>(ServicesApp) 20 | .toSelf() 21 | .inSingletonScope(); 22 | }); 23 | 24 | const container = new Container(); 25 | container.load(configBinding); 26 | container.load(bindings); 27 | container.load(bindingsSubscription); 28 | export const servicesContainer = container; 29 | -------------------------------------------------------------------------------- /packages/backend/src/service/service.module.ts: -------------------------------------------------------------------------------- 1 | import { ServiceSymbol } from '@bcdapps/common-backend'; 2 | import { Global, Module } from '@nestjs/common'; 3 | import { servicesContainer } from './inversify.config'; 4 | 5 | const getServices = () => 6 | servicesContainer.getAll(ServiceSymbol).map(service => ({ 7 | provide: service.constructor, 8 | useValue: service, 9 | })); 10 | 11 | const services = getServices(); 12 | 13 | @Global() 14 | @Module({ 15 | providers: services, 16 | exports: services, 17 | }) 18 | export class ServicesModule { 19 | constructor() { 20 | // Empty 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/backend/src/service/services.app.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | 3 | @injectable() 4 | export class ServicesApp {} 5 | -------------------------------------------------------------------------------- /packages/backend/src/shared/base.graphql: -------------------------------------------------------------------------------- 1 | scalar date 2 | 3 | scalar Date 4 | 5 | type Pagination { 6 | total: Int 7 | has_more: Boolean 8 | limit: Int 9 | skip: Int 10 | } 11 | 12 | scalar JSON 13 | scalar JSONObject 14 | schema { 15 | query: Query 16 | mutation: Mutation 17 | } 18 | -------------------------------------------------------------------------------- /packages/backend/src/shared/exception-filter/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | ExceptionFilter, 5 | HttpException, 6 | } from '@nestjs/common'; 7 | 8 | @Catch(HttpException) 9 | export class HttpExceptionFilter implements ExceptionFilter { 10 | catch(exception: HttpException, host: ArgumentsHost): void { 11 | const ctx = host.switchToHttp(); 12 | const response = ctx.getResponse(); 13 | const request = ctx.getRequest(); 14 | const status = exception.getStatus(); 15 | const message = exception.message; 16 | response.code(status).view({ 17 | message, 18 | statusCode: status, 19 | timestamp: new Date().toISOString(), 20 | path: request.url, 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/backend/src/shared/interceptor/exception.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | HttpException, 5 | Injectable, 6 | NestInterceptor, 7 | } from '@nestjs/common'; 8 | import { Observable, throwError } from 'rxjs'; 9 | import { catchError } from 'rxjs/operators'; 10 | 11 | @Injectable() 12 | export class ExceptionInterceptor implements NestInterceptor { 13 | intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 14 | return next 15 | .handle() 16 | .pipe( 17 | catchError((err) => throwError(new HttpException(err, err.status))), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/backend/src/shared/interceptor/logging.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { createEverLogger } from '@bcdapps/common-backend'; 2 | import { 3 | CallHandler, 4 | ExecutionContext, 5 | Injectable, 6 | NestInterceptor, 7 | } from '@nestjs/common'; 8 | import { Observable } from 'rxjs'; 9 | import { tap } from 'rxjs/operators'; 10 | 11 | @Injectable() 12 | export class LoggingInterceptor implements NestInterceptor { 13 | constructor() { 14 | // Empty 15 | } 16 | private logger = createEverLogger({ name: 'LoggingInterceptor' }); 17 | 18 | intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 19 | this.logger.info('Before...'); 20 | 21 | const now = Date.now(); 22 | return next 23 | .handle() 24 | .pipe(tap(() => this.logger.info(`After... ${Date.now() - now}ms`))); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/backend/src/shared/interceptor/timeout.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { Observable } from 'rxjs'; 8 | import { timeout } from 'rxjs/operators'; 9 | 10 | @Injectable() 11 | export class TimeoutInterceptor implements NestInterceptor { 12 | intercept(context: ExecutionContext, next: CallHandler): Observable<any> { 13 | return next.handle().pipe(timeout(5000)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/backend/src/subscription_plans/subcription_plan.graphql: -------------------------------------------------------------------------------- 1 | enum SubscriptionDuration{ 2 | DAY 3 | WEEK 4 | MONTH 5 | YEAR 6 | } 7 | input SubscriptionPlanInput { 8 | name: String! 9 | code: String! 10 | description: String 11 | price: Int! 12 | extra_fee: Int 13 | invoice_period: Int! 14 | invoice_duration: SubscriptionDuration! 15 | trail_period: Int! 16 | trail_duration: SubscriptionDuration! 17 | } 18 | type SubscriptionPlan { 19 | id: String 20 | name: String 21 | code: String 22 | slug: String 23 | description: String 24 | price: Int 25 | extra_fee: Int 26 | invoice_period: Int 27 | invoice_duration: SubscriptionDuration 28 | trail_period: Int 29 | trail_duration: SubscriptionDuration 30 | } 31 | type DeleteSubscriptionPlanResponse { 32 | modified: Int 33 | edges: [SubscriptionPlan] 34 | } 35 | input SubscriptionPlanFilter { 36 | id: String 37 | } 38 | 39 | type UpdateSubscriptionPlanResponse { 40 | modified: Int 41 | edges: [SubscriptionPlan] 42 | } 43 | type SubscriptionPlanResponse { 44 | page_info: Pagination 45 | edges: [SubscriptionPlan] 46 | 47 | } 48 | 49 | type Query { 50 | find_all_subscription_plans: SubscriptionPlanResponse 51 | find_one_subscription_plan(where: SubscriptionPlanFilter): SubscriptionPlan 52 | } 53 | type Mutation{ 54 | createSubscriptionPlan(payload:SubscriptionPlanInput!) : SubscriptionPlan 55 | updateSubscriptionPlan( payload: SubscriptionPlanInput!,where: SubscriptionPlanFilter!): UpdateSubscriptionPlanResponse 56 | DeleteSubscriptionPlanResponse(where: SubscriptionPlanFilter!):DeleteSubscriptionPlanResponse 57 | } -------------------------------------------------------------------------------- /packages/backend/src/subscription_plans/subscription_plan.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SubscriptionPlanController } from './subscription_plan.controller'; 3 | import { SubscriptionPlanResolver } from './subscription_plan.resolver'; 4 | 5 | @Module({ 6 | controllers: [SubscriptionPlanController], 7 | providers: [SubscriptionPlanResolver], 8 | exports: [], 9 | }) 10 | export class SubscriptionPlanModule {} 11 | -------------------------------------------------------------------------------- /packages/backend/src/subscription_plans/subscription_plan.resolver.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionPlanService } from '@bcdapps/subscription'; 2 | import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; 3 | import { inject, LazyServiceIdentifer } from 'inversify'; 4 | import { 5 | DeleteSubscriptionPlanResponse, 6 | SubscriptionPlan, 7 | SubscriptionPlanFilter, 8 | SubscriptionPlanInput, 9 | SubscriptionPlanResponse, 10 | UpdateSubscriptionPlanResponse, 11 | } from '../graphql.schema'; 12 | 13 | @Resolver('SubscriptionPlan') 14 | export class SubscriptionPlanResolver { 15 | constructor( 16 | @inject(new LazyServiceIdentifer(() => SubscriptionPlanService)) 17 | private readonly subscriptionPlanService: SubscriptionPlanService, 18 | ) {} 19 | @Query('find_all_subscription_plans') 20 | async find_all_subscription_plans(): Promise<SubscriptionPlanResponse> { 21 | try { 22 | const obj = await this.subscriptionPlanService.findAll(); 23 | return (obj as unknown) as SubscriptionPlanResponse; 24 | } catch (error) { 25 | return error; 26 | } 27 | } 28 | @Query('find_one_subscription_plan') 29 | async find_one_subscription_plan( 30 | @Args('where') where?: SubscriptionPlanFilter, 31 | ): Promise<SubscriptionPlan> { 32 | try { 33 | const id = where?.id; 34 | const obj = await this.subscriptionPlanService.findOne({ id }); 35 | return (obj as unknown) as SubscriptionPlan; 36 | } catch (error) { 37 | return error; 38 | } 39 | } 40 | @Mutation('createSubscriptionPlan') 41 | async createSubscriptionPlan( 42 | @Args('payload') payload: SubscriptionPlanInput, 43 | ): Promise<SubscriptionPlan> { 44 | try { 45 | const obj = await this.subscriptionPlanService.create({ 46 | name: payload?.name, 47 | price: payload?.price, 48 | invoice_duration: payload?.invoice_duration as any, 49 | invoice_period: payload?.invoice_period, 50 | trail_period: payload?.trail_period, 51 | trail_duration: payload?.trail_duration as any, 52 | description: payload?.description, 53 | code: payload?.code, 54 | }); 55 | return (obj as unknown) as SubscriptionPlan; 56 | } catch (error) { 57 | return error; 58 | } 59 | } 60 | @Mutation('updateSubscriptionPlan') 61 | async updateSubscriptionPlan( 62 | @Args('payload') payload: SubscriptionPlanInput, 63 | @Args('where') where?: SubscriptionPlanFilter, 64 | ): Promise<UpdateSubscriptionPlanResponse> { 65 | try { 66 | const obj = await this.subscriptionPlanService.update(payload, where); 67 | return (obj as unknown) as UpdateSubscriptionPlanResponse; 68 | } catch (error) { 69 | return error; 70 | } 71 | } 72 | @Mutation('DeleteSubscriptionPlanResponse') 73 | async DeleteSubscriptionPlanResponse( 74 | @Args('where') where?: SubscriptionPlanFilter, 75 | ): Promise<DeleteSubscriptionPlanResponse> { 76 | try { 77 | const id = where?.id; 78 | const obj = await this.subscriptionPlanService.delete({ id }); 79 | return (obj as unknown) as DeleteSubscriptionPlanResponse; 80 | } catch (error) { 81 | return error; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/backend/tools/gen.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | cd $(dirname $0)/../ 4 | 5 | PROTOC=./node_modules/grpc-tools/bin/protoc 6 | SRC_DIR=./protos 7 | DEST_DIR=./src/codegen 8 | 9 | # #generate js codes via grpc-tools 10 | # ./node_modules/.bin/grpc_tools_node_protoc \ 11 | # --proto_path=${SRC_DIR} \ 12 | # --js_out=import_style=commonjs,binary:${DEST_DIR} \ 13 | # --grpc_out=${DEST_DIR} \ 14 | # --plugin=protoc-gen-grpc=./node_modules/.bin/grpc_tools_node_protoc_plugin \ 15 | # ${SRC_DIR}/**/*.proto 16 | 17 | # # generate d.ts codes 18 | # ${PROTOC} \ 19 | # --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \ 20 | # --ts_out=${DEST_DIR} \ 21 | # -I ${SRC_DIR} \ 22 | # ${SRC_DIR}/**/*.proto 23 | 24 | # protobuf.js 25 | node_modules/.bin/pbjs \ 26 | --target static-module \ 27 | --wrap commonjs \ 28 | --keep-case \ 29 | --path ${SRC_DIR} \ 30 | --out ${DEST_DIR}/rpc.js \ 31 | ${SRC_DIR}/**/*.proto 32 | 33 | node_modules/.bin/pbts \ 34 | --out ${DEST_DIR}/rpc.d.ts \ 35 | ${DEST_DIR}/rpc.js 36 | -------------------------------------------------------------------------------- /packages/backend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"], 6 | "emitDecoratorMetadata": true, 7 | "target": "es2015" 8 | }, 9 | "exclude": ["**/*.spec.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | } 4 | -------------------------------------------------------------------------------- /packages/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"], 5 | "rootDir": ".", 6 | "strict":false, 7 | "emitDecoratorMetadata": true, 8 | "allowSyntheticDefaultImports":true 9 | }, 10 | 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/backend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/common-backend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | modules: 'commonjs', 10 | }, 11 | ], 12 | '@babel/typescript', 13 | ], 14 | 15 | plugins: [ 16 | 'const-enum', 17 | '@babel/transform-typescript', 18 | ['@babel/plugin-proposal-decorators', { legacy: true }], 19 | ['@babel/plugin-proposal-class-properties', { loose: true }], 20 | '@babel/proposal-object-rest-spread', 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /packages/common-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bcdapps/common-backend", 3 | "version": "1.0.1", 4 | "main": "dist/index.js", 5 | "module": "dist/common-backend.esm.js", 6 | "typings": "dist/index.d.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "prebuild": "rm -rf dist && yarn typecheck", 13 | "build": "concurrently yarn:build:*", 14 | "build:cjs": "cross-env BABEL_ENV=cjs babel src --root-mode upward --extensions .ts -d dist/cjs --source-maps --copy-files", 15 | "build:esm": "cross-env BABEL_ENV=esm babel src --root-mode upward --extensions .ts,.tsx -d dist/esm --source-maps --copy-files", 16 | "build:types": "tsc --emitDeclarationOnly --declaration --declarationDir dist/types", 17 | "typecheck": "tsc -p tsconfig.json" 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/common-backend/src/IService.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 2 | export interface IService {} 3 | 4 | export const ServiceSymbol = Symbol('Service'); 5 | -------------------------------------------------------------------------------- /packages/common-backend/src/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { Env, env, Environments } from '../env'; 3 | 4 | @injectable() 5 | export class ConfigService { 6 | /** 7 | * Get the config setting. 8 | * In many cases, it get's environment variables by 'key' from '.env' file 9 | * @param key This is settings name or the environemnt variable (HTTPPORT, HTTPSPORT...) etc. 10 | * @returns Returns a value for the given settings key 11 | */ 12 | get(key: string): string | number | boolean | Environments { 13 | return env[key]; 14 | } 15 | 16 | /** 17 | * Get All Env settings 18 | */ 19 | // eslint-disable-next-line @typescript-eslint/tslint/config 20 | get Env(): Env { 21 | return env; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/common-backend/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config.service'; 2 | -------------------------------------------------------------------------------- /packages/common-backend/src/database/database_service.abstract.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | /** 4 | * Base Repository to implement database service 5 | * Add any database by implementing IDatabaseRepo 6 | * @export 7 | * @abstract 8 | * @class IDatabaseRepo 9 | */ 10 | export abstract class IDatabaseRepo { 11 | /** 12 | * Return Database connection instance 13 | * 14 | * @returns {Promise<any>} 15 | * @memberof IDatabaseRepo 16 | */ 17 | connectDB(): Promise<any> { 18 | throw new Error('Method not implemented.'); 19 | } 20 | /** 21 | * Insert / Create Record 22 | * 23 | * @template T Model 24 | * @template P Type for creating record 25 | * @param {P} payload 26 | * @returns {Promise<T>} 27 | * @memberof IDatabaseRepo 28 | */ 29 | create<T, P>(payload: P): Promise<T> { 30 | throw new Error('Method not implemented.'); 31 | } 32 | /** 33 | * Find One Record 34 | * 35 | * @template T Model 36 | * @template F Type of Filter data 37 | * @param {F} where 38 | * @returns {Promise<T>} 39 | * @memberof IDatabaseRepo 40 | */ 41 | findOne<T, F>(where: F): Promise<T> { 42 | throw new Error('Method not implemented.'); 43 | } 44 | /** 45 | * Find All Record 46 | * 47 | * @template T Model 48 | * @template F Type of Filter data 49 | * @template F 50 | * @param {F} [where] 51 | * @param {number} [limit] 52 | * @param {number} [skip] 53 | * @returns {Promise<T[]>} 54 | * @memberof IDatabaseRepo 55 | */ 56 | findAll<T, F>( 57 | where?: F, 58 | limit?: number, 59 | skip?: number, 60 | ): Promise<[T[], number]> { 61 | throw new Error('Method not implemented.'); 62 | } 63 | /** 64 | * Count the record 65 | * 66 | * @template F Type of Filter data 67 | * @param {F} [where] 68 | * @returns {Promise<number>} 69 | * @memberof IDatabaseRepo 70 | */ 71 | count<F>(where?: F): Promise<number> { 72 | throw new Error('Method not implemented.'); 73 | } 74 | /** 75 | * Update Record 76 | * 77 | * @template F Type of Filter data 78 | * @template U Type of Update payload 79 | * @template T Model 80 | * @param {U} payload 81 | * @param {F} where 82 | * @returns {Promise<T[]>} 83 | * @memberof IDatabaseRepo 84 | */ 85 | update<F, U, T>(payload: U, where: F): Promise<[T[], number]> { 86 | throw new Error('Method not implemented.'); 87 | } 88 | 89 | /** 90 | * Delete Record 91 | * 92 | * @template T Model 93 | * @template F Type of Filter data 94 | * @param {F} where 95 | * @returns {(Promise<[T | T[], number]>)} 96 | * @memberof IDatabaseRepo 97 | */ 98 | delete<T, F>(where: F): Promise<[T[], number]> { 99 | throw new Error('Method not implemented.'); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/common-backend/src/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './database_service.abstract'; 2 | -------------------------------------------------------------------------------- /packages/common-backend/src/env.ts: -------------------------------------------------------------------------------- 1 | import { cleanEnv, host, port, str } from 'envalid'; 2 | 3 | export type Environments = 'production' | 'development' | 'test'; 4 | 5 | export type Env = Readonly<{ 6 | NODE_ENV: Environments; 7 | LOGS_PATH: string; 8 | DB_URI: string; 9 | DB_NAME: string; 10 | DB_CERTS: string; 11 | LOG_LEVEL?: string; 12 | HOST: string; 13 | PORT: number; 14 | }>; 15 | 16 | export const env: Env = cleanEnv( 17 | process.env, 18 | { 19 | NODE_ENV: str({ 20 | choices: ['production', 'development', 'test'], 21 | default: 'development', 22 | }), 23 | 24 | LOGS_PATH: str({ default: './tmp/logs' }), 25 | 26 | DB_URI: str({ default: '' }), 27 | DB_NAME: str({ default: '' }), 28 | DB_CERTS: str({ 29 | default: '', 30 | }), 31 | HOST: host({ default: 'localhost' }), 32 | PORT: port({ default: 3000 }), 33 | LOG_LEVEL: str({ 34 | choices: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'], 35 | default: 'debug', 36 | }), 37 | }, 38 | { strict: true, dotEnvPath: `${__dirname}/../../../../.env` }, 39 | ); 40 | -------------------------------------------------------------------------------- /packages/common-backend/src/errors.generator.ts: -------------------------------------------------------------------------------- 1 | export const ErrorGenerator = { 2 | /** 3 | * Return Error message 4 | * `key`: must be at least `length` characters 5 | * @template T Model 6 | * @param {keyof T} key Model Key 7 | * @param {number} length 8 | * @returns {string} 9 | */ 10 | MiniLength: <T>(key: keyof T, length: number): string => 11 | `${key.toString()}: must be at least ${length} characters`, 12 | 13 | /** 14 | * Return Error message 15 | * `key`: must not be longer than `length` characters 16 | * @template T Model 17 | * @param {keyof T} key Model Key 18 | * @param {number} length 19 | * @returns {string} 20 | */ 21 | MaxLength: <T>(key: keyof T, length: number): string => 22 | `${key.toString()}: must not be longer than ${length} characters`, 23 | 24 | /** 25 | * Return Error message 26 | * `key`: is required 27 | * @template T Model 28 | * @param {keyof T} key Model Key 29 | * @returns {string} 30 | */ 31 | Required: <T>(key: keyof T): string => `${key.toString()}: is required`, 32 | /** 33 | * Return Error message 34 | * `key`: must be greater than or equal to `value` 35 | * @template T Model 36 | * @param {keyof T} key Model Key 37 | * @param {number} value 38 | * @returns {string} 39 | */ 40 | MinValue: <T>(key: keyof T, value: number): string => 41 | `${key.toString()}: must be greater than or equal to ${value}`, 42 | /** 43 | * Return Error message 44 | * `key`: must be less than or equal to `value` 45 | * @template T Model 46 | * @param {keyof T} key Model Key 47 | * @param {number} value 48 | * @returns {string} 49 | */ 50 | MaxValue: <T>(key: keyof T, value: number): string => 51 | `${key.toString()}: must be less than or equal to ${value}`, 52 | /** 53 | * Return Error message 54 | * No `modelName` found 55 | * @param {string} modelName 56 | * @returns {string} 57 | */ 58 | NotFound: (modelName: string): string => `No ${modelName} found`, 59 | /** 60 | * Return Error message 61 | * Unable to save `modelName` 62 | * @param {string} modelName 63 | * @returns {string} 64 | */ 65 | UnableSave: (modelName: string): string => `Unable to save ${modelName}`, 66 | /** 67 | * Return Error message 68 | * `key` already exists 69 | * @template T Model 70 | * @param {keyof T} key Model Key 71 | * @returns {string} 72 | */ 73 | Duplicate: <T>(key: keyof T): string => `${key.toString()}: already exists`, 74 | 75 | /** 76 | * Return Error message 77 | * Unable to delete `key` 78 | * @template T Model 79 | * @param {keyof T} key Model Key 80 | * @returns {string} 81 | */ 82 | UnableToDelete: <T>(key: keyof T): string => 83 | `Unable to delete ${key.toString()}`, 84 | }; 85 | -------------------------------------------------------------------------------- /packages/common-backend/src/errors.messages.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from 'http-json-errors'; 2 | import _ from 'lodash'; 3 | 4 | const enum ErrorCode { 5 | NO_RECORD = 'NO_RECORD', 6 | MISSING_REQUIRED_FIELDS = 'MISSING_REQUIRED_FIELDS', 7 | NOT_FOUND_ERROR = 'NOT_FOUND_ERROR', 8 | FORBIDDEN_ERROR = 'FORBIDDEN_ERROR', 9 | BAD_REQUEST_ERROR = 'BAD_REQUEST_ERROR', 10 | PAYLOAD_ERROR = 'PAYLOAD_ERROR', 11 | CONFLICT_ERROR = 'CONFLICT_ERROR', 12 | NOT_IMPLEMENTED_ERROR = 'NOT_IMPLEMENTED_ERROR', 13 | PRE_CONDITION_FAILED_ERROR = 'PRE_CONDITION_FAILED_ERROR', 14 | } 15 | 16 | export const PerConditionFailed = (message = ''): void => { 17 | throw new HttpError(412, { 18 | message, 19 | title: ErrorCode.PRE_CONDITION_FAILED_ERROR, 20 | body: { 21 | error_code: ErrorCode.PRE_CONDITION_FAILED_ERROR, 22 | error_text: message, 23 | }, 24 | }); 25 | }; 26 | export const NoRecordError = (message = ''): void => { 27 | throw new HttpError(404, { 28 | message, 29 | title: ErrorCode.NO_RECORD, 30 | body: { 31 | error_code: ErrorCode.NO_RECORD, 32 | error_text: message, 33 | }, 34 | }); 35 | }; 36 | 37 | export const NotFoundError = (message = ''): void => { 38 | throw new HttpError(404, { 39 | message, 40 | title: ErrorCode.NOT_FOUND_ERROR, 41 | body: { 42 | error_code: ErrorCode.NOT_FOUND_ERROR, 43 | error_text: message, 44 | }, 45 | }); 46 | }; 47 | 48 | export const ForbiddenError = (message = ''): void => { 49 | throw new HttpError(403, { 50 | message, 51 | title: ErrorCode.FORBIDDEN_ERROR, 52 | body: { 53 | error_code: ErrorCode.FORBIDDEN_ERROR, 54 | error_text: message, 55 | }, 56 | }); 57 | }; 58 | 59 | export const MissingRequiredFieldsError = (message = ''): void => { 60 | throw new HttpError(400, { 61 | message, 62 | title: ErrorCode.MISSING_REQUIRED_FIELDS, 63 | body: { 64 | error_code: ErrorCode.MISSING_REQUIRED_FIELDS, 65 | error_text: message, 66 | }, 67 | }); 68 | }; 69 | 70 | export const BadRequestError = (message = ''): void => { 71 | throw new HttpError(400, { 72 | message, 73 | title: ErrorCode.BAD_REQUEST_ERROR, 74 | body: { 75 | error_code: ErrorCode.BAD_REQUEST_ERROR, 76 | error_text: message, 77 | }, 78 | }); 79 | }; 80 | 81 | export const NotImplementedError = (message = ''): void => { 82 | throw new HttpError(400, { 83 | message, 84 | title: ErrorCode.NOT_IMPLEMENTED_ERROR, 85 | body: { 86 | error_code: ErrorCode.NOT_IMPLEMENTED_ERROR, 87 | error_text: message, 88 | }, 89 | }); 90 | }; 91 | 92 | export const PayloadError = (message = ''): void => { 93 | throw new HttpError(412, { 94 | message, 95 | title: ErrorCode.PAYLOAD_ERROR, 96 | body: { 97 | error_code: ErrorCode.PAYLOAD_ERROR, 98 | error_text: message, 99 | }, 100 | }); 101 | }; 102 | 103 | export const ConflictError = (message = ''): void => { 104 | throw new HttpError(409, { 105 | message, 106 | title: ErrorCode.CONFLICT_ERROR, 107 | body: { 108 | error_code: ErrorCode.CONFLICT_ERROR, 109 | error_text: message, 110 | }, 111 | }); 112 | }; 113 | 114 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 115 | export const ParseError = (e: any, message: string): void => { 116 | let errorMessage = e; 117 | 118 | if ( 119 | errorMessage?.title === 'NOT_FOUND_ERROR' || 120 | errorMessage?.title === 'CONFLICT_ERROR' 121 | ) { 122 | throw e; 123 | } 124 | 125 | try { 126 | if (e.name.trim() === 'Error') { 127 | errorMessage = JSON.parse(e?.message); 128 | } 129 | } catch (__) { 130 | errorMessage = e?.message; 131 | } 132 | 133 | if (errorMessage?.name === 'ValidationError') { 134 | if (!_.isEmpty(e?.inner)) { 135 | throw PayloadError( 136 | JSON.stringify( 137 | errorMessage.inner.map((validationError) => ({ 138 | message: validationError.message, 139 | field: validationError.path, 140 | })), 141 | ), 142 | ); 143 | } 144 | throw PayloadError(e?.message); 145 | } 146 | }; 147 | -------------------------------------------------------------------------------- /packages/common-backend/src/helpers/Log.ts: -------------------------------------------------------------------------------- 1 | import Logger from 'bunyan'; 2 | import PrettyStream from 'bunyan-prettystream'; 3 | import { existsSync } from 'fs'; 4 | import mkdirp from 'mkdirp'; 5 | import { env } from '../env'; 6 | 7 | export interface ILogArgs { 8 | // which file used to store logs 9 | name: string; 10 | } 11 | 12 | let isLogsFolderExists = env.LOGS_PATH ? existsSync(env.LOGS_PATH) : false; 13 | 14 | const prettyStdOut = new PrettyStream(); 15 | 16 | prettyStdOut.pipe(process.stdout); 17 | 18 | export function createEverLogger({ name }: ILogArgs): Logger { 19 | if (!isLogsFolderExists) { 20 | mkdirp.sync(env.LOGS_PATH); 21 | isLogsFolderExists = true; 22 | } 23 | 24 | const logger = Logger.createLogger({ 25 | name: `${name}`, 26 | serializers: Logger.stdSerializers, 27 | streams: [ 28 | { 29 | level: 'info', 30 | path: `${env.LOGS_PATH}/info_${name}.log`, 31 | }, 32 | { 33 | level: 'error', 34 | path: `${env.LOGS_PATH}/error_${name}.log`, 35 | }, 36 | { 37 | level: 'debug', 38 | path: `${env.LOGS_PATH}/debug_${name}.log`, 39 | }, 40 | { 41 | level: 'debug', 42 | type: 'raw', 43 | stream: prettyStdOut, 44 | }, 45 | ], 46 | }); 47 | 48 | if (env.LOG_LEVEL) { 49 | logger.level(Logger[env.LOG_LEVEL.toUpperCase()]); 50 | } 51 | 52 | return logger; 53 | } 54 | 55 | export const Log = (logArgs: ILogArgs): ClassDecorator => target => { 56 | target.prototype.logName = logArgs.name; 57 | target.prototype.log = createEverLogger(logArgs); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/common-backend/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Log'; 2 | -------------------------------------------------------------------------------- /packages/common-backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | export * from './config'; 4 | export * from './database'; 5 | export * from './env'; 6 | export * from './errors.generator'; 7 | export * from './errors.messages'; 8 | export * from './helpers'; 9 | export * from './interfaces'; 10 | export * from './inversify.config'; 11 | export * from './IService'; 12 | export * from './services'; 13 | export * from './types'; 14 | -------------------------------------------------------------------------------- /packages/common-backend/src/interfaces/common.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IResponseInfo<T> { 2 | page_info: IPagination; 3 | edges: T[]; 4 | } 5 | export interface IPagination { 6 | has_more: boolean; 7 | total: number; 8 | limit: number; 9 | skip: number; 10 | } 11 | 12 | export interface IChangeResponseInfo<T> { 13 | edges: T[]; 14 | modified: number; 15 | } 16 | -------------------------------------------------------------------------------- /packages/common-backend/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common.interfaces'; 2 | -------------------------------------------------------------------------------- /packages/common-backend/src/inversify.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/tslint/config */ 2 | import { ContainerModule, interfaces } from 'inversify'; 3 | import _ from 'lodash'; 4 | import 'reflect-metadata'; 5 | import { ConfigService } from './config/config.service'; 6 | import { ServiceSymbol } from './IService'; 7 | 8 | export const configBinding = new ContainerModule((bind: interfaces.Bind) => { 9 | _.each([ConfigService], (Service: any) => { 10 | bind(Service) 11 | .to(Service) 12 | .inSingletonScope(); 13 | 14 | bind<any>(ServiceSymbol).toFactory<any>(context => 15 | context.container.get<any>(Service), 16 | ); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/common-backend/src/services/base.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IChangeResponseInfo, 3 | IResponseInfo, 4 | } from '../interfaces/common.interfaces'; 5 | 6 | /** 7 | * Base Service for the CRUD operations 8 | * 9 | * @export 10 | * @interface IBaseService 11 | * @template T Model 12 | * @template P Type to accept for creating record 13 | * @template F Type to accept for filter 14 | * @template U Type to accept for update 15 | */ 16 | export interface IBaseService<T, P, F, U> { 17 | create(payload: P): Promise<T>; 18 | findOne(where: F): Promise<T>; 19 | findAll(where?: F): Promise<IResponseInfo<T>>; 20 | count(where?: F): Promise<number>; 21 | update(payload: U, where: F): Promise<IChangeResponseInfo<T>>; 22 | delete(where: F): Promise<IChangeResponseInfo<T>>; 23 | } 24 | -------------------------------------------------------------------------------- /packages/common-backend/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.service'; 2 | -------------------------------------------------------------------------------- /packages/common-backend/src/types/bulk.types.ts: -------------------------------------------------------------------------------- 1 | export type BulKTransform<T> = { 2 | [P in keyof T]: { [P in keyof T]: T[P][] }; 3 | }[keyof T]; 4 | -------------------------------------------------------------------------------- /packages/common-backend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bulk.types'; 2 | export * from './sort.types'; 3 | -------------------------------------------------------------------------------- /packages/common-backend/src/types/sort.types.ts: -------------------------------------------------------------------------------- 1 | export enum SortDirectionEnum { 2 | ASC = 'ASC', 3 | DESC = 'DESC', 4 | } 5 | export type SortDirection = keyof typeof SortDirectionEnum; 6 | export type SortProperties<T extends string> = { 7 | [P in T]?: SortDirection; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/common-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitThis": false, 5 | "target": "es2017", 6 | "baseUrl": "src", 7 | "declaration":true , 8 | "declarationMap": true, 9 | "outDir": "dist", 10 | "strict": true, 11 | "noEmit": false, 12 | "experimentalDecorators": true 13 | }, 14 | "include": ["src"], 15 | "references": [ 16 | { 17 | "path": "./tsconfig.lib.json" 18 | }, 19 | { 20 | "path": "./tsconfig.spec.json" 21 | } 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /packages/common-backend/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "outDir": "dist", 6 | "baseUrl": "src", 7 | "rootDir": ".", 8 | "declaration": true, 9 | "allowSyntheticDefaultImports":true, 10 | "types": ["node"] 11 | }, 12 | "exclude": ["**/*.spec.ts","node_modules","dist"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/common-backend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/subscription/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | modules: 'commonjs', 10 | }, 11 | ], 12 | '@babel/typescript', 13 | ], 14 | 15 | plugins: [ 16 | ['@babel/plugin-proposal-decorators', { legacy: true }], 17 | ['@babel/plugin-proposal-class-properties', { loose: true }], 18 | '@babel/proposal-object-rest-spread', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /packages/subscription/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bcdapps/subscription", 3 | "version": "1.0.1", 4 | "main": "dist/index.js", 5 | "module": "dist/subscription.esm.js", 6 | "typings": "dist/index.d.ts", 7 | "license": "MIT", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "prebuild": "rm -rf dist && yarn typecheck", 13 | "build": "concurrently yarn:build:*", 14 | "build:cjs": "cross-env BABEL_ENV=cjs babel src --root-mode upward --extensions .ts,.tsx -d dist/cjs --plugins=inline-react-svg --source-maps --copy-files", 15 | "build:esm": "cross-env BABEL_ENV=esm babel src --root-mode upward --extensions .ts,.tsx -d dist/esm --source-maps --copy-files", 16 | "build:types": "tsc --emitDeclarationOnly --declaration --declarationDir dist/types", 17 | "typecheck": "tsc -p tsconfig.json" 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/subscription/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './inversify.config'; 2 | export * from './subscription_plan.service'; 3 | -------------------------------------------------------------------------------- /packages/subscription/src/interfaces/subscription_plan.interface.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionDuration } from '../types/subscription_plan.types'; 2 | 3 | /** 4 | * Subscription Plan Model 5 | * Manage Subscription plans for your service 6 | * Define its duration for both invoice and trail period 7 | * 8 | * #TODO: Implement the Grace Period 9 | * Implement the Multiple Currencies 10 | * Implement the Plans for each Subscription Plan 11 | * @export 12 | * @interface ISubscriptionPlan 13 | */ 14 | export interface ISubscriptionPlan { 15 | id: string; 16 | 17 | /** 18 | * Store the Subscription Name 19 | * 20 | * @type {string} 21 | * @memberof ISubscriptionPlan 22 | */ 23 | name: string; 24 | /** 25 | * Random Unique code generated for the subscription plan 26 | * 27 | * @type {string} 28 | * @memberof ISubscriptionPlan 29 | */ 30 | code?: string; 31 | 32 | /** 33 | * Alias of the Subscription Name with lowercase 34 | * 35 | * @type {string} 36 | * @memberof ISubscriptionPlan 37 | */ 38 | slug?: string; 39 | 40 | /** 41 | * Description of the subscription plan 42 | * 43 | * @type {string} 44 | * @memberof ISubscriptionPlan 45 | */ 46 | description?: string; 47 | 48 | /** 49 | * One time cost of the subscription plan 50 | * 51 | * @type {number} 52 | * @memberof ISubscriptionPlan 53 | */ 54 | price: number; 55 | 56 | /** 57 | * Extra fee for the subscription plan like 58 | * signup_fee or credit_card fee 59 | * 60 | * @type {number} 61 | * @memberof ISubscriptionPlan 62 | */ 63 | extra_fee?: number; 64 | create_at?: Date; 65 | /** 66 | * Subscription period must be between 1 and 30 67 | * 68 | * @type {number} 69 | * @memberof ISubscriptionPlan 70 | */ 71 | invoice_period?: number; 72 | 73 | /** 74 | * Duration must be DAY | WEEK | MONTH | YEAR 75 | * 76 | * @type {SubscriptionDuration} 77 | * @memberof ISubscriptionPlan 78 | */ 79 | invoice_duration?: SubscriptionDuration; 80 | 81 | /** 82 | * To offer trail period for the subscription 83 | * Trail period must be between 1 and 30 84 | * 85 | * @type {number} 86 | * @memberof ISubscriptionPlan 87 | */ 88 | trail_period?: number; 89 | /** 90 | * Duration must be DAY | WEEK | MONTH | YEAR 91 | * 92 | * @type {SubscriptionDuration} 93 | * @memberof ISubscriptionPlan 94 | */ 95 | trail_duration?: SubscriptionDuration; 96 | } 97 | -------------------------------------------------------------------------------- /packages/subscription/src/inversify.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/tslint/config */ 2 | import { configBinding, ServiceSymbol } from '@bcdapps/common-backend'; 3 | import { Container, ContainerModule, interfaces } from 'inversify'; 4 | import _ from 'lodash'; 5 | import 'reflect-metadata'; 6 | import { DatabaseService } from './database/database.service'; 7 | import { ServicesApp } from './services.app'; 8 | import { SubscriptionPlanService } from './subscription_plan.service'; 9 | 10 | export const bindingsSubscription = new ContainerModule( 11 | (bind: interfaces.Bind) => { 12 | _.each([DatabaseService, SubscriptionPlanService], (Service: any) => { 13 | bind(Service) 14 | .to(Service) 15 | .inSingletonScope(); 16 | 17 | bind<any>(ServiceSymbol).toFactory<any>(context => 18 | context.container.get<any>(Service), 19 | ); 20 | }); 21 | bind<ServicesApp>(ServicesApp) 22 | .toSelf() 23 | .inSingletonScope(); 24 | }, 25 | ); 26 | 27 | const container = new Container(); 28 | container.load(configBinding); 29 | container.load(bindingsSubscription); 30 | 31 | export const servicesContainer = container; 32 | -------------------------------------------------------------------------------- /packages/subscription/src/message/error.message.ts: -------------------------------------------------------------------------------- 1 | export const SubscriptionPlanErrorMessage = { 2 | /* --------------------------------- Others --------------------------------- */ 3 | INVOICE_PERIOD_DURATION: 'Invoice Period and Duration not matched', 4 | INVOICE_PERIOD_DURATION_DAY: 5 | 'Invoice Period: must be less than or equal to 31', 6 | INVOICE_PERIOD_DURATION_WEEK: 7 | 'Invoice Period: must be less than or equal to 4', 8 | INVOICE_PERIOD_DURATION_MONTH: 9 | 'Invoice Period: must be less than or equal to 12', 10 | INVOICE_PERIOD_DURATION_YEAR: 11 | 'Invoice Period: must be less than or equal to 5', 12 | TRAIL_PERIOD_DURATION: 'Trail Period and Duration not matched', 13 | }; 14 | -------------------------------------------------------------------------------- /packages/subscription/src/model/subscription_plan.model.ts: -------------------------------------------------------------------------------- 1 | import { ISubscriptionPlan } from '../interfaces/subscription_plan.interface'; 2 | import { SubscriptionDuration } from '../types/subscription_plan.types'; 3 | 4 | /** 5 | * Subscription Plans Model 6 | * @CollectionName will be SubscriptionPlan 7 | * 8 | * @export 9 | * @class SubscriptionPlanModel 10 | * @implements {ISubscriptionPlan} 11 | */ 12 | export class SubscriptionPlans implements Partial<ISubscriptionPlan> { 13 | protected readonly collection_name = 'SubscriptionPlans'; 14 | // Note: All the properties must be partial 15 | id?: string; 16 | name?: string; 17 | code?: string; 18 | slug?: string; 19 | description?: string; 20 | price?: number; 21 | extra_fee?: number; 22 | create_at?: Date; 23 | invoice_period?: number; 24 | invoice_duration?: SubscriptionDuration; 25 | trail_period?: number; 26 | trail_duration?: SubscriptionDuration; 27 | constructor(payload?: Partial<ISubscriptionPlan>) { 28 | Object.assign(this, payload); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/subscription/src/services.app.ts: -------------------------------------------------------------------------------- 1 | import { createEverLogger } from '@bcdapps/common-backend'; 2 | import { inject, injectable } from 'inversify'; 3 | import { DatabaseService } from './database/database.service'; 4 | 5 | @injectable() 6 | export class ServicesApp { 7 | private log = createEverLogger({ name: 'main' }); 8 | constructor( 9 | @inject(DatabaseService) 10 | private readonly dbservice: DatabaseService, 11 | ) {} 12 | async start(): Promise<void> { 13 | // TODO: Uncomment in dev or production mode 14 | // await this.dbservice.connectDB(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/subscription/src/subscription_plan.seed.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | export const CreateSubscriptionPlanPayloadError = 3 | '[{"message":"name: must be at least 3 characters","field":"name"},{"message":"name: is required","field":"name"},{"message":"invoice_period: must be less than or equal to 31","field":"invoice_period"},{"message":"Invoice Period: must be less than or equal to 31","field":"invoice_period"}]'; 4 | export const UpdateSubscriptionPayloadError = 5 | '[{"message":"name: must be at least 3 characters","field":"name"},{"message":"invoice_period: must be less than or equal to 31","field":"invoice_period"},{"message":"Invoice Period: must be less than or equal to 31","field":"invoice_period"}]'; 6 | export const DeleteSubscriptionPlanError = 7 | '[{"message":"id: must be greater than or equal to 3","field":"id"}]'; 8 | export const ValidateDayError = 9 | '[{"message":"invoice_period: must be less than or equal to 31","field":"invoice_period"},{"message":"Invoice Period: must be less than or equal to 31","field":"invoice_period"}]'; 10 | export const ValidateWeekError = 11 | '[{"message":"Invoice Period: must be less than or equal to 4","field":"invoice_period"}]'; 12 | export const ValidateMonthError = 13 | '[{"message":"Invoice Period: must be less than or equal to 12","field":"invoice_period"}]'; 14 | export const ValidateYearError = 15 | '[{"message":"Invoice Period: must be less than or equal to 5","field":"invoice_period"}]'; 16 | export const FindSubscriptionPlanPayloadError = 17 | '[{"message":"id: must be greater than or equal to 3","field":"id"}]'; 18 | export const FindAllSubscriptionPlanPayloadError = 19 | '[{"message":"id: must be greater than or equal to 3","field":"id"},{"message":"id: must be less than or equal to 1000","field":"limit"}]'; 20 | -------------------------------------------------------------------------------- /packages/subscription/src/types/subscription_plan.types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IBaseService, 3 | IChangeResponseInfo, 4 | IPagination, 5 | IResponseInfo, 6 | SortProperties, 7 | } from '@bcdapps/common-backend'; 8 | import { ISubscriptionPlan } from '../interfaces/subscription_plan.interface'; 9 | 10 | export enum SubscriptionPlanDurationEnum { 11 | DAY = 'DAY', 12 | WEEK = 'WEEK', 13 | MONTH = 'MONTH', 14 | YEAR = 'YEAR', 15 | } 16 | export type SubscriptionDuration = keyof typeof SubscriptionPlanDurationEnum; 17 | 18 | export type SubscriptionPlan = ISubscriptionPlan; 19 | export type Ta = keyof ISubscriptionPlan; 20 | // Type for creating Subscription Plan 21 | export type SubscriptionInputPayload = Required< 22 | Pick< 23 | SubscriptionPlan, 24 | 'name' | 'price' | 'invoice_period' | 'invoice_duration' 25 | > 26 | > & 27 | Pick< 28 | SubscriptionPlan, 29 | 'code' | 'description' | 'trail_period' | 'trail_duration' 30 | >; 31 | 32 | // Type of Subscription Plan Properties used for sorting 33 | // id | price 34 | type SubscriptionPlanSortKeys = Extract< 35 | keyof Pick<SubscriptionPlan, 'id' | 'price'>, 36 | string 37 | >; 38 | 39 | // Transform the property type to SortDirection 40 | // {id: "ASC" | "DESC"} 41 | export type SubscriptionPlanSort = SortProperties<SubscriptionPlanSortKeys>; 42 | 43 | // Type for getting Subscription Plan 44 | export type SubscriptionPlanFilter = Partial< 45 | Pick<SubscriptionPlan, 'id' | 'code' | 'slug'> & 46 | Pick<IPagination, 'limit' | 'skip'> & { sort_by: SubscriptionPlanSort } 47 | >; 48 | 49 | // Type for Updating Subscription Plan 50 | export type SubscriptionPlanUpdatePayload = Partial<SubscriptionInputPayload>; 51 | 52 | export type SubscriptionPlanResponse = IResponseInfo<SubscriptionPlan>; 53 | export type DeleteSubscriptionPlanResponse = IChangeResponseInfo< 54 | SubscriptionPlan 55 | >; 56 | export type UpdateSubscriptionPlanResponse = IChangeResponseInfo< 57 | SubscriptionPlan 58 | >; 59 | 60 | /** 61 | * Interface for Subscription Plan Service 62 | * CRUD operation 63 | * 64 | * @export 65 | * @interface ISubscriptionPlanService 66 | */ 67 | export type ISubscriptionPlanService = IBaseService< 68 | SubscriptionPlan, 69 | SubscriptionInputPayload, 70 | SubscriptionPlanFilter, 71 | SubscriptionPlanUpdatePayload 72 | >; 73 | -------------------------------------------------------------------------------- /packages/subscription/src/validators/subscription_plan.filter.yup.ts: -------------------------------------------------------------------------------- 1 | import { ErrorGenerator } from '@bcdapps/common-backend'; 2 | import * as yup from 'yup'; 3 | import { 4 | SubscriptionPlanFilter, 5 | SubscriptionPlanSort, 6 | } from '../types/subscription_plan.types'; 7 | 8 | export const subscriptionPlanFilterSchema = yup.object<SubscriptionPlanFilter>({ 9 | code: yup 10 | .string() 11 | .min(3, ErrorGenerator.MiniLength<SubscriptionPlanFilter>('code', 3)), 12 | id: yup 13 | .string() 14 | .min(3, ErrorGenerator.MinValue<SubscriptionPlanFilter>('id', 3)), 15 | limit: yup 16 | .number() 17 | .max(1000, ErrorGenerator.MaxValue<SubscriptionPlanFilter>('id', 1000)) 18 | .min(1, ErrorGenerator.MinValue<SubscriptionPlanFilter>('id', 1)), 19 | skip: yup 20 | .number() 21 | .min(1, ErrorGenerator.MinValue<SubscriptionPlanFilter>('id', 1)), 22 | slug: yup.string(), 23 | sort_by: yup.mixed<SubscriptionPlanSort>(), 24 | }); 25 | -------------------------------------------------------------------------------- /packages/subscription/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitThis": false, 5 | "target": "es2017", 6 | "baseUrl": "src", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "outDir": "dist", 10 | "strict": true, 11 | "experimentalDecorators":true, 12 | "noEmit": false 13 | }, 14 | "include": ["src"], 15 | "references": [ 16 | { 17 | "path": "./tsconfig.lib.json" 18 | }, 19 | { 20 | "path": "./tsconfig.spec.json" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /packages/subscription/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "outDir": "dist", 6 | "baseUrl": "src", 7 | "rootDir": ".", 8 | "allowSyntheticDefaultImports":true, 9 | "experimentalDecorators":true, 10 | "declaration": true, 11 | "types": ["node"] 12 | }, 13 | "exclude": ["**/*.spec.ts"], 14 | "include": ["**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/subscription/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/ui/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '../../../.storybook/addons'; 2 | -------------------------------------------------------------------------------- /packages/ui/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from '@storybook/react'; 2 | import { withKnobs } from '@storybook/addon-knobs'; 3 | 4 | addDecorator(withKnobs); 5 | configure(require.context('../stories', true, /\.stories\.(j|t)sx?$/), module); 6 | -------------------------------------------------------------------------------- /packages/ui/.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDecoratorMetadata": true 5 | }, 6 | "exclude": ["../**/*.spec.ts"], 7 | "include": ["../stories/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 2 | const rootWebpackConfig = require('../../../.storybook/webpack.config'); 3 | // Export a function. Accept the base config as the only param. 4 | module.exports = async ({ config, mode }) => { 5 | config = await rootWebpackConfig({ config, mode }); 6 | 7 | const tsPaths = new TsconfigPathsPlugin({ 8 | configFile: './tsconfig.json', 9 | }); 10 | 11 | config.resolve.plugins 12 | ? config.resolve.plugins.push(tsPaths) 13 | : (config.resolve.plugins = [tsPaths]); 14 | 15 | config.resolve.extensions.push('.tsx'); 16 | config.resolve.extensions.push('.ts'); 17 | 18 | config.module.rules.push({ 19 | test: /\.(ts|tsx)$/, 20 | loader: require.resolve('babel-loader'), 21 | options: { 22 | presets: [ 23 | '@babel/preset-env', 24 | '@babel/preset-react', 25 | '@babel/preset-typescript', 26 | ], 27 | }, 28 | }); 29 | return config; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bcdapps/ui", 3 | "version": "1.0.0", 4 | "license": "Apache-2.0", 5 | "main": "dist/index.js", 6 | "module": "dist/esm.index.js", 7 | "types": "dist/index.d.ts", 8 | "typings": "dist/index.d.ts", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "build": "tsdx build --targe node --format cjs,esm --tsconfig ./tsconfig.json" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/src/components/backdrop/Backdrop.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | interface BackdropProps { 4 | className?: string; 5 | onClick: () => void; 6 | } 7 | 8 | export const Backdrop:React.FC<BackdropProps> = React.forwardRef(function Backdrop(props, ref) { 9 | const { className, ...other } = props 10 | 11 | return ( 12 | <div 13 | className={`absolute inset-0 z-40 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center ${className}`} 14 | ref={ref as any} {...other} 15 | ></div> 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/ui/src/components/backdrop/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Backdrop'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/background/Background.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import { useBackground } from './variants/BackgroundProvider'; 4 | import { useWindowSize } from '../hooks/useWindowSize'; 5 | 6 | export const Background: React.FC<IBackgroundProps> = ({ 7 | url, 8 | offset = '0', 9 | bgColor = '#ffffff', 10 | animate = true, 11 | updateHeight = 'on-mount-only', 12 | centerContent = false, 13 | children, 14 | }) => { 15 | const { offset: hookOffset, setOffset } = useBackground(); 16 | const { innerHeight } = useWindowSize(); 17 | const [height, setHeight] = useState<number | null>(null); 18 | 19 | useEffect(() => { 20 | if (offset) { 21 | setOffset(offset); 22 | } 23 | }, [offset]); 24 | 25 | useEffect(() => { 26 | switch (updateHeight) { 27 | case 'on-mount-only': 28 | if (height === null) setHeight(innerHeight); 29 | break; 30 | case 'on-resize': 31 | setHeight(innerHeight); 32 | break; 33 | case 'fixed-100vh': 34 | break; 35 | default: 36 | throw Error(`Unsupported option: '${updateHeight}'`); 37 | } 38 | }, [innerHeight, updateHeight]); 39 | 40 | return ( 41 | <div 42 | className="bg-wrapper" 43 | style={{ 44 | background: bgColor, 45 | height: 46 | updateHeight === 'fixed-100vh' 47 | ? '100vh' 48 | : height 49 | ? `${height}px` 50 | : '100vh', 51 | }} 52 | > 53 | <motion.div 54 | className={'h-full w-full'} 55 | style={{ backgroundImage: `url(${url})` }} 56 | initial={{ y: animate ? '0%' : offset }} 57 | animate={{ 58 | y: hookOffset, 59 | transition: animate 60 | ? { 61 | type: 'spring', 62 | damping: 10, 63 | stiffness: 150, 64 | } 65 | : { 66 | type: 'just', 67 | }, 68 | }} 69 | /> 70 | {children && ( 71 | <div 72 | className={`fixed w-full h-full ${centerContent ? 'flex justify-center Items-center': ''}`} 73 | > 74 | {children} 75 | </div> 76 | )} 77 | </div> 78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /packages/ui/src/components/background/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Background'; 2 | export { 3 | BackgroundProvider, 4 | useBackground, 5 | } from './variants/BackgroundProvider'; 6 | -------------------------------------------------------------------------------- /packages/ui/src/components/background/types.ts: -------------------------------------------------------------------------------- 1 | interface IBackgroundProps { 2 | url: string; 3 | offset?: string; 4 | bgColor?: string; 5 | animate?: boolean; 6 | // Check https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ for 7 | // why using simply 100vh doesn't work on mobile browsers. 8 | updateHeight?: 'on-mount-only' | 'on-resize' | 'fixed-100vh'; 9 | centerContent?: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /packages/ui/src/components/background/variants/BackgroundProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | 3 | interface BackgroundHook { 4 | setOffset: (offset: string) => void; 5 | offset: string; 6 | } 7 | 8 | const BackgroundContext = React.createContext<BackgroundHook>({ 9 | setOffset: () => {}, 10 | offset: '0', 11 | }); 12 | 13 | export const BackgroundProvider: React.FC = ({ children }) => { 14 | const [state, setState] = useState<string>('0'); 15 | 16 | return ( 17 | <BackgroundContext.Provider 18 | value={{ 19 | setOffset: setState, 20 | offset: state, 21 | }} 22 | > 23 | {children} 24 | </BackgroundContext.Provider> 25 | ); 26 | }; 27 | 28 | export const useBackground = () => useContext(BackgroundContext); 29 | -------------------------------------------------------------------------------- /packages/ui/src/components/button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useImperativeHandle } from 'react'; 2 | import { ButtonProps, ButtonHandler } from './types'; 3 | import { motion } from 'framer-motion'; 4 | import { useUpdateEffect } from 'ahooks'; 5 | import * as BnStyles from './style'; 6 | import { Icon } from '../icon'; 7 | 8 | /* ------------------------------- animations ------------------------------- */ 9 | 10 | const tapAnimation = { 11 | tap: { 12 | scale: 0.85, 13 | }, 14 | }; 15 | const spinTransition = { 16 | loop: Infinity, 17 | ease: 'linear', 18 | duration: 1, 19 | }; 20 | 21 | const ButtonBg = { 22 | hover: { 23 | scale: 20, 24 | transition: { 25 | duration: 0.2, 26 | }, 27 | }, 28 | closed: { 29 | scale: 0, 30 | transition: { 31 | duration: 0.2, 32 | }, 33 | }, 34 | success: { 35 | scale: 20, 36 | transition: { 37 | duration: 0.2, 38 | }, 39 | }, 40 | error: { 41 | scale: 20, 42 | transition: { 43 | duration: 0.2, 44 | }, 45 | }, 46 | }; 47 | 48 | /* -------------------------------------------------------------------------- */ 49 | /* Button Implementation */ 50 | /* -------------------------------------------------------------------------- */ 51 | 52 | const RefButton: React.ForwardRefRenderFunction<ButtonHandler, ButtonProps> = ( 53 | { onClick, label, icon, variant = 'default', ...props }, 54 | ref, 55 | ) => { 56 | const [isPending, setPending] = useState<boolean>(false); 57 | const [clickDisable, setClickDisable] = useState<boolean>(false); 58 | const [buttonState, setButtonState] = useState<string>('closed'); 59 | 60 | const _handleEvent = (e, parentHandler) => { 61 | if (clickDisable) { 62 | return; 63 | } 64 | setPending(true); 65 | parentHandler ? parentHandler(e) : e; 66 | }; 67 | useUpdateEffect(() => { 68 | if (isPending) { 69 | setClickDisable(true); 70 | } 71 | }, [isPending]); 72 | 73 | useImperativeHandle(ref, () => ({ 74 | result: (isSuccess: boolean) => { 75 | setPending(false); 76 | setClickDisable(false); 77 | setButtonState(isSuccess ? 'success' : 'error'); 78 | }, 79 | })); 80 | return ( 81 | <BnStyles.ButtonWrapper> 82 | <motion.div 83 | whileTap={'tap'} 84 | variants={tapAnimation} 85 | onClick={e => _handleEvent(e, onClick)} 86 | onMouseEnter={() => setButtonState('hover')} 87 | onMouseLeave={() => setButtonState('closed')} 88 | > 89 | <BnStyles.SButton 90 | variant={buttonState} 91 | label={label} 92 | animate={buttonState} 93 | type="button" 94 | {...props} 95 | className={`flex items-center px-4 py-2 focus:outline-none ${props.className}`} 96 | > 97 | <BnStyles.BgCircle 98 | variant={buttonState} 99 | initial={{ scale: 0 }} 100 | animate={buttonState} 101 | variants={ButtonBg} 102 | /> 103 | {buttonState === 'error' ? ( 104 | <BnStyles.ButtonLabel>Failed</BnStyles.ButtonLabel> 105 | ) : buttonState === 'success' ? ( 106 | <BnStyles.ButtonLabel>Success</BnStyles.ButtonLabel> 107 | ) : ( 108 | <BnStyles.ButtonContent> 109 | {isPending ? ( 110 | <BnStyles.ButtonIcon 111 | className={`pr-2 spin-origin`} 112 | style={{ originX: 0.34 }} 113 | animate={{ rotate: 360 }} 114 | transition={spinTransition} 115 | > 116 | <Icon icon="AiOutlineLoading3Quarters" /> 117 | </BnStyles.ButtonIcon> 118 | ) : ( 119 | <>{icon && <Icon icon={icon} className={`mr-2`} />}</> 120 | )} 121 | <BnStyles.ButtonLabel>{label}</BnStyles.ButtonLabel> 122 | </BnStyles.ButtonContent> 123 | )} 124 | </BnStyles.SButton> 125 | </motion.div> 126 | </BnStyles.ButtonWrapper> 127 | ); 128 | }; 129 | 130 | export const Button = React.memo(React.forwardRef(RefButton)); 131 | -------------------------------------------------------------------------------- /packages/ui/src/components/button/index.ts: -------------------------------------------------------------------------------- 1 | import LinkButton from './variants/LinkButton'; 2 | import StandardButton from './variants/StandardButton'; 3 | 4 | export * from './Button'; 5 | export * from './types'; 6 | export { StandardButton, LinkButton }; 7 | -------------------------------------------------------------------------------- /packages/ui/src/components/button/style.tsx: -------------------------------------------------------------------------------- 1 | import theme from 'styled-theming'; 2 | import styled from 'styled-components'; 3 | import { themesMap } from '../theme/themes'; 4 | import { motion } from 'framer-motion'; 5 | import { ButtonProps } from './types'; 6 | 7 | /* --------------------------------- styles --------------------------------- */ 8 | export const backgroundColor = theme.variants('mode', 'variant', { 9 | default: { 10 | light: themesMap.light.colors.fill, 11 | dark: themesMap.dark.colors.fill, 12 | }, 13 | }); 14 | 15 | const buttonColor = theme.variants('mode', 'variant', { 16 | default: { 17 | light: themesMap.light.colors.font.active, 18 | dark: themesMap.dark.colors.font.active, 19 | }, 20 | hover: { 21 | light: themesMap.light.colors.font.active, 22 | dark: themesMap.dark.colors.font.active, 23 | }, 24 | closed: { 25 | light: themesMap.light.colors.font.active, 26 | dark: themesMap.dark.colors.font.active, 27 | }, 28 | success: { light: '#FFF', dark: themesMap.dark.colors.hover }, 29 | error: { light: '#FFF', dark: themesMap.dark.colors.hover }, 30 | }); 31 | 32 | const buttonHoverColor = theme.variants('mode', 'variant', { 33 | default: { 34 | light: themesMap.light.colors.hover, 35 | dark: themesMap.dark.colors.hover, 36 | }, 37 | hover: { 38 | light: themesMap.light.colors.hover, 39 | dark: themesMap.dark.colors.hover, 40 | }, 41 | closed: { 42 | light: themesMap.light.colors.hover, 43 | dark: themesMap.dark.colors.hover, 44 | }, 45 | success: { light: '#27ae60', dark: themesMap.dark.colors.hover }, 46 | error: { light: '#e74c3c', dark: themesMap.dark.colors.hover }, 47 | }); 48 | export const SButton = styled(motion.button).attrs( 49 | (props: Partial<ButtonProps>) => ({ 50 | variant: props.variant, 51 | }), 52 | )<Partial<ButtonProps>>` 53 | position: relative; 54 | color: ${props => (props.color ? props.color : buttonColor)}; 55 | background-color: ${backgroundColor}; 56 | overflow: hidden; 57 | `; 58 | export const ButtonWrapper = styled.div` 59 | width: fit-content; 60 | `; 61 | 62 | export const BgCircle = styled(motion.div).attrs( 63 | (props: Partial<ButtonProps>) => ({ 64 | variant: props.variant, 65 | }), 66 | )<Partial<ButtonProps>>` 67 | position: absolute; 68 | left: calc(50% - 5px); 69 | top: calc(50% - 5px); 70 | width: 10px; 71 | height: 10px; 72 | border-radius: 50px; 73 | margin: 0; 74 | background-color: ${buttonHoverColor}; 75 | `; 76 | export const ButtonContent = styled.div` 77 | z-index: 2; 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | `; 82 | export const ButtonIcon = styled(motion.div)` 83 | width: 24px; 84 | `; 85 | export const ButtonLabel = styled.div` 86 | z-index:0 87 | `; 88 | -------------------------------------------------------------------------------- /packages/ui/src/components/button/types.ts: -------------------------------------------------------------------------------- 1 | import { IconName } from '../icon/types'; 2 | 3 | type ButtonType = 'default' | 'primary' | 'success' | 'warning' | string; 4 | 5 | export type ButtonHandler = { 6 | result: (isSuccess: boolean) => void; 7 | }; 8 | 9 | export interface ButtonProps { 10 | to?: string; 11 | onClick?: (e) => void; 12 | disabled?: boolean; 13 | className?: string; 14 | color?: string; 15 | label: string; 16 | icon?: IconName; 17 | variant?: ButtonType; 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/src/components/button/variants/LinkButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { ButtonProps } from '../types'; 4 | 5 | interface LinkButton extends ButtonProps { 6 | to: string; 7 | } 8 | 9 | const LinkButton: React.FC<LinkButton> = ({ ...props }) => <Link {...props} />; 10 | 11 | export default LinkButton; 12 | -------------------------------------------------------------------------------- /packages/ui/src/components/button/variants/StandardButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ButtonProps } from '../types'; 3 | import { motion } from 'framer-motion'; 4 | import styled from 'styled-components'; 5 | import {backgroundColor} from '../style' 6 | function getAnimationProps() { 7 | return { 8 | whileTap: { 9 | scale: 0.85, 10 | }, 11 | }; 12 | } 13 | const Button = styled.button<ButtonProps>` 14 | color: ${props => props.color}; 15 | background: ${props => (props.disabled ? '#cccccc' : backgroundColor)}; 16 | `; 17 | const ButtonWrapper = styled.div` 18 | width: fit-content; 19 | `; 20 | const StandardButton: React.FC<ButtonProps> = ({ children, ...props }) => ( 21 | <ButtonWrapper> 22 | <motion.div {...getAnimationProps()}> 23 | <Button 24 | type="button" 25 | {...props} 26 | className={`flex items-center px-4 py-2 focus:outline-none ${props.className}`} 27 | > 28 | {children} 29 | </Button> 30 | </motion.div> 31 | </ButtonWrapper> 32 | ); 33 | 34 | export default StandardButton; 35 | -------------------------------------------------------------------------------- /packages/ui/src/components/button/variants/index.ts: -------------------------------------------------------------------------------- 1 | import LinkButton from './LinkButton'; 2 | import StandardButton from './StandardButton'; 3 | 4 | export { LinkButton, StandardButton }; 5 | -------------------------------------------------------------------------------- /packages/ui/src/components/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Sidebar } from '../sidebar'; 3 | import { Header } from '../header'; 4 | import { IDropdownProps } from 'components/dropdown/types'; 5 | import { DashboardProps } from './types'; 6 | import styled from 'styled-components'; 7 | import { SidebarContext } from '../../context/sidebar.context'; 8 | interface DashboardContainerProps { 9 | isOpen: boolean 10 | } 11 | 12 | const DashboardContainer = styled.div<DashboardContainerProps>` 13 | display: grid; 14 | grid-template-columns:${props => props.isOpen ? "256px auto" : "0px auto" } ; 15 | grid-template-rows: 50px auto 50px; 16 | grid-template-areas: 17 | 'sidenav header' 18 | 'sidenav main' 19 | 'sidenav footer'; 20 | height: 100vh; 21 | `; 22 | 23 | 24 | const MainWrapper = styled.div` 25 | grid-area: main; 26 | `; 27 | const FooterWrapper = styled.div` 28 | grid-area: footer; 29 | `; 30 | export const Dashboard: React.FC<DashboardProps> = ({ children, siderbar, 31 | headerDropDownMenuTitle = 'Settings', 32 | headerDropDownMenuItems, title= "Admin Panel", footer= "Developed with love" }) => { 33 | const { isSidebarOpen } = useContext(SidebarContext) 34 | 35 | return ( 36 | <DashboardContainer isOpen={isSidebarOpen as boolean}> 37 | <Header 38 | rightDropdown={ 39 | { 40 | title: headerDropDownMenuTitle, 41 | menuItems: headerDropDownMenuItems, 42 | } as IDropdownProps 43 | } 44 | /> 45 | <Sidebar menuItems={siderbar} title={title} /> 46 | <MainWrapper className="z-20">{children}</MainWrapper> 47 | <FooterWrapper>{footer}</FooterWrapper> 48 | </DashboardContainer> 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /packages/ui/src/components/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Dashboard'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/dashboard/types.ts: -------------------------------------------------------------------------------- 1 | import { IMenuItemProps } from 'components/sidebar/types'; 2 | 3 | export interface DashboardProps { 4 | title?: string; 5 | footer?: string; 6 | siderbar: IMenuItemProps[]; 7 | headerDropDownMenuTitle?: string; 8 | headerDropDownMenuItems: IMenuItemProps[]; 9 | } 10 | -------------------------------------------------------------------------------- /packages/ui/src/components/dropdown/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Popper from 'popper.js'; 3 | import { IDropdownProps } from './types'; 4 | import { IMenuItemProps } from '../sidebar/types'; 5 | import { Icon } from '../icon'; 6 | import { StandardButton } from '../button'; 7 | 8 | export const Dropdown: React.FC<IDropdownProps> = ({ 9 | placement = 'bottom-end', 10 | title, 11 | menuItems, 12 | ...props 13 | }) => { 14 | const [dropdownPopoverShow, setDropdownPopoverShow] = React.useState(false); 15 | const btnDropdownRef = React.createRef<HTMLButtonElement>(); 16 | const popoverDropdownRef = React.createRef<HTMLUListElement>(); 17 | const openDropdownPopover = () => { 18 | new Popper( 19 | btnDropdownRef.current as HTMLButtonElement, 20 | popoverDropdownRef.current as HTMLUListElement, 21 | { 22 | placement: placement, 23 | }, 24 | ); 25 | setDropdownPopoverShow(true); 26 | }; 27 | const closeDropdownPopover = () => { 28 | setDropdownPopoverShow(false); 29 | }; 30 | return ( 31 | <div className="relative"> 32 | <button 33 | className="p-2 border-2 text-black rounded-md border-gray-600 dark:hover:bg-gray-700 dark:text-white outline-none focus:outline-none" 34 | ref={btnDropdownRef} 35 | onClick={() => { 36 | dropdownPopoverShow ? closeDropdownPopover() : openDropdownPopover(); 37 | }} 38 | > 39 | {title} 40 | </button> 41 | <ul 42 | ref={popoverDropdownRef} 43 | className={ 44 | (dropdownPopoverShow ? 'block ' : 'hidden ') + 45 | 'w-56 p-2 mt-2 text-sm font-semibold text-gray-600 bg-white border border-gray-100 rounded-md shadow-md' 46 | } 47 | > 48 | {menuItems.map((item: IMenuItemProps) => ( 49 | <li key={item.path} className="relative px-2 py-1"> 50 | <a 51 | href={item.path} 52 | className="inline-flex items-center w-full cursor-pointer hover:text-gray-800 hover:bg-gray-100" 53 | 54 | > 55 | <Icon className="w-5 h-5" icon={item.icon} /> 56 | <span className="ml-3">{item.name}</span> 57 | </a> 58 | </li> 59 | ))} 60 | </ul> 61 | </div> 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/ui/src/components/dropdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Dropdown'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/dropdown/types.ts: -------------------------------------------------------------------------------- 1 | import { IMenuItemProps } from 'components/sidebar/types'; 2 | 3 | export type Placement = 4 | | 'auto' 5 | | 'auto-start' 6 | | 'auto-end' 7 | | 'top' 8 | | 'top-start' 9 | | 'top-end' 10 | | 'bottom' 11 | | 'bottom-start' 12 | | 'bottom-end' 13 | | 'right' 14 | | 'right-start' 15 | | 'right-end' 16 | | 'left' 17 | | 'left-start' 18 | | 'left-end'; 19 | 20 | export interface IDropdownProps { 21 | placement?: Placement; 22 | className?: string; 23 | title: string; 24 | menuItems: IMenuItemProps[]; 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/src/components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import '../../style.css'; 3 | import { Dropdown } from '../dropdown'; 4 | import { StandardButton } from '../button'; 5 | import { HeaderProps } from './types'; 6 | import { SidebarContext } from '../../context/sidebar.context'; 7 | import { Icon } from '../icon'; 8 | import styled from 'styled-components'; 9 | const HeaderWrapper = styled.div` 10 | grid-area: header; 11 | `; 12 | export const Header: React.FC<HeaderProps> = ({ ...props }) => { 13 | const { toggleSidebar } = useContext(SidebarContext); 14 | return ( 15 | <HeaderWrapper className="bg-white shadow-md dark:bg-gray-800 z-40"> 16 | <header className="px-1 py-1 "> 17 | <div className="flex items-center justify-between mx-auto"> 18 | <StandardButton 19 | label="Home" 20 | onClick={toggleSidebar} 21 | className="p-1 mr-5 -ml-1 text-black hover:bg-white dark:bg-transparent dark:text-white focus:shadow-outline-gray" 22 | ><Icon icon="HiOutlineMenu"/>Home</StandardButton> 23 | <ul className="flex flex-row flex-1 space-x-6 justify-end items-center "> 24 | <li className="cursor-pointer"> 25 | {/* <ThemeSwitchButton label="" /> */} 26 | </li> 27 | {props.rightDropdown ? ( 28 | <li> 29 | <Dropdown 30 | title={props.rightDropdown.title} 31 | menuItems={props.rightDropdown.menuItems} 32 | /> 33 | </li> 34 | ) : ( 35 | <></> 36 | )} 37 | </ul> 38 | </div> 39 | </header> 40 | </HeaderWrapper> 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/ui/src/components/header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Header'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/header/types.ts: -------------------------------------------------------------------------------- 1 | import { IDropdownProps } from 'components/dropdown/types'; 2 | 3 | export interface HeaderProps { 4 | rightDropdown?: IDropdownProps; 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/src/components/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState } from 'react'; 2 | 3 | type WindowSize = { 4 | innerWidth: number; 5 | innerHeight: number; 6 | outerWidth: number; 7 | outerHeight: number; 8 | }; 9 | 10 | export function useWindowSize(): WindowSize { 11 | const [windowSize, setWindowSize] = useState(getWindowSize); 12 | function update() { 13 | setWindowSize(getWindowSize()); 14 | } 15 | useLayoutEffect(() => { 16 | window.addEventListener('resize', update); 17 | return () => window.removeEventListener('resize', update); 18 | }, []); 19 | return windowSize; 20 | } 21 | 22 | function getWindowSize() { 23 | return { 24 | innerWidth: window.innerWidth, 25 | innerHeight: window.innerHeight, 26 | outerWidth: window.outerWidth, 27 | outerHeight: window.outerHeight, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/ui/src/components/icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../style.css'; 3 | import { IconProps, IconList } from './types'; 4 | 5 | export const Icon: React.FC<IconProps> = ({ icon, ...props }) => { 6 | const _Icon = IconList[icon]; 7 | return <_Icon {...props} />; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/ui/src/components/icon/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Icon'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/icon/types.ts: -------------------------------------------------------------------------------- 1 | import { AiOutlineLoading3Quarters } from 'react-icons/ai'; 2 | import * as HIcons from 'react-icons/hi'; 3 | export const IconList = { ...HIcons, AiOutlineLoading3Quarters }; 4 | export type IconName = keyof typeof IconList; 5 | export interface IconProps { 6 | icon: IconName; 7 | className?: string; 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/src/components/input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './input'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/input/input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Field, ErrorMessage, getIn, FormikErrors } from 'formik'; 3 | import classNames from 'classnames'; 4 | import styled from 'styled-components'; 5 | import { motion } from 'framer-motion'; 6 | 7 | export interface InputProps { 8 | className?: string; 9 | label?: string; 10 | value?: string; 11 | name: string; 12 | errors: FormikErrors<{[key: string]:string}>; 13 | [key: string]: any; 14 | } 15 | function getStyles(errors, fieldName) { 16 | if (getIn(errors, fieldName)) { 17 | return { 18 | border: '1px solid red' 19 | } 20 | } 21 | } 22 | const InputWrapper = styled.div``; 23 | 24 | const InputLabel = styled.label``; 25 | 26 | export const Input: React.FC<InputProps> = ({ className, label, errors, value, name, ...props }) => { 27 | return ( 28 | <InputWrapper className={classNames('mb-4', className)} > 29 | <InputLabel className="block text-gray-700 text-sm font-semibold mb-2" htmlFor={name}>{label}</InputLabel> 30 | <Field style={getStyles(errors, name)} name={name} {...props} 31 | className={'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline outline-none '} /> 32 | <ErrorMessage name={name}> 33 | {(invalidMessage) => 34 | <motion.div 35 | animate={{ opacity: 1, x: 0 }} 36 | initial={{ opacity: 0, x: -1000 }} 37 | > 38 | <div className="field-error text-red-100 ">{invalidMessage}</div> 39 | </motion.div> 40 | } 41 | </ErrorMessage> 42 | </InputWrapper> 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/ui/src/components/notification/index.ts: -------------------------------------------------------------------------------- 1 | export * from './notification'; 2 | export * from './notification.provider'; 3 | export * from './useNotify' -------------------------------------------------------------------------------- /packages/ui/src/components/notification/notification.provider.tsx: -------------------------------------------------------------------------------- 1 | import React ,{ useEffect, createContext} from "react"; 2 | import { createPortal } from "react-dom"; 3 | import styled from "styled-components"; 4 | import shortId from "shortid"; 5 | import { AnimatePresence } from "framer-motion"; 6 | 7 | import Notification from "./notification"; 8 | import { NotifiactionProps } from './types'; 9 | 10 | 11 | const NotificationsContainer = styled.div` 12 | position: fixed; 13 | top: 16px; 14 | right: 16px; 15 | pointer-events: none; 16 | `; 17 | 18 | const useCreateDomElement = () => { 19 | const [domElement, setDomElement] = React.useState<HTMLDivElement>(); 20 | 21 | useEffect((): any => { 22 | const element = document.createElement("div"); 23 | document.body.appendChild(element); 24 | setDomElement(element); 25 | 26 | return () => document.body.removeChild(element); 27 | }, []); 28 | 29 | return domElement; 30 | } 31 | 32 | 33 | function useNotifications() { 34 | 35 | const [notifications, setNotifications] = React.useState<NotifiactionProps[]>([]); 36 | 37 | const notify = React.useCallback((notificationPayload: NotifiactionProps)=> { 38 | const id = shortId(); 39 | 40 | function removeNotification() { 41 | setNotifications(notifications => notifications.filter(n => n.id !== id)); 42 | } 43 | 44 | setNotifications(notifications => [ 45 | ...notifications, 46 | { 47 | id, 48 | onClose: removeNotification, 49 | onMore: notificationPayload.onMore && notificationPayload.onMore, 50 | title: notificationPayload.title, 51 | description: notificationPayload.description && notificationPayload.description, 52 | type: notificationPayload.type && notificationPayload.type 53 | } 54 | ]); 55 | 56 | setTimeout(removeNotification, 2000); 57 | }, []); 58 | 59 | return { notify, notifications }; 60 | } 61 | 62 | type ContextProps = { 63 | notify?: (notificationPayload:NotifiactionProps) => void; 64 | // notifications?: NotifiactionProps[] 65 | }; 66 | export const NotifyContext = createContext<ContextProps>({}); 67 | 68 | const NotificationProvider = ({ children }) => { 69 | const notificationRoot = useCreateDomElement(); 70 | 71 | const { notify, notifications } = useNotifications(); 72 | const value = { notify} 73 | return ( 74 | <> 75 | <NotifyContext.Provider value={value}>{children}</NotifyContext.Provider> 76 | {notificationRoot && 77 | createPortal( 78 | <NotificationsContainer> 79 | <AnimatePresence> 80 | {notifications.map((notification:NotifiactionProps) => ( 81 | <Notification key={notification.id} {...notification} /> 82 | ))} 83 | </AnimatePresence> 84 | </NotificationsContainer>, 85 | notificationRoot 86 | )} 87 | </> 88 | ) 89 | 90 | } 91 | 92 | export default NotificationProvider; 93 | -------------------------------------------------------------------------------- /packages/ui/src/components/notification/notification.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import styled from "styled-components"; 3 | import { motion } from "framer-motion"; 4 | 5 | import {MdCheck, MdWarning, MdErrorOutline} from 'react-icons/md' 6 | import { NotifiactionProps } from './types'; 7 | 8 | const Base = styled(motion.div)` 9 | display: flex; 10 | color: white; 11 | border-radius: 4px; 12 | background-color: rgba(0, 0, 0, 0.75); 13 | overflow: hidden; 14 | width: 350px; 15 | margin-bottom: 24px; 16 | box-shadow: 3px 3px 15px 0px rgba(0, 0, 0, 0.25); 17 | ` as any; 18 | 19 | const Main = styled.div` 20 | padding: 20px 28px 24px 20px; 21 | display: flex; 22 | flex: 1; 23 | 24 | & > div:first-child { 25 | margin-right: 20px; 26 | } 27 | `; 28 | 29 | const Description = styled.div` 30 | line-height: 1.3; 31 | font-size: 12px; 32 | color: rgba(255, 255, 255, 0.75); 33 | `; 34 | 35 | const Title = styled.div` 36 | line-height: 1.3; 37 | font-weight: bold; 38 | font-size: 14px; 39 | margin-bottom: 4px; 40 | `; 41 | 42 | const Buttons = styled.div` 43 | width: 80px; 44 | display: flex; 45 | flex-direction: column; 46 | 47 | & > button:not(:first-child) { 48 | border-top: 1px solid #404040; 49 | } 50 | `; 51 | 52 | const Button = styled.button` 53 | pointer-events: all; 54 | transition: background-color 0.15s ease-in-out; 55 | flex: 1; 56 | padding: 8px; 57 | background-color: rgb(255, 255, 255, 0.05); 58 | outline: 0; 59 | border: 0; 60 | color: white; 61 | width: 100%; 62 | cursor: pointer; 63 | 64 | &:hover { 65 | background-color: rgb(255, 255, 255, 0.1); 66 | } 67 | `; 68 | 69 | function getIcon(notificationType) { 70 | if (notificationType === "error") { 71 | return { 72 | Icon: MdErrorOutline, 73 | iconColor: "hsl(360,64%,55%)" 74 | }; 75 | } 76 | 77 | if (notificationType === "warning") { 78 | return { 79 | Icon: MdWarning, 80 | iconColor: "hsl(44,92%,63%)" 81 | }; 82 | } 83 | 84 | return { 85 | Icon: MdCheck, 86 | iconColor: "hsl(122, 40%, 52%)" 87 | }; 88 | } 89 | 90 | const Notification: React.FC<NotifiactionProps> = ({ title, description, type, onClose, onMore }) => { 91 | const { Icon, iconColor } = getIcon(type); 92 | 93 | return ( 94 | <Base 95 | initial={{ opacity: 0, scale: 0.8, x: 300 }} // animate from 96 | animate={{ opacity: 1, scale: 1, x: 0 }} // animate to 97 | exit={{ opacity: 0, scale: 0.8, x: 300 }} // animate exit 98 | transition={{ 99 | type: "spring", 100 | stiffness: 500, 101 | damping: 40 102 | }} 103 | positionTransition// auto animates the element when it's position changes 104 | > 105 | <Main> 106 | <div> 107 | <Icon size={32} style={{ color: iconColor }} /> 108 | </div> 109 | <div> 110 | <Title>{title} 111 | {description} 112 | 113 |
114 | 115 | 116 | {onMore && } 117 | 118 | 119 | ); 120 | } 121 | 122 | export default Notification; 123 | -------------------------------------------------------------------------------- /packages/ui/src/components/notification/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface NotifiactionProps { 3 | id?: string 4 | onClose?: any 5 | onMore?: any 6 | title?: string 7 | description?: string 8 | type : "error" | "warning" | "success" 9 | } -------------------------------------------------------------------------------- /packages/ui/src/components/notification/useNotify.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { NotifyContext } from "./notification.provider"; 3 | 4 | function useNotify() { 5 | return React.useContext(NotifyContext); 6 | } 7 | 8 | export default useNotify; 9 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import '../../style.css'; 3 | import { SidebarContent } from './internals/SidebarContent'; 4 | import { ISideBarProps } from './types'; 5 | import { isRenderer } from '../../types'; 6 | import { SidebarContext } from '../../context/sidebar.context'; 7 | import { Backdrop } from '../backdrop'; 8 | import { motion } from "framer-motion"; 9 | import styled from 'styled-components'; 10 | 11 | const sidebar = { 12 | open: { 13 | x: 0, 14 | transition: { 15 | duration: 0.2, 16 | } 17 | }, 18 | closed: { 19 | x: "-400px" 20 | }, 21 | }; 22 | const SidebarWrapper = styled.div` 23 | grid-area: sidenav; 24 | `; 25 | export const Sidebar: React.FC = ({ 26 | menuItems, 27 | title, 28 | ...props 29 | }) => { 30 | const { isSidebarOpen, closeSidebar } = useContext(SidebarContext) 31 | return ( 32 | 33 | 34 | 40 | 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Sidebar'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar/internals/SidebarContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../../style.css'; 3 | import { Icon } from '../../icon'; 4 | import { IMenuItemProps, ISideBarProps } from '../types'; 5 | 6 | export const SidebarContent: React.FC = ({ menuItems }) => { 7 | return ( 8 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar/types.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from '../../types'; 2 | import { IconProps } from '../icon/types'; 3 | 4 | export interface IMenuItemProps extends IconProps { 5 | path: string; 6 | name: string; 7 | exact?: boolean; 8 | } 9 | 10 | export interface ISideBarProps { 11 | title?: string | Renderer; 12 | menuItems: IMenuItemProps[]; 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/src/components/table/index.ts: -------------------------------------------------------------------------------- 1 | export * from './table'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/table/table.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { useTable, usePagination } from 'react-table' 4 | 5 | 6 | const Styles = styled.div` 7 | padding: 1rem; 8 | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 9 | 10 | table { 11 | width:100%; 12 | border-spacing: 0; 13 | border-bottom: 0px solid #e2e8f0; 14 | 15 | th { 16 | color: #a0aec0; 17 | font-weight: 200; 18 | text-transform: uppercase; 19 | text-align: start; 20 | } 21 | th, 22 | td { 23 | margin: 0; 24 | padding: 0.5rem; 25 | border-bottom: 1px solid #e2e8f0; 26 | font-weight:100; 27 | text-align: start; 28 | font-size: 0.875rem; 29 | } 30 | } 31 | 32 | .pagination { 33 | padding: 0.5rem; 34 | display: grid; 35 | grid-template-columns: 100px 1fr 100px; 36 | grid-template-rows: 50px; 37 | 38 | place-content: space-between; 39 | margin-top: 0.5rem; 40 | .icons { 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | } 45 | .center-col { 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | text-align: center; 50 | span { 51 | font-weight:100; 52 | font-size: 0.875rem; 53 | } 54 | } 55 | .show-btn { 56 | display: flex; 57 | justify-content: center; 58 | align-items: center; 59 | select { 60 | font-weight:100; 61 | font-size: 0.875rem; 62 | border: 1px solid rgba(237,242,247); 63 | border-radius: 0.25rem; 64 | padding: 0.75rem; 65 | } 66 | } 67 | 68 | } 69 | 70 | ` 71 | 72 | export const Table = ({ columns, data }) => { 73 | // Use the state and functions returned from useTable to build your UI 74 | const { 75 | getTableProps, 76 | getTableBodyProps, 77 | headerGroups, 78 | prepareRow, 79 | page, // Instead of using 'rows', we'll use page, 80 | // which has only the rows for the active page 81 | 82 | // The rest of these things are super handy, too ;) 83 | canPreviousPage, 84 | canNextPage, 85 | pageOptions, 86 | pageCount, 87 | gotoPage, 88 | nextPage, 89 | previousPage, 90 | setPageSize, 91 | state: { pageIndex, pageSize }, 92 | } = useTable( 93 | { 94 | columns, 95 | data, 96 | initialState: { pageIndex: 2 }, 97 | }, 98 | usePagination 99 | ) 100 | 101 | // Render the UI for your table 102 | return ( 103 | <> 104 | 105 | 106 | 107 | {headerGroups.map(headerGroup => ( 108 | 109 | {headerGroup.headers.map(column => ( 110 | 111 | ))} 112 | 113 | ))} 114 | 115 | 116 | {page.map((row, i) => { 117 | prepareRow(row) 118 | return ( 119 | 120 | {row.cells.map(cell => { 121 | return 122 | })} 123 | 124 | ) 125 | })} 126 | 127 |
{column.render('Header')}
{cell.render('Cell')}
128 | {/* 129 | Pagination can be built however you'd like. 130 | This is just a very basic UI implementation: 131 | */} 132 |
133 |
134 | {' '} 137 | {' '} 140 | {' '} 143 | {' '} 146 |
147 |
148 | 149 | Page{' '} 150 | 151 | {pageIndex + 1} of {pageOptions.length} 152 | {' '} 153 | 154 | 155 | | Go to page:{' '} 156 | { 161 | const page = e.target.value ? Number(e.target.value) - 1 : 0 162 | gotoPage(page) 163 | }} 164 | style={{ width: '100px', border:'1px solid #e2e8f0'}} 165 | /> 166 | {' '} 167 |
168 |
169 | 181 |
182 |
183 |
184 | 185 | ) 186 | } -------------------------------------------------------------------------------- /packages/ui/src/components/theme-switcher/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { ButtonProps } from '../button/types'; 3 | import { ThemeContext } from '../../context/theme.context'; 4 | import { Icon } from '../icon'; 5 | import { StandardButton } from '../button'; 6 | 7 | export const ThemeSwitchButton: React.FC = ({ 8 | children, 9 | ...props 10 | }) => { 11 | const { theme, toggleTheme } = useContext(ThemeContext); 12 | 13 | return ( 14 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/ui/src/components/theme-switcher/index.ts: -------------------------------------------------------------------------------- 1 | export { ThemeSwitchButton } from './ThemeSwitcher'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/theme/themes.ts: -------------------------------------------------------------------------------- 1 | import { ThemeConfig, ThemeName } from './types'; 2 | 3 | export const ligthTheme: ThemeConfig = { 4 | colors: { 5 | fill: '#FFFFFF', 6 | hover: ' #CCCCCC', 7 | border: '#FAFAFA', 8 | font: { 9 | inactive: '#BEBEBE', 10 | active: '#000000', 11 | }, 12 | }, 13 | }; 14 | 15 | export const darkTheme: ThemeConfig = { 16 | colors: { 17 | fill: '#212121', 18 | hover: ' #CCCCCC', 19 | border: '#3C3C3C', 20 | font: { 21 | active: '#FFFFFF', 22 | inactive: '#3C3C3C', 23 | }, 24 | }, 25 | }; 26 | 27 | export const nightTheme: ThemeConfig = { 28 | colors: { 29 | fill: '#1B232C', 30 | hover: ' #CCCCCC', 31 | border: '#343B47', 32 | font: { 33 | active: '#FFFFFF', 34 | inactive: '#63758D', 35 | }, 36 | }, 37 | }; 38 | 39 | export const themesMap = { 40 | [ThemeName.light]: ligthTheme, 41 | [ThemeName.dark]: darkTheme, 42 | [ThemeName.night]: nightTheme, 43 | } as const; 44 | -------------------------------------------------------------------------------- /packages/ui/src/components/theme/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export enum ThemeName { 4 | light = 'light', 5 | dark = 'dark', 6 | night = 'night', 7 | } 8 | 9 | export type ThemeNameType = keyof typeof ThemeName; 10 | 11 | type DeepPartial = { 12 | [P in keyof T]?: DeepPartial; 13 | }; 14 | 15 | export interface ThemeConfig { 16 | colors: { 17 | fill: string; 18 | hover: string; 19 | border: string; 20 | font: { 21 | inactive: string; 22 | active: string; 23 | }; 24 | }; 25 | } 26 | 27 | export interface ThemeProviderProps { 28 | theme: ThemeNameType; 29 | custom?: DeepPartial; 30 | children: ReactNode; 31 | } 32 | -------------------------------------------------------------------------------- /packages/ui/src/context/sidebar.context.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, createContext } from 'react' 2 | 3 | type ContextProps = { 4 | toggleSidebar: () => void; 5 | closeSidebar: () => void; 6 | isSidebarOpen: boolean; 7 | }; 8 | // create context 9 | export const SidebarContext = createContext>({}) 10 | 11 | export const SidebarProvider = ({ children }) => { 12 | const [isSidebarOpen, setIsSidebarOpen] = useState(true) 13 | 14 | const toggleSidebar = () => { 15 | setIsSidebarOpen(!isSidebarOpen) 16 | } 17 | 18 | const closeSidebar = () => { 19 | setIsSidebarOpen(false) 20 | } 21 | 22 | const value = useMemo( 23 | () => ({ 24 | isSidebarOpen, 25 | toggleSidebar, 26 | closeSidebar, 27 | }), 28 | [isSidebarOpen] 29 | ) 30 | 31 | return {children} 32 | } -------------------------------------------------------------------------------- /packages/ui/src/context/theme.context.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useEffect, 4 | useRef, 5 | useLayoutEffect, 6 | useMemo, 7 | createContext, 8 | } from 'react'; 9 | 10 | /** 11 | * Saves the old theme for future use 12 | * @param {string} theme - Name of curent theme 13 | * @return {string} previousTheme 14 | */ 15 | function usePrevious(theme) { 16 | const ref = useRef(); 17 | useEffect(() => { 18 | ref.current = theme; 19 | }); 20 | return ref.current; 21 | } 22 | 23 | /** 24 | * Gets user preferences from local storage 25 | * @param {string} key - localStorage key 26 | * @return {array} getter and setter for user preferred theme 27 | */ 28 | function useStorageTheme(key) { 29 | const userPreference = 30 | !!window.matchMedia && 31 | window.matchMedia('(prefers-color-scheme: dark)').matches; 32 | 33 | const [theme, setTheme] = useState( 34 | // use stored theme; fallback to user preference 35 | localStorage.getItem(key) || userPreference, 36 | ); 37 | 38 | // update stored theme 39 | useEffect(() => { 40 | localStorage.setItem(key, theme as string); 41 | }, [theme, key]); 42 | 43 | return [theme, setTheme]; 44 | } 45 | 46 | type ContextProps = { 47 | toggleTheme: () => void; 48 | theme: string; 49 | }; 50 | // create context 51 | export const ThemeContext = createContext>({}); 52 | 53 | // create context provider 54 | export const ThemeProvider = ({ children }) => { 55 | const [theme, setTheme] = useStorageTheme('theme') as any; 56 | 57 | // update root element class on theme change 58 | const oldTheme = usePrevious(theme); 59 | useLayoutEffect(() => { 60 | document.documentElement.classList.remove(`theme-${oldTheme}`); 61 | document.documentElement.classList.add(`theme-${theme}`); 62 | }, [theme, oldTheme]); 63 | 64 | function toggleTheme() { 65 | if (theme === 'light') setTheme('dark'); 66 | else setTheme('light'); 67 | } 68 | 69 | const value: ContextProps = useMemo( 70 | () => ({ 71 | theme, 72 | toggleTheme, 73 | }), 74 | [theme], 75 | ); 76 | 77 | return ( 78 | {children} 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components/button'; 2 | export * from './components/dashboard'; 3 | export * from './components/dropdown'; 4 | export * from './components/header'; 5 | export * from './components/icon'; 6 | export * from './components/input'; 7 | export * from './components/sidebar'; 8 | export * from './components/table'; 9 | export * from './components/theme-switcher'; 10 | export * from './context/sidebar.context'; 11 | export * from './components/notification' -------------------------------------------------------------------------------- /packages/ui/src/style.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | -------------------------------------------------------------------------------- /packages/ui/src/types.ts: -------------------------------------------------------------------------------- 1 | export type RenderFunction = (props: any) => Element; 2 | 3 | export type Renderer = { 4 | render: RenderFunction; 5 | }; 6 | 7 | export function isRenderer(props: Renderer | T): props is Renderer { 8 | if (typeof props === 'string') { 9 | return false; 10 | } 11 | return props && 'render' in props; 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/stories/background.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Background, BackgroundProvider } from '../src/components/background'; 3 | import { withKnobs } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | export default { 7 | title: 'Background', 8 | decorators: [withKnobs, withInfo], 9 | }; 10 | 11 | export const BasicUsage = () => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/ui/stories/button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, ButtonHandler } from '../src/components/button'; 3 | 4 | import { action } from '@storybook/addon-actions'; 5 | import { withKnobs, text } from '@storybook/addon-knobs'; 6 | import { withInfo } from '@storybook/addon-info'; 7 | import { ThemeProvider } from 'styled-components'; 8 | export default { 9 | title: 'Button', 10 | decorators: [withKnobs, withInfo], 11 | }; 12 | 13 | export const BasicUsage = () => { 14 | const label = text('Label', 'See now'); 15 | return ( 16 | 17 | 28 | 40 | 52 | 64 | 65 | ) 66 | }; 67 | export const BasicUsage = () => { 68 | return ( 69 | 70 | 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /packages/ui/stories/sidebar.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Sidebar } from '../src/components/sidebar'; 3 | import { withKnobs, text, select } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | import { MemoryRouter } from 'react-router-dom'; 6 | import { SidebarProvider } from '../src/context/sidebar.context'; 7 | import { IMenuItemProps } from '../src/components/sidebar/types'; 8 | import { iconLists } from './data/icons'; 9 | 10 | export default { 11 | title: 'Sidebar', 12 | decorators: [withKnobs, withInfo], 13 | }; 14 | 15 | const routes: IMenuItemProps[] = [ 16 | { 17 | path: '/app/dashboard', 18 | icon: 'HiOutlineHome', 19 | name: 'Dashboard', 20 | exact: true, 21 | }, 22 | { 23 | path: '/app/forms', 24 | icon: 'HiOutlineNewspaper', 25 | name: 'Forms', 26 | }, 27 | { 28 | path: '/app/charts', 29 | icon: 'HiOutlineChartPie', 30 | name: 'Charts', 31 | }, 32 | ]; 33 | export const BasicUsage = () => { 34 | const label = text('Label', 'SideBar Title'); 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export const CustomTitleUsage = () => { 43 | const label = text('Label', 'Logo'); 44 | 45 | return ( 46 | 47 |

{label}

}} 49 | menuItems={routes as IMenuItemProps[]} 50 | /> 51 |
52 | ); 53 | }; 54 | 55 | export const Knobs = () => { 56 | const label = text('Label', 'Custom Knobs'); 57 | 58 | return ( 59 | 60 | 61 | 81 | 82 | 83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /packages/ui/stories/table.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { withKnobs } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | import makeData from './data/table'; 6 | import { Table } from '../src/components/table'; 7 | export default { 8 | title: 'Table', 9 | decorators: [withKnobs, withInfo], 10 | }; 11 | 12 | export const BasicUsage = () => { 13 | const columns = React.useMemo( 14 | () => [ 15 | { 16 | Header: 'First Name', 17 | accessor: 'firstName', 18 | }, 19 | { 20 | Header: 'Last Name', 21 | accessor: 'lastName', 22 | }, 23 | { 24 | Header: 'Age', 25 | accessor: 'age', 26 | }, 27 | { 28 | Header: 'Visits', 29 | accessor: 'visits', 30 | }, 31 | { 32 | Header: 'Status', 33 | accessor: 'status', 34 | }, 35 | { 36 | Header: 'Profile Progress', 37 | accessor: 'progress', 38 | }, 39 | ], 40 | [], 41 | ); 42 | 43 | const data = React.useMemo(() => makeData(100), []); 44 | return ; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/ui/stories/theme-switcher.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { withKnobs } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | import { ThemeProvider } from '../src/context/theme.context'; 6 | import { ThemeSwitchButton } from '../src/components/theme-switcher/'; 7 | 8 | export default { 9 | title: 'Theme Switcher', 10 | decorators: [withKnobs, withInfo], 11 | }; 12 | 13 | export const BasicUsage = () => { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitThis": false, 5 | "target": "es2017", 6 | "baseUrl": "src", 7 | "jsx": "react", 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "outDir": "dist", 12 | "strict": true, 13 | "moduleResolution": "node", 14 | "noEmit": false 15 | 16 | }, 17 | "include": ["src"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node" 6 | ] 7 | }, 8 | "files": [ 9 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", 10 | "../../node_modules/@nrwl/react/typings/image.d.ts" 11 | ], 12 | "exclude": [ 13 | "**/*.spec.ts", 14 | "**/*.spec.tsx", 15 | "**/*.stories.tsx", 16 | "**/*.stories.js", 17 | "**/*.stories.ts", 18 | "**/*.stories.js" 19 | ], 20 | "include": [ 21 | "**/*.js", 22 | "**/*.jsx", 23 | "**/*.ts", 24 | "**/*.tsx" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/ui/tsdx.config.js: -------------------------------------------------------------------------------- 1 | const postcss = require('rollup-plugin-postcss'); 2 | const autoprefixer = require('autoprefixer'); 3 | const cssnano = require('cssnano'); 4 | 5 | module.exports = { 6 | rollup(config, options) { 7 | config.plugins.push( 8 | postcss({ 9 | plugins: [ 10 | autoprefixer(), 11 | cssnano({ 12 | preset: 'default', 13 | }), 14 | ], 15 | inject: false, 16 | modules: true, 17 | 18 | // only write out CSS for the first bundle (avoids pointless extra files): 19 | extract: 'style.css', 20 | }), 21 | ); 22 | return config; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /patches/nestjs-flub+0.2.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/nestjs-flub/dist/flub-error-handler.js b/node_modules/nestjs-flub/dist/flub-error-handler.js 2 | index 50dc711..1ed4ed9 100644 3 | --- a/node_modules/nestjs-flub/dist/flub-error-handler.js 4 | +++ b/node_modules/nestjs-flub/dist/flub-error-handler.js 5 | @@ -22,7 +22,7 @@ let FlubErrorHandler = class FlubErrorHandler { 6 | .then(data => { 7 | const ctx = host.switchToHttp(); 8 | const response = ctx.getResponse(); 9 | - response.status(500).send(data); 10 | + response.status(500).type('text/html').send(data); 11 | }) 12 | .catch(e => { 13 | common_2.Logger.error(e.message, e.context); 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | const path = require('path'); 3 | 4 | const NODE_ENV = process.env.NODE_ENV || 'development'; 5 | const isDev = NODE_ENV === 'development'; 6 | 7 | module.exports = { 8 | plugins: [ 9 | require('postcss-easy-import')({ prefix: '_' }), 10 | require('autoprefixer'), 11 | tailwindcss(path.resolve(__dirname, './tailwind.config.js')), 12 | 13 | // require('@fullhuman/postcss-purgecss')({ 14 | // content: ['./**/*.tsx', './**/*.js'], 15 | // defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || [], 16 | // }), 17 | require('postcss-preset-env')({ 18 | stage: 1, 19 | features: { 20 | 'focus-within-pseudo-class': false, 21 | }, 22 | }), 23 | require('cssnano'), 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /protos/rpc/rpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package rpc; 3 | 4 | import "rpc/subscription_plan.proto"; 5 | 6 | service SubscriptionPlanService { 7 | 8 | rpc FindAll(Empty) returns (subscription_plan.SubscriptionPlanResponse) {} 9 | 10 | rpc FindOne(subscription_plan.SubscriptionPlanFilter) returns (subscription_plan.SubscriptionPlan){} 11 | 12 | rpc Create(subscription_plan.SubscriptionPlanInput) returns (subscription_plan.SubscriptionPlan){} 13 | 14 | rpc Update(subscription_plan.UpdateSubscriptionPlanRequest) returns (subscription_plan.UpdateSubscriptionPlanResponse){} 15 | 16 | rpc Delete(subscription_plan.SubscriptionPlanFilter) returns (subscription_plan.DeleteSubscriptionPlanResponse){} 17 | 18 | } 19 | message Empty {} 20 | 21 | -------------------------------------------------------------------------------- /protos/rpc/subscription_plan.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package subscription_plan; 4 | 5 | message SubscriptionPlanInput { 6 | enum SubscriptionDuration { 7 | DAY = 0; 8 | WEEK = 1; 9 | MONTH = 2; 10 | YEAR = 3; 11 | } 12 | 13 | string name = 1; 14 | string code = 2; 15 | string description = 3; 16 | int32 price = 4; 17 | int32 extra_fee = 5; 18 | int32 invoice_period = 6; 19 | SubscriptionDuration invoice_duration = 7; 20 | int32 trail_period = 8; 21 | SubscriptionDuration trail_duration = 9; 22 | } 23 | 24 | 25 | message SubscriptionPlan { 26 | string id = 1; 27 | string name = 2; 28 | string code = 3; 29 | string slug = 4; 30 | string description = 5; 31 | int32 price = 6; 32 | int32 extra_fee = 7; 33 | int32 invoice_period = 8; 34 | string invoice_duration = 9; 35 | int32 trail_period = 10; 36 | string trail_duration = 11; 37 | 38 | } 39 | message DeleteSubscriptionPlanResponse { 40 | int32 modified = 1; 41 | repeated SubscriptionPlan edges = 2; 42 | } 43 | 44 | message UpdateSubscriptionPlanRequest { 45 | SubscriptionPlanInput payload = 1; 46 | SubscriptionPlanFilter where = 2; 47 | } 48 | 49 | message UpdateSubscriptionPlanResponse { 50 | int32 modified = 1; 51 | repeated SubscriptionPlan edges = 2; 52 | } 53 | 54 | message SubscriptionPlanResponse { 55 | int32 page_info = 1; 56 | repeated SubscriptionPlan edges = 2; 57 | } 58 | message Pagination { 59 | int32 total = 1; 60 | bool has_more =2; 61 | int32 limit =3; 62 | int32 skip = 4; 63 | } 64 | message SubscriptionPlanFilter { 65 | string id = 1; 66 | } 67 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | // tailwind.config.js 2 | module.exports = { 3 | purge: { 4 | enabled: false, 5 | content: ['./packages/**/**/*.tsx'], 6 | }, 7 | theme: { 8 | themeVariants: ['dark'], 9 | colors: { 10 | transparent: 'transparent', 11 | black: '#16171A', 12 | white: '#FFFFFF', 13 | primary: '#7B16FF', 14 | secondary: '#24292E', 15 | hello: 'red', 16 | purple: { 17 | 100: '#F8F6FD', 18 | 200: '#E8E5FF', 19 | 300: '#DDD9FF', 20 | 400: '#5856D6', 21 | 600: '#7e3af2', 22 | }, 23 | red: { 24 | 100: '#ea4335', 25 | 200: '#fbd5d5', 26 | 300: '#f8b4b4', 27 | 500: '#85000C', 28 | }, 29 | gray: { 30 | default: '#67717A', 31 | 100: '#FAFAFA', 32 | 200: '#F6F7F8', 33 | 300: '#EBECED', 34 | 400: '#DFE7EF', 35 | 500: '#a0aec0', 36 | 600: '#718096', 37 | 700: '#4a5568', 38 | 800: '#2d3748', 39 | 900: '#1a202c', 40 | }, 41 | }, 42 | }, 43 | variants: { 44 | backgroundColor: [ 45 | 'hover', 46 | 'focus', 47 | 'active', 48 | 'odd', 49 | 'dark', 50 | 'dark:hover', 51 | 'dark:focus', 52 | 'dark:active', 53 | 'dark:odd', 54 | ], 55 | display: ['responsive', 'dark'], 56 | textColor: [ 57 | 'focus-within', 58 | 'hover', 59 | 'active', 60 | 'dark', 61 | 'dark:focus-within', 62 | 'dark:hover', 63 | 'dark:active', 64 | ], 65 | zIndex: { 66 | '60': '60', 67 | }, 68 | placeholderColor: ['focus', 'dark', 'dark:focus'], 69 | borderColor: ['focus', 'hover', 'dark', 'dark:focus', 'dark:hover'], 70 | divideColor: ['dark'], 71 | boxShadow: ['focus', 'dark:focus'], 72 | }, 73 | plugins: [require('tailwindcss-multi-theme')], 74 | }; 75 | -------------------------------------------------------------------------------- /tools/schematics/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asimashfaq/fullstack-starter/69bc399e482b76251a347968aae9f09c3a4abcdc/tools/schematics/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"] 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "checkJs": false, 6 | "lib": ["dom", "dom.iterable", "esnext","es2017"], 7 | "noImplicitAny": false, // Remove any usage 8 | "outDir": "dist", 9 | "removeComments": true, 10 | "sourceMap": true, 11 | "typeRoots": ["./node_modules/@types"], 12 | //"declaration": true, 13 | "emitDecoratorMetadata": true, 14 | "importHelpers": true, 15 | "target": "es2015", 16 | "skipDefaultLibCheck": true, 17 | "baseUrl": ".", 18 | 19 | "module": "esnext", 20 | "strict": true, 21 | "jsx": "preserve", 22 | "forceConsistentCasingInFileNames": true, 23 | "noEmit": true, 24 | "esModuleInterop": true, 25 | "allowSyntheticDefaultImports": true, 26 | "skipLibCheck": true, 27 | "moduleResolution": "node", 28 | "resolveJsonModule": true, 29 | "isolatedModules": true, 30 | "allowJs": true, 31 | "experimentalDecorators":true 32 | }, 33 | "include": ["packages", "postcss.config.js", "tailwind.config.js"], 34 | } --------------------------------------------------------------------------------