├── README.md ├── api ├── .dockerignore ├── .env.sample ├── .gitignore ├── .graphqlconfig.yaml ├── .nvmrc ├── GENERATE_SSL.md ├── Procfile ├── README.md ├── local.admin-panel.crt ├── local.admin-panel.key ├── migrations │ ├── 20201202122403-first │ │ ├── README.md │ │ ├── schema.prisma │ │ └── steps.json │ └── migrate.lock ├── package.json ├── schema.prisma ├── seeds │ └── index.ts ├── src │ ├── generated │ │ ├── nexus.ts │ │ └── schema.graphql │ ├── index.ts │ ├── mail.js │ ├── schema │ │ ├── enumTypes.ts │ │ ├── generated │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── inputTypes.ts │ │ ├── mutations.ts │ │ ├── mutations │ │ │ ├── completeOnboarding.ts │ │ │ ├── createCategory.ts │ │ │ ├── createProduct.ts │ │ │ ├── deleteAccount.ts │ │ │ ├── deleteCategory.ts │ │ │ ├── login.ts │ │ │ ├── logout.ts │ │ │ ├── requestPasswordReset.ts │ │ │ ├── resetPassword.ts │ │ │ ├── signup.ts │ │ │ ├── updateCategory.ts │ │ │ ├── updateProduct.ts │ │ │ └── updateUser.ts │ │ ├── objectTypes.ts │ │ ├── queries.ts │ │ └── scarlarTypes.ts │ └── utils │ │ ├── analytics.ts │ │ ├── auth.ts │ │ ├── constants.ts │ │ ├── generateToken.ts │ │ ├── mail.ts │ │ ├── stripe.ts │ │ ├── verifyEnvironmentVariables.ts │ │ └── verifyUserIsAuthenticated.ts ├── tsconfig.json ├── typings │ ├── amplitude.ts │ └── global.ts └── yarn.lock └── web-app ├── .babelrc ├── .dockerignore ├── .env.example ├── .gitignore ├── .graphqlconfig.yaml ├── .nvmrc ├── .prettierrc.js ├── .vscode └── settings.json ├── Dockerfile ├── README.md ├── apollo.config.js ├── assets ├── icons │ ├── clear.svg │ ├── google.svg │ ├── info.svg │ ├── logo.png │ ├── rightArrow.svg │ └── star-unfilled.svg └── images │ ├── earning.svg │ ├── image.svg │ ├── refund.svg │ └── user.jpg ├── components ├── AllSvgIcon-2.tsx ├── AllSvgIcon.tsx ├── App │ └── App.tsx ├── Category │ ├── AddCategory.tsx │ ├── Category.tsx │ ├── CategoryMutation.ts │ ├── CategoryQuery.ts │ └── ParentCategories.tsx ├── Dashboard │ ├── Customers.tsx │ ├── DailyVisitsInsight.tsx │ ├── Dashboard.tsx │ ├── LatestOrders.tsx │ ├── LatestProducts.tsx │ ├── ProfitAnalysis.tsx │ ├── Resolution.tsx │ ├── Sale.tsx │ ├── SaleCategoryAnalysis.tsx │ ├── TotalProfit.tsx │ └── TrafficByDevice.tsx ├── Layout │ ├── HeaderUserBox.tsx │ ├── Layout.tsx │ ├── Sidebar.tsx │ └── Topbar.tsx ├── Loader │ └── Loader.tsx ├── LoginLayout │ └── LoginLayout.tsx ├── LoginOrSignup │ └── LoginOrSignup.tsx ├── Material │ ├── ErrorMessage.js │ ├── Snackbar.tsx │ └── SuccessMessage.js ├── Product │ ├── AddProduct.tsx │ ├── AddProductStyle.tsx │ ├── EditProduct.tsx │ ├── Product.tsx │ ├── ProductMutation.ts │ ├── ProductQuery.ts │ └── ProductStyle.tsx ├── Profile │ └── index.tsx └── User │ ├── User.tsx │ ├── UserMutation.ts │ └── UserQuery.ts ├── graphql ├── generated │ ├── CompleteOnboardingMutation.ts │ ├── CurrentUserQuery.ts │ ├── DELETE_CATEGORY_MUTATION.ts │ ├── DELETE_USER_MUTATION.ts │ ├── DeleteAccountMutation.ts │ ├── LoginMutation.ts │ ├── LogoutMutation.ts │ ├── RequestResetPasswordMutation.ts │ ├── ResetPasswordMutation.ts │ ├── SignupMutation.ts │ ├── UPDATE_CATEGORY_MUTATION.ts │ ├── UPDATE_PRODUCT_MUTATION.ts │ ├── UPDATE_USER_MUTATION.ts │ ├── categories.ts │ ├── createCategory.ts │ ├── createProduct.ts │ ├── graphql-global-types.ts │ ├── isLeftDrawerOpen.ts │ ├── products.ts │ ├── snackbar.ts │ └── users.ts ├── mutations │ └── index.ts └── queries │ └── index.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── add-category.tsx ├── add-product.tsx ├── categories.tsx ├── example.tsx ├── example1.tsx ├── index.tsx ├── login.tsx ├── products.tsx ├── reset-password.tsx ├── settings.tsx ├── signup.tsx └── users.tsx ├── public ├── images │ ├── adminLogo.png │ ├── conversation.svg │ ├── customers.svg │ ├── discuss-issue.svg │ ├── earning.svg │ ├── image.svg │ ├── money-bag.svg │ ├── no-product-image.png │ ├── product │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg │ ├── refund.svg │ ├── sales.svg │ ├── target.svg │ └── user.jpg └── static │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon.ico │ ├── fonts │ ├── Rubik-Black.woff │ ├── Rubik-Black.woff2 │ ├── Rubik-BlackItalic.woff │ ├── Rubik-BlackItalic.woff2 │ ├── Rubik-Bold.woff │ ├── Rubik-Bold.woff2 │ ├── Rubik-BoldItalic.woff │ ├── Rubik-BoldItalic.woff2 │ ├── Rubik-Italic.woff │ ├── Rubik-Italic.woff2 │ ├── Rubik-Light.woff │ ├── Rubik-Light.woff2 │ ├── Rubik-LightItalic.woff │ ├── Rubik-LightItalic.woff2 │ ├── Rubik-Medium.woff │ ├── Rubik-Medium.woff2 │ ├── Rubik-MediumItalic.woff │ ├── Rubik-MediumItalic.woff2 │ ├── Rubik-Regular.woff │ └── Rubik-Regular.woff2 │ └── manifest.json ├── routes.js ├── schema.graphql ├── tests └── pages.test.tsx ├── theme └── index.tsx ├── tsconfig.json ├── types ├── ContactType.ts ├── index.ts └── typeDefs │ ├── custom.d.ts │ ├── next-env.d.ts │ └── react-tippy.d.ts ├── utils ├── constants │ └── index.ts ├── formatDate.ts ├── getError.ts ├── hooks │ ├── useGooglePlacesScript.tsx │ ├── useGooglePlacesService.tsx │ ├── useModalQuery.tsx │ ├── useOnClickOutside.tsx │ ├── usePaginationQuery.tsx │ └── useWindowResize.tsx ├── resolvers.tsx ├── testUtils.tsx └── withApollo.ts └── yarn.lock /README.md: -------------------------------------------------------------------------------- 1 | # NextJS/ GraphQL Admin Panel inspired by Material UI 2 | 3 | #### Tech Stack Frontend 4 | * React(Next JS) 5 | * Typescript 6 | * Apollo Client 7 | * GraphQL 8 | * Material UI 9 | 10 | #### Tech Stack Backend 11 | * GraphQL 12 | * Prisma 2 13 | * Nexus 14 | * Typescript 15 | * Mysql 16 | 17 | ## Install VS Code extensions 🚀 18 | 19 | Please installed these VS Code extensions for syntax highlighting and auto-formatting - [Prisma](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma) and [GraphQL](https://marketplace.visualstudio.com/items?itemName=Prisma.vscode-graphql). 20 | 21 | ![Prisma Extension for VS Code](https://i.imgur.com/IuyHd6e.png)
Prisma Extension for VS Code
22 | 23 | ![GraphQL Extension for VS Code](https://i.imgur.com/Lbsk1TY.png)
GraphQL Extension for VS Code
24 | 25 | ## Install Frontend 26 | [Check Readme file for install instructions](https://github.com/dvikas/next-graphql-admin/tree/main/web-app) 27 | 28 | ## Install Backend 29 | [Check Readme file for install instructions](https://github.com/dvikas/next-graphql-admin/tree/main/api) 30 | 31 | ## Screenshots 32 | ![Dashboard](https://i.imgur.com/rZs1OEo.png)
Dashboard
33 | 34 | ![Products](https://i.imgur.com/MEBPjdL.png)
Products
35 | 36 | ![Edit Product](https://i.imgur.com/1tqhaLP.png)
Edit Product
37 | 38 | ![Categories](https://i.imgur.com/JWuO2Zt.png)
Categories
39 | 40 | ## Contributing 41 | Please share your opinion/ feedback for improvements! As you know it's open source project, don't hesitate to contribute your thoughts. Also, you can send email on vikas.nice@gmail.com. 42 | 43 | Feel free to create a PR for your changes :) 44 | -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .now 4 | .env* -------------------------------------------------------------------------------- /api/.env.sample: -------------------------------------------------------------------------------- 1 | COMMON_FRONTEND_URL=http://local.app.nextgraphqladmin.com:3000 2 | COMMON_BACKEND_URL=https://local.api.nextgraphqladmin.com:4000 3 | HTTPS_KEY=./local.admin-panel.key 4 | HTTPS_CERT=./local.admin-panel.crt 5 | # COMMON_STRIPE_YEARLY_PLAN_ID=plan_GTLU37cHfQQYg5 6 | # COMMON_STRIPE_MONTHLY_PLAN_ID=plan_GpOi3czoSsBCuT 7 | 8 | # Locally, we use localstack to run an S3 bucket locally. We can use any value for the API_AWS_ACCESS_KEY_ID and API_AWS_ACCESS_KEY to get localstack to work 9 | 10 | API_AWS_ACCESS_KEY_ID='ACCESSKEYAWSUSER' 11 | API_AWS_ACCESS_KEY='sEcreTKey' 12 | API_AWS_BUCKET_NAME='demo-bucket' 13 | 14 | API_COOKIE_DOMAIN=.nextgraphqladmin.com 15 | API_GOOGLE_CLIENT_ID=39819969479-dnjepvpvtb1tufhr2rltpvhjlhtdrse8.apps.googleusercontent.com 16 | API_GOOGLE_CLIENT_SECRET=gsoGwfoRUKvmeEl4fzHLhh94 17 | API_APP_SECRET=4WRCZH2aYciMyk 18 | API_PORT=4000 19 | IS_DEMO_ACCOUNT=false 20 | 21 | API_STRIPE_API_KEY=sk_test_WEEHCPAAw4mYtS71SZcQ9LCn00VuDg7wVY 22 | API_STRIPE_WEBHOOK_SECRET=whsec_9Xg0jAaKxFLPuQcgkyD05fKW0b5pucTb 23 | API_CLOUDFRONT_DOMAIN=https://dev.assets.nextgraphqladmin.com 24 | API_AMPLITUDE_API_KEY=a04e243ac303609207a6ec1afbd9bd36 25 | API_DATABASE_URL=mysql://root:secret@localhost:3306/next_graphql_admin 26 | 27 | MAIL_HOST="smtp.sendgrid.net" 28 | MAIL_PORT=25 29 | MAIL_USER="apikey" 30 | MAIL_PASS="YOUR KEY" 31 | FROM_EMAIL="support@nextgraphqladmin.com" -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode 3 | node_modules 4 | -------------------------------------------------------------------------------- /api/.graphqlconfig.yaml: -------------------------------------------------------------------------------- 1 | projects: 2 | Web App: 3 | schemaPath: src/generated/schema.graphql 4 | extensions: 5 | endpoints: 6 | local: 'http://local.api.nextgraphqladmin.com:4000' 7 | -------------------------------------------------------------------------------- /api/.nvmrc: -------------------------------------------------------------------------------- 1 | 12.16.1 -------------------------------------------------------------------------------- /api/GENERATE_SSL.md: -------------------------------------------------------------------------------- 1 | ### Following Steps is for Mac OS 2 | * `sudo openssl genrsa -out ./localhost.key 2048` 3 | * `sudo openssl rsa -in ./localhost.key -out ./localhost.key.rsa` 4 | * `sudo vim ./localhost.conf` 5 | Copy following contents 6 | ```javascript 7 | [req] 8 | default_bits = 1024 9 | distinguished_name = req_distinguished_name 10 | req_extensions = v3_req 11 | 12 | [req_distinguished_name] 13 | 14 | [v3_req] 15 | basicConstraints = CA:FALSE 16 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 17 | subjectAltName = @alt_names 18 | 19 | [alt_names] 20 | DNS.1 = local.api.nextgraphqladmin.com 21 | DNS.2 = *.nextgraphqladmin.com 22 | ``` 23 | 24 | * `sudo openssl req -new -key ./localhost.key.rsa -subj "/C=/ST=/L=/O=/CN=localhost/" -out ./localhost.csr -config ./localhost.conf` 25 | * `sudo openssl x509 -req -extensions v3_req -days 365 -in ./localhost.csr -signkey ./localhost.key.rsa -out ./localhost.crt -extfile ./localhost.conf` 26 | * `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ./localhost.crt` 27 | 28 | 29 | #### Update .env file 30 | ``` 31 | HTTPS_KEY=./localhost.key 32 | HTTPS_CERT=./localhost.crt 33 | ``` 34 | #### Resource: 35 | https://www.webconsol.com/post/how-to-generate-ssl-certificate-locally-for-macos 36 | -------------------------------------------------------------------------------- /api/Procfile: -------------------------------------------------------------------------------- 1 | web: node dist/index.js 2 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | A Node.js GraphQL server built with [GraphQL Yoga](https://github.com/prisma/graphql-yoga). 4 | 5 | ## Getting started 6 | 7 | ### Pre-requisites 8 | 9 | The following must be installed locally in order to run the api and it's backing services: 10 | 11 | - yarn (https://classic.yarnpkg.com/en/docs/install/#mac-stable) 12 | - node (https://nodejs.org/en/download/) 13 | 14 | ### Host file 15 | 16 | Add the following line to your `/etc/hosts` file in order to alias your localhost to *local.app.nextgraphqladmin.com* 17 | 18 | ```text 19 | 127.0.0.1 local.api.nextgraphqladmin.com 20 | ``` 21 | ### Install Instructions 22 | * Copy **.env.sample** and create **.env** file. Local environment variables are set in the .env file. 23 | 24 | Must use **yarn** in order to use *yarn.lock*. (Do not delete `yarn.lock`) 25 | 26 | * Open MYSQL and Create Database `next_graphql_admin` 27 | * Open Terminal in `api` Directory and use following commands 28 | * `yarn` 29 | 30 | > If you aree getting error `The engine "node" is incompatible with this module. Expected version "12". Got "14.17.3"` 31 | 32 | > `nvm use --delete-prefix v12.0` 33 | 34 | > `nvm install v12.0` 35 | 36 | * `npx prisma migrate save --experimental` 37 | * `npx prisma migrate up --experimental` (It will create DB Tables) 38 | * `yarn seed` (it will create test user **admin@example.com / admin** in DB user table) 39 | 40 | ### Starting the server 41 | 42 | Run the following commands to get started. 43 | 44 | ```bash 45 | yarn dev # Starts the local api server accessible at http://local.api.nextgraphqladmin.com:4000 46 | ``` 47 | > **Note** Generate SSL certificate for your local https://local.api.nextgraphqladmin.com:4000/ to make it working. 48 | Please follow https://github.com/dvikas/nextjs-graphql-adminpanel/blob/main/api/GENERATE_SSL.md 49 | 50 | ### Dealing with Stripe subscriptions 51 | 52 | In order to handle Stripe webhooks that are triggered when a user upgrades to premium, or cancels their subscription, the Stripe CLI should be used to listen to incoming Stripe webhooks to the local server. Run `yarn stripe` to handle this. If upgrading a user to premium using a fake credit card (see the [Stripe docs](https://stripe.com/docs/billing/subscriptions/set-up-subscription#test-integration) for the card numbers that will work), it won't be possible to cancel the user subscription through the UI (it will have to be done through the Prisma GraphQL playground to access the DB directly). 53 | 54 | Make sure that if you're running stripe locally, that you get a webhook secret by executing `yarn stripe` and copy/pasting it in the `.env` file, otherwise the API won't be able to read the stripe event. 55 | 56 | ## GraphQL Playground 57 | 58 | Once the GraphQL API server is running, visit http://local.api.nextgraphqladmin.com:4000/ to view the GraphQL playground. The playground allows for calls to queries and mutations that have been defined in this repository. 59 | 60 | ## Prisma & Nexus 61 | 62 | This project uses [Prisma](https://www.prisma.io/) to generate a CRUD API to interact with the back-end database. Nexus is used to generate a fully typed back-end API. 63 | 64 | ```bash 65 | yarn generate # Generates the prisma API, API graphql schema, and API types 66 | yarn generate:prisma # Generates the prisma API 67 | yarn generate:nexus # Generates a graphql schema and types for the API 68 | ``` 69 | -------------------------------------------------------------------------------- /api/local.admin-panel.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+DCCAuCgAwIBAgIUXZhHJeMyrhI1Bk1egS0nbQQgZEkwDQYJKoZIhvcNAQEL 3 | BQAwfzELMAkGA1UEBhMCSU4xCzAJBgNVBAgMAlVQMQ4wDAYDVQQHDAVOb2lkYTEQ 4 | MA4GA1UECgwHYWRlcHRyYTEMMAoGA1UECwwDd2ViMQ4wDAYDVQQDDAV2aWthczEj 5 | MCEGCSqGSIb3DQEJARYUdmlrYXMubmljZUBnbWFpbC5jb20wHhcNMjAxMDE1MTU0 6 | ODQ5WhcNMjMwMTE4MTU0ODQ5WjB/MQswCQYDVQQGEwJJTjELMAkGA1UECAwCVVAx 7 | DjAMBgNVBAcMBU5vaWRhMRAwDgYDVQQKDAdhZGVwdHJhMQwwCgYDVQQLDANXZWIx 8 | DjAMBgNVBAMMBXZpa2FzMSMwIQYJKoZIhvcNAQkBFhR2aWthcy5uaWNlQGdtYWls 9 | LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOu0NG/2FbFQLWp5 10 | NBExO/rDpu7NT3DwC7iGRSUlL+j7D1NL8ZE0vqq61dVEFCMsVvrAmQC0KocUZInA 11 | D8C8WQj7ag/XXPM/0wUE4sezvVQwS9x8F4ii+RwmFeMswUfj+Ii4HNgtJNLcD/c7 12 | jZuAiYw9GiXs7App5AuoKVyUgi/y6z2Jawtbv+Jup1E64le2V/6tdgsML8YcukZS 13 | HvPfaIxy5vCCCYfAjg8DSShR7hw8VJ+VpWhMg4ElBDQ0G+nd7PVvdVF541/CDFaa 14 | iGCIun7xEEQ/y62Q/FU/gkO/HnXsgXEkFJmAj4QT+gpO2QI0fiT2Q24RUYswaOh0 15 | vUsf69ECAwEAAaNsMGowHwYDVR0jBBgwFoAUWpPoLbpa9uzAnguWC+qURtcY6jgw 16 | CQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwLwYDVR0RBCgwJoILY29nbmV0aS5jb22C 17 | EWxvY2FsLmNvZ25ldGkuY29thwTAqAANMA0GCSqGSIb3DQEBCwUAA4IBAQBCI/7z 18 | QO4Se2DQiTf1HffFJah+NkT/qDL0eIAfaNMxB14v6tgKhFdgSTJ2cPqLl2a8FXL9 19 | mWc+QEceEAkZev9gSl/kLdwerfvmKRlX9kRnLvwpT3j95NF+B/+M0UxUCHPzFeqB 20 | 4Uz9m6Dd/plmRfpUveAzIdshnT32vuuguDuao8OjGMYy+IPBd00kI6FCt+M5seNF 21 | YUt8mW+zTMnJs240Bew+mOvjACIO+nS/DK4fmtKaHgMZTdSJagH5Na3d1dssunAj 22 | S79WlSt6Bwayvr+5CUrGhh/rnXrm+62p+10IKYsuK2kbg9305ibZULEE5KXb3ROp 23 | J0x2nblxKGsJL3w4 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /api/local.admin-panel.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA67Q0b/YVsVAtank0ETE7+sOm7s1PcPALuIZFJSUv6PsPU0vx 3 | kTS+qrrV1UQUIyxW+sCZALQqhxRkicAPwLxZCPtqD9dc8z/TBQTix7O9VDBL3HwX 4 | iKL5HCYV4yzBR+P4iLgc2C0k0twP9zuNm4CJjD0aJezsCmnkC6gpXJSCL/LrPYlr 5 | C1u/4m6nUTriV7ZX/q12Cwwvxhy6RlIe899ojHLm8IIJh8CODwNJKFHuHDxUn5Wl 6 | aEyDgSUENDQb6d3s9W91UXnjX8IMVpqIYIi6fvEQRD/LrZD8VT+CQ78edeyBcSQU 7 | mYCPhBP6Ck7ZAjR+JPZDbhFRizBo6HS9Sx/r0QIDAQABAoIBAQDjyNBQTyqhpBFv 8 | 71gRMVp8ui4OZB3c0C8Tkbcq8ag+aLpjzmXS5X1J86uJIfSwFT6tsAltM7BRwLR0 9 | p0bSBXOqCYZzbrbmYYzmMdWUFzDmNpJprwbzRkSmHmxSkkLr3fWm8v71L5OBr6hC 10 | TqxIVk0XWUl202M9oR4A4e+vB9pUyHwipwmiQZaQ+skk3VqxUhwc5Smx4CUsWyE9 11 | mpKohTJTkEAh2e+ubNyVeGb05cSFfMBAVeayr42fOwwj1YSvHX1rgrK5Wo8dqCoD 12 | +1K6JOmFFSB7gBfKUUxKv0NNEKL3axnX5MXtTOf3WnYbjJ3jOvfL7/hLWwsfPvui 13 | rGAEsCA9AoGBAPaQ8sl/zbvGpKjGNRtaLM7ign1vJGyb7R596kqeKYywS7FO+RRH 14 | eBM7PeFfxJeQwAD4T5UDCOy94N/lflpOd/2A6UYFl5ygZuZ+UfxJNgIoldWg3Pg9 15 | HLnEqHUrLVY/3AmFaIKvtxpo/GYZqWmcsJQqdym6ROAZfT2a2Jt/g3qnAoGBAPS4 16 | 3P407w6chW/9wsI6755ZyHDkAdQ6Y51Q2iLXmLh+UuvAPNx2z8DagEgNcOWZJ5M/ 17 | 97jNlkVTT+5LcgfOT3zs1XKI7M3KL/4jxC9gvUnp+sv7X72feW9zdg5eLU5SCOFr 18 | yviAvoCnG/g89t4WWWqSL4EhuTOIG7lZNdvKrkzHAoGBALVhrekDRoJTQAAURy8G 19 | 6B2NTbcekqn/DrE2qasYrLIdYqFd2ifL544mL4Bi5gklZ8mO4WRaJi+aAxpSBeBD 20 | B0wKkBB9vqlu6iO3W3J/HOb7mjXcL5HByybxf4cqKyDeu2yZomc5AjbAcqRdTl4t 21 | 8Uwd7SlaKJ6+wX4XMi8536vTAoGBAOAvPdvumBT1lFQczr7qCLsymqm4ZmiKONlT 22 | yRFkGjbhGot3pwl8GiQcxqm7DnJ21EdTsVbtlzzY7n9pRAQcnrrdp0fuYajAESkq 23 | kL2qTJ2aqDMXjASFRFSyHDNbWPvHsPT4r47pOhtXewr0pl6bcLxtQPF1+FhZ1rP8 24 | Ipe/297fAoGAMrHsoAGQbdvq7/DDP1VeeCKOBRJRQE60pBzNFsaJ2ae27n5nbOW2 25 | c4Vra90Pb6UVp1MHLK6Pw7j7SqjatdkYJDUs7uXhHzj+Dc3NpNi7aiFSKqaluPZr 26 | 3rLMEOzmYFvZAxL31MAa4xYyrDhjcmIsea+p7YMz9Ls4onN02T/UQD8= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /api/migrations/20201202122403-first/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mysql" 7 | url = "***" 8 | } 9 | 10 | model Card { 11 | brand String 12 | expMonth Int 13 | expYear Int 14 | id String @id @default(cuid()) 15 | last4Digits String 16 | stripePaymentMethodId String 17 | } 18 | 19 | model GoogleMapsLocation { 20 | googlePlacesId String 21 | id String @id @default(cuid()) 22 | name String 23 | OrderDetail OrderDetail[] 24 | } 25 | 26 | model OrderDetail { 27 | 28 | id String @id @default(cuid()) 29 | googleMapsLocationId String? 30 | User User? @relation(fields: [userId], references: [id]) 31 | userId String? 32 | GoogleMapsLocation GoogleMapsLocation? @relation(fields: [googleMapsLocationId], references: [id]) 33 | 34 | } 35 | 36 | model Product { 37 | category String 38 | createdAt DateTime @default(now()) 39 | description String 40 | discount Int 41 | id String @id @default(cuid()) 42 | name String 43 | price Int 44 | salePrice Int 45 | sku String @unique 46 | unit String 47 | updatedAt DateTime @default(now()) 48 | user String 49 | Category Category @relation(fields: [category], references: [id]) 50 | User User @relation(fields: [user], references: [id]) 51 | CartItem CartItem[] 52 | ProductImages ProductImage[] 53 | 54 | @@index([category], name: "category") 55 | @@index([user], name: "user") 56 | } 57 | 58 | model ProductImage { 59 | createdAt DateTime @default(now()) 60 | id String @id @default(cuid()) 61 | image String 62 | productId String 63 | updatedAt DateTime @default(now()) 64 | Product Product @relation(fields: [productId], references: [id]) 65 | 66 | @@index([productId], name: "ProductImage_ibfk_1") 67 | } 68 | 69 | enum User_role { 70 | USER 71 | ADMIN 72 | MANAGER 73 | } 74 | 75 | enum User_status { 76 | INACTIVE 77 | ACTIVE 78 | BLOCKED 79 | } 80 | 81 | model User { 82 | name String 83 | email String @unique 84 | emailConfirmationToken String? 85 | googleId String? @unique 86 | hasCompletedOnboarding Boolean @default(false) 87 | hasVerifiedEmail Boolean? 88 | id String @id @default(cuid()) 89 | password String? 90 | resetToken String? 91 | resetTokenExpiry Float? 92 | role User_role @default(USER) 93 | status User_status @default(ACTIVE) 94 | billing String? 95 | CartItem CartItem[] 96 | Product Product[] 97 | OrderDetail OrderDetail[] 98 | } 99 | 100 | ////////////////////////////////////////////////////// 101 | model CartItem { 102 | id String @id 103 | item String 104 | quantity Int 105 | user String 106 | User User @relation(fields: [user], references: [id]) 107 | 108 | Product Product? @relation(fields: [productId], references: [id]) 109 | productId String? 110 | @@index([item], name: "item") 111 | @@index([user], name: "user") 112 | } 113 | 114 | model Category { 115 | createdAt DateTime @default(now()) 116 | id String @id @default(cuid()) 117 | name String 118 | parent String 119 | slug String @unique 120 | updatedAt DateTime @default(now()) 121 | Product Product[] 122 | } 123 | -------------------------------------------------------------------------------- /api/migrations/migrate.lock: -------------------------------------------------------------------------------- 1 | # IF THERE'S A GIT CONFLICT IN THIS FILE, DON'T SOLVE IT MANUALLY! 2 | # INSTEAD EXECUTE `prisma migrate fix` 3 | # Prisma Migrate lockfile v1 4 | # Read more about conflict resolution here: TODO 5 | 6 | 20200806212456-ecommerce 7 | 20200807181657-user 8 | 20201202122403-first -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextgraphqladmin-backend", 3 | "description": "Back-end for Admin", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "postinstall": "yarn generate", 8 | "start": "ts-node src/index.ts", 9 | "dev": "ts-node-dev --no-notify --respawn --transpileOnly ./src/", 10 | "generate": "yarn generate:prisma && yarn generate:nexus", 11 | "generate:prisma": "prisma generate", 12 | "generate:nexus": "ts-node --transpile-only src/schema", 13 | "stripe": "stripe listen --forward-to local.api.nextgraphqladmin.com:4000/stripe", 14 | "seed": "ts-node ./seeds" 15 | }, 16 | "dependencies": { 17 | "@nexus/schema": "^0.13.1", 18 | "@prisma/client": "^2.0.0-beta.1", 19 | "amplitude": "^4.0.1", 20 | "aws-sdk": "^2.649.0", 21 | "bcrypt": "^3.0.7", 22 | "body-parser": "^1.19.0", 23 | "cookie-parser": "^1.4.4", 24 | "cuid": "^2.1.8", 25 | "date-fns": "^2.9.0", 26 | "dotenv": "^8.2.0", 27 | "dotenv-flow": "^3.1.0", 28 | "express-session": "^1.17.0", 29 | "formidable": "^1.2.1", 30 | "graphql": "^14.5.8", 31 | "graphql-upload": "^8.1.0", 32 | "graphql-yoga": "^1.18.3", 33 | "jsonwebtoken": "^8.5.1", 34 | "nexus": "^0.12.0", 35 | "nexus-prisma": "^0.12.0", 36 | "node-fetch": "^2.6.0", 37 | "nodemailer": "^6.4.14", 38 | "passport": "^0.4.0", 39 | "passport-google-oauth20": "^2.0.0", 40 | "stripe": "^7.15.0", 41 | "ts-node": "^8.8.2", 42 | "uuid": "^3.3.3" 43 | }, 44 | "devDependencies": { 45 | "fs": "0.0.1-security", 46 | "@prisma/cli": "2.0.0-beta.1", 47 | "@types/bcrypt": "^3.0.0", 48 | "@types/cookie-parser": "^1.4.2", 49 | "@types/express-session": "^1.17.0", 50 | "@types/faker": "^4.1.9", 51 | "@types/formidable": "^1.0.31", 52 | "@types/graphql-upload": "^8.0.3", 53 | "@types/jsonwebtoken": "^8.3.5", 54 | "@types/node": "^12.12.8", 55 | "@types/passport": "^1.0.1", 56 | "@types/passport-google-oauth20": "^2.0.3", 57 | "@types/stripe": "^7.13.19", 58 | "@types/uuid": "^3.4.6", 59 | "faker": "^4.1.0", 60 | "knex": "^0.20.10", 61 | "mysql": "^2.18.1", 62 | "now": "^16.7.0", 63 | "ts-node-dev": "^1.0.0-pre.44", 64 | "typescript": "3.8.3" 65 | }, 66 | "engines": { 67 | "node": "12", 68 | "yarn": "^1.16.0" 69 | }, 70 | "resolutions": { 71 | "graphql": "^14.5.8" 72 | } 73 | } -------------------------------------------------------------------------------- /api/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mysql" 7 | url = env("API_DATABASE_URL") 8 | } 9 | 10 | model Card { 11 | brand String 12 | expMonth Int 13 | expYear Int 14 | id String @id @default(cuid()) 15 | last4Digits String 16 | stripePaymentMethodId String 17 | } 18 | 19 | model GoogleMapsLocation { 20 | googlePlacesId String 21 | id String @id @default(cuid()) 22 | name String 23 | OrderDetail OrderDetail[] 24 | } 25 | 26 | model OrderDetail { 27 | 28 | id String @id @default(cuid()) 29 | googleMapsLocationId String? 30 | User User? @relation(fields: [userId], references: [id]) 31 | userId String? 32 | GoogleMapsLocation GoogleMapsLocation? @relation(fields: [googleMapsLocationId], references: [id]) 33 | 34 | } 35 | 36 | model Product { 37 | category String 38 | createdAt DateTime @default(now()) 39 | description String 40 | discount Int 41 | id String @id @default(cuid()) 42 | name String 43 | price Int 44 | salePrice Int 45 | sku String @unique 46 | unit String 47 | updatedAt DateTime @default(now()) 48 | user String 49 | Category Category @relation(fields: [category], references: [id]) 50 | User User @relation(fields: [user], references: [id]) 51 | CartItem CartItem[] 52 | ProductImages ProductImage[] 53 | 54 | @@index([category], name: "category") 55 | @@index([user], name: "user") 56 | } 57 | 58 | model ProductImage { 59 | createdAt DateTime @default(now()) 60 | id String @id @default(cuid()) 61 | image String 62 | productId String 63 | updatedAt DateTime @default(now()) 64 | Product Product @relation(fields: [productId], references: [id]) 65 | 66 | @@index([productId], name: "ProductImage_ibfk_1") 67 | } 68 | 69 | enum User_role { 70 | USER 71 | ADMIN 72 | MANAGER 73 | } 74 | 75 | enum User_status { 76 | INACTIVE 77 | ACTIVE 78 | BLOCKED 79 | } 80 | 81 | model User { 82 | name String 83 | email String @unique 84 | emailConfirmationToken String? 85 | googleId String? @unique 86 | hasCompletedOnboarding Boolean @default(false) 87 | hasVerifiedEmail Boolean? 88 | id String @id @default(cuid()) 89 | password String? 90 | resetToken String? 91 | resetTokenExpiry Float? 92 | role User_role @default(USER) 93 | status User_status @default(ACTIVE) 94 | billing String? 95 | CartItem CartItem[] 96 | Product Product[] 97 | OrderDetail OrderDetail[] 98 | } 99 | 100 | ////////////////////////////////////////////////////// 101 | model CartItem { 102 | id String @id 103 | item String 104 | quantity Int 105 | user String 106 | User User @relation(fields: [user], references: [id]) 107 | 108 | Product Product? @relation(fields: [productId], references: [id]) 109 | productId String? 110 | @@index([item], name: "item") 111 | @@index([user], name: "user") 112 | } 113 | 114 | model Category { 115 | createdAt DateTime @default(now()) 116 | id String @id @default(cuid()) 117 | name String 118 | parent String 119 | slug String @unique 120 | updatedAt DateTime @default(now()) 121 | Product Product[] 122 | } 123 | -------------------------------------------------------------------------------- /api/seeds/index.ts: -------------------------------------------------------------------------------- 1 | if (require.main === module) { 2 | // Only import the environment variables if executing this file directly: https://stackoverflow.com/a/6090287/8360496 3 | // The schema file gets executed directly when running the generate command: yarn generate 4 | // Without this check, we would be trying to load the environment variables twice and that causes warnings to be thrown in the console 5 | require('dotenv-flow').config(); 6 | } 7 | 8 | import cuid from 'cuid'; 9 | import bcrypt from 'bcrypt'; 10 | import faker from 'faker'; 11 | import knex from 'knex'; 12 | import subDays from 'date-fns/subDays'; 13 | import format from 'date-fns/format'; 14 | import { 15 | PrismaClient, 16 | } from '@prisma/client'; 17 | 18 | const prisma = new PrismaClient(); 19 | 20 | const knexInstance = knex({ 21 | client: 'mysql', 22 | connection: { 23 | host: '127.0.0.1', 24 | user: 'root', 25 | password: 'secret', 26 | database: 'next_graphql_admin@local', 27 | }, 28 | }); 29 | 30 | const userEmail = `admin@example.com`; 31 | const userPassword = 'admin'; 32 | 33 | // eslint-disable-next-line no-console 34 | console.log(`Creating user '${userEmail}' with password '${userPassword}'`); 35 | 36 | async function main(): Promise { 37 | const hashedPassword = await bcrypt.hash(userPassword, 10); 38 | const userCuid = cuid(); 39 | 40 | 41 | // Create user 42 | await prisma.user.create({ 43 | data: { 44 | email: userEmail, 45 | password: hashedPassword, 46 | id: userCuid, 47 | hasVerifiedEmail: true, 48 | name: 'Vikas Dwivedi', 49 | role: 'ADMIN' 50 | }, 51 | }); 52 | // eslint-disable-next-line no-console 53 | console.log(`Seeded user: ${userEmail}`); 54 | 55 | process.exit(0); 56 | } 57 | 58 | // eslint-disable-next-line no-console 59 | main().catch((e) => console.error(e)); 60 | -------------------------------------------------------------------------------- /api/src/mail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | 3 | const transport = nodemailer.createTransport({ 4 | host: process.env.MAIL_HOST, 5 | port: process.env.MAIL_PORT, 6 | auth: { 7 | user: process.env.MAIL_USER, 8 | pass: process.env.MAIL_PASS, 9 | }, 10 | }); 11 | 12 | const makeANiceEmail = text => ` 13 |
20 |

Hello There!

21 |

${text}

22 | 23 |

😘, Wes Bos

24 |
25 | `; 26 | 27 | exports.transport = transport; 28 | exports.makeANiceEmail = makeANiceEmail; 29 | -------------------------------------------------------------------------------- /api/src/schema/enumTypes.ts: -------------------------------------------------------------------------------- 1 | import { enumType } from '@nexus/schema'; 2 | 3 | export const OrderByArg = enumType({ 4 | name: 'OrderByArg', 5 | members: ['asc', 'desc'], 6 | }); 7 | -------------------------------------------------------------------------------- /api/src/schema/inputTypes.ts: -------------------------------------------------------------------------------- 1 | import { inputObjectType } from '@nexus/schema'; 2 | 3 | export const ProductOrderByInput = inputObjectType({ 4 | name: 'ProductOrderByInput', 5 | definition(t) { 6 | t.field('updatedAt', { 7 | type: 'OrderByArg', 8 | }); 9 | t.field('price', { 10 | type: 'OrderByArg', 11 | }); 12 | t.field('name', { 13 | type: 'OrderByArg', 14 | }); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /api/src/schema/mutations.ts: -------------------------------------------------------------------------------- 1 | import { mutationType } from '@nexus/schema'; 2 | 3 | export const Mutation = mutationType({ 4 | definition(t) { 5 | // The following aliased mutations are only used in order to generate types. Their alias lines up with another mutation name in order 6 | // to define our own resolvers: https://github.com/graphql-nexus/nexus-schema-plugin-prisma/issues/381#issuecomment-575357444 7 | t.crud.updateOneGoogleMapsLocation({ alias: 'updateUser' }); 8 | t.crud.createOneGoogleMapsLocation({ alias: 'createUser' }); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /api/src/schema/mutations/completeOnboarding.ts: -------------------------------------------------------------------------------- 1 | import { mutationField } from '@nexus/schema'; 2 | import analytics from '../../utils/analytics'; 3 | import { verifyUserIsAuthenticated } from '../../utils/verifyUserIsAuthenticated'; 4 | 5 | export const completeOnboardingMutationField = mutationField('completeOnboarding', { 6 | type: 'Boolean', 7 | resolve: async (_, _args, ctx) => { 8 | verifyUserIsAuthenticated(ctx.user); 9 | await ctx.prisma.user.update({ 10 | where: { id: ctx.user.id }, 11 | data: { hasCompletedOnboarding: true }, 12 | }); 13 | analytics.track({ eventType: 'Onboarding completed', userId: ctx.user.id }); 14 | return true; 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /api/src/schema/mutations/createCategory.ts: -------------------------------------------------------------------------------- 1 | import { mutationField, stringArg, arg } from '@nexus/schema'; 2 | import analytics from '../../utils/analytics'; 3 | import { verifyUserIsAuthenticated } from '../../utils/verifyUserIsAuthenticated'; 4 | 5 | export const createCategoryMutationField = mutationField('createCategory', { 6 | type: 'Category', 7 | args: { 8 | name: stringArg({ required: true }), 9 | slug: stringArg({ required: true }), 10 | parent: stringArg({ required: true }), 11 | }, 12 | resolve: async (_, { name, slug, parent }, ctx) => { 13 | verifyUserIsAuthenticated(ctx.user); 14 | if (process.env.IS_DEMO_ACCOUNT === 'true') { 15 | throw Error('Sorry, you can\'t do update or delete in DEMO account'); 16 | } 17 | // Then create the Category entry in Prisma, storing the S3 file info 18 | const createdCategory = await ctx.prisma.category.create({ 19 | data: { 20 | name, 21 | slug, 22 | parent, 23 | }, 24 | }); 25 | analytics.track({ 26 | eventType: 'Category created', 27 | userId: ctx.user.id, 28 | eventProperties: { 29 | id: createdCategory.id, 30 | }, 31 | }); 32 | return createdCategory; 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /api/src/schema/mutations/createProduct.ts: -------------------------------------------------------------------------------- 1 | import { mutationField, idArg, stringArg, intArg, arg, booleanArg } from '@nexus/schema'; 2 | import analytics from '../../utils/analytics'; 3 | import { verifyUserIsAuthenticated } from '../../utils/verifyUserIsAuthenticated'; 4 | 5 | export const createProductMutationField = mutationField('createProduct', { 6 | type: 'Product', 7 | args: { 8 | name: stringArg({ required: true }), 9 | description: stringArg({ required: true }), 10 | price: intArg({ required: true }), 11 | discount: intArg({ required: true }), 12 | salePrice: intArg({ required: true }), 13 | sku: stringArg({ required: true }), 14 | unit: stringArg({ required: true }), 15 | categoryId: idArg({ required: true }), 16 | images: arg({ 17 | type: 'ProductImageCreateWithoutProductInput', 18 | list: true, 19 | required: true, 20 | }), 21 | }, 22 | // name description price discount salePrice sku unit category* user* 23 | resolve: async (_, { name, description, price, discount, salePrice, sku, unit, categoryId, images }, ctx) => { 24 | verifyUserIsAuthenticated(ctx.user); 25 | if (process.env.IS_DEMO_ACCOUNT === 'true') { 26 | throw Error('Sorry, you can\'t do update or delete in DEMO account'); 27 | } 28 | const user = await ctx.prisma.user.findOne({ 29 | where: { id: ctx.user.id } 30 | }); 31 | 32 | const newProduct = await ctx.prisma.product.create({ 33 | data: { 34 | User: { connect: { id: ctx.user.id } }, 35 | Category: { connect: { id: categoryId } }, 36 | name, 37 | description, 38 | price, 39 | discount, 40 | salePrice, 41 | sku, 42 | unit, 43 | ProductImages: { 44 | create: images 45 | } 46 | }, 47 | }); 48 | 49 | analytics.track({ 50 | eventType: 'Product created', 51 | userId: ctx.user.id, 52 | eventProperties: { 53 | id: newProduct.id, 54 | }, 55 | }); 56 | return newProduct; 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /api/src/schema/mutations/deleteAccount.ts: -------------------------------------------------------------------------------- 1 | import { mutationField, idArg } from '@nexus/schema'; 2 | import analytics from '../../utils/analytics'; 3 | import { verifyUserIsAuthenticated } from '../../utils/verifyUserIsAuthenticated'; 4 | 5 | export const deleteAccountMutationField = mutationField('deleteAccount', { 6 | type: 'User', 7 | args: { 8 | id: idArg({ required: true }), 9 | }, 10 | resolve: async (_, { id }, ctx) => { 11 | verifyUserIsAuthenticated(ctx.user); 12 | if (process.env.IS_DEMO_ACCOUNT === 'true') { 13 | throw Error('Sorry, you can\'t do update or delete in DEMO account'); 14 | } 15 | const User = await ctx.prisma.user.findOne({ 16 | where: { id } 17 | }); 18 | if (User === null) { 19 | throw Error('User not found'); 20 | } 21 | const deletedUser = await ctx.prisma.user.delete({ where: { id } }); 22 | 23 | analytics.track({ 24 | eventType: 'User deleted', 25 | userId: ctx.user.id, 26 | eventProperties: { 27 | id, 28 | }, 29 | }); 30 | 31 | return deletedUser; 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /api/src/schema/mutations/deleteCategory.ts: -------------------------------------------------------------------------------- 1 | import { Category } from './../objectTypes'; 2 | import { mutationField, idArg } from '@nexus/schema'; 3 | import analytics from '../../utils/analytics'; 4 | import { verifyUserIsAuthenticated } from '../../utils/verifyUserIsAuthenticated'; 5 | 6 | export const deleteCategoryMutationField = mutationField('deleteCategory', { 7 | type: 'Category', 8 | args: { 9 | id: idArg({ required: true }), 10 | }, 11 | resolve: async (_, { id }, ctx) => { 12 | verifyUserIsAuthenticated(ctx.user); 13 | if (process.env.IS_DEMO_ACCOUNT === 'true') { 14 | throw Error('Sorry, you can\'t do update or delete in DEMO account'); 15 | } 16 | const Category = await ctx.prisma.category.findOne({ 17 | where: { id } 18 | }); 19 | if (Category === null) { 20 | throw Error('Category not found'); 21 | } 22 | const deletedCategory = await ctx.prisma.category.delete({ where: { id } }); 23 | 24 | analytics.track({ 25 | eventType: 'Category deleted', 26 | userId: ctx.user.id, 27 | eventProperties: { 28 | id, 29 | }, 30 | }); 31 | 32 | return deletedCategory; 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /api/src/schema/mutations/login.ts: -------------------------------------------------------------------------------- 1 | import { stringArg, mutationField } from '@nexus/schema'; 2 | import bcrypt from 'bcrypt'; 3 | import jwt from 'jsonwebtoken'; 4 | import { cookieDuration } from '../../utils/constants'; 5 | import analytics from '../../utils/analytics'; 6 | import { verifyEnvironmentVariables } from '../../utils/verifyEnvironmentVariables'; 7 | 8 | export const loginMutationField = mutationField('login', { 9 | type: 'User', 10 | args: { email: stringArg({ required: true }), password: stringArg({ required: true }) }, 11 | resolve: async (_, { email, password }, ctx) => { 12 | email = email.toLowerCase(); 13 | const user = await ctx.prisma.user.findOne({ where: { email } }); 14 | if (!user) { 15 | throw new Error(`No such user found for email ${email}`); 16 | } 17 | if (!user.password) { 18 | throw new Error(`No password set for that email. Sign in with Google instead.`); 19 | } 20 | 21 | const isValid = await bcrypt.compare(password, user.password); 22 | if (!isValid) { 23 | throw new Error('Invalid Password!'); 24 | } 25 | verifyEnvironmentVariables(process.env.API_APP_SECRET, 'API_APP_SECRET'); 26 | const token = jwt.sign({ userId: user.id }, process.env.API_APP_SECRET); 27 | 28 | ctx.response.cookie('token', token, { 29 | httpOnly: true, 30 | maxAge: cookieDuration, 31 | sameSite: 'none', 32 | secure: true, 33 | // domain: process.env.API_COOKIE_DOMAIN, 34 | }); 35 | 36 | analytics.track({ eventType: 'Login', userId: user.id, eventProperties: { method: 'Password' } }); 37 | 38 | return user; 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /api/src/schema/mutations/logout.ts: -------------------------------------------------------------------------------- 1 | import { mutationField } from '@nexus/schema'; 2 | import { verifyEnvironmentVariables } from '../../utils/verifyEnvironmentVariables'; 3 | 4 | export const logoutMutationField = mutationField('logout', { 5 | type: 'Boolean', 6 | resolve: (_, _args, ctx) => { 7 | verifyEnvironmentVariables(process.env.API_COOKIE_DOMAIN, 'API_COOKIE_DOMAIN'); 8 | // ctx.response.clearCookie('token', { 9 | // domain: process.env.API_COOKIE_DOMAIN, 10 | // }); 11 | ctx.response.cookie('token', '', { 12 | httpOnly: true, 13 | maxAge: 1, 14 | sameSite: 'none', 15 | secure: true, 16 | // domain: process.env.API_COOKIE_DOMAIN, 17 | }); 18 | 19 | return true; 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /api/src/schema/mutations/requestPasswordReset.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'util'; 2 | import { randomBytes } from 'crypto'; 3 | import { mutationField, stringArg } from '@nexus/schema'; 4 | // import { sendEmail } from '../../utils/mail'; 5 | import analytics from '../../utils/analytics'; 6 | const { transport, makeANiceEmail } = require('../../mail'); 7 | 8 | export const requestPasswordResetMutationField = mutationField('requestPasswordReset', { 9 | type: 'Boolean', 10 | args: { 11 | email: stringArg(), 12 | }, 13 | resolve: async (_, { email }, ctx) => { 14 | const user = await ctx.prisma.user.findOne({ where: { email } }); 15 | if (!user) { 16 | throw new Error(`No user found for ${email}`); 17 | } 18 | 19 | const randomBytesPromisified = promisify(randomBytes); 20 | const resetToken = (await randomBytesPromisified(20)).toString('hex'); 21 | const resetTokenExpiry = Date.now() + 3600000; // 1 hour from now 22 | await ctx.prisma.user.update({ 23 | where: { email }, 24 | data: { resetToken, resetTokenExpiry }, 25 | }); 26 | 27 | // 3. Email them that reset token 28 | const mailRes = await transport.sendMail({ 29 | from: process.env.FROM_EMAIL, 30 | to: user.email, 31 | subject: 'Your Password Reset Token', 32 | html: makeANiceEmail(`Your Password Reset Token is here! 33 | \n\n 34 | Click Here to Reset`), 35 | }); 36 | 37 | // await sendEmail({ 38 | // subject: 'Your password reset token', 39 | // toAddress: [user.email], 40 | // text: `Your Password Reset Token is here! 41 | // \n\n 42 | // Click Here to Reset`, 43 | // }); 44 | 45 | analytics.track({ eventType: 'Reset password request' }); 46 | 47 | return true; 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /api/src/schema/mutations/resetPassword.ts: -------------------------------------------------------------------------------- 1 | import { mutationField, stringArg } from '@nexus/schema'; 2 | import bcrypt from 'bcrypt'; 3 | import jwt from 'jsonwebtoken'; 4 | import analytics from '../../utils/analytics'; 5 | import { verifyEnvironmentVariables } from '../../utils/verifyEnvironmentVariables'; 6 | 7 | export const resetPasswordMutationField = mutationField('resetPassword', { 8 | type: 'User', 9 | args: { 10 | password: stringArg(), 11 | confirmPassword: stringArg(), 12 | resetToken: stringArg(), 13 | }, 14 | resolve: async (_, { password, confirmPassword, resetToken }, ctx) => { 15 | if (password !== confirmPassword) { 16 | throw new Error("Passwords don't match!"); 17 | } 18 | const [user] = await ctx.prisma.user.findMany({ 19 | where: { 20 | resetToken: resetToken, 21 | resetTokenExpiry: { gte: Date.now() - 3600000 }, 22 | }, 23 | }); 24 | if (!user) { 25 | throw new Error('This token is either invalid or expired!'); 26 | } 27 | 28 | const hashedPassword = await bcrypt.hash(password, 10); 29 | 30 | const updatedUser = await ctx.prisma.user.update({ 31 | where: { email: user.email }, 32 | data: { 33 | password: hashedPassword, 34 | resetToken: null, 35 | resetTokenExpiry: null, 36 | }, 37 | }); 38 | verifyEnvironmentVariables(process.env.API_APP_SECRET, 'API_APP_SECRET'); 39 | const token = jwt.sign({ userId: updatedUser.id }, process.env.API_APP_SECRET); 40 | 41 | ctx.response.cookie('token', token, { 42 | httpOnly: true, 43 | maxAge: 1000 * 60 * 60 * 24 * 365, 44 | sameSite: 'none', 45 | secure: true, 46 | // domain: process.env.API_COOKIE_DOMAIN, 47 | }); 48 | 49 | analytics.track({ eventType: 'Reset password success' }); 50 | 51 | return updatedUser; 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /api/src/schema/mutations/signup.ts: -------------------------------------------------------------------------------- 1 | import { stringArg, mutationField } from '@nexus/schema'; 2 | import bcrypt from 'bcrypt'; 3 | import jwt from 'jsonwebtoken'; 4 | import { generateToken } from '../../utils/generateToken'; 5 | import { cookieDuration } from '../../utils/constants'; 6 | import analytics from '../../utils/analytics'; 7 | import { verifyEnvironmentVariables } from '../../utils/verifyEnvironmentVariables'; 8 | 9 | export const signupMutationField = mutationField('signup', { 10 | type: 'User', 11 | args: { 12 | name: stringArg(), 13 | email: stringArg(), 14 | password: stringArg(), 15 | confirmPassword: stringArg(), 16 | }, 17 | resolve: async (_, { name, email, password, confirmPassword }, ctx) => { 18 | if (password !== confirmPassword) { 19 | throw new Error("Passwords don't match!"); 20 | } 21 | email = email?.toLowerCase(); 22 | if (email === null || email === undefined) { 23 | throw Error('Email is not defined'); 24 | } 25 | // Check if user already exists with that email 26 | const existingUser = await ctx.prisma.user.findOne({ where: { email } }); 27 | if (existingUser) { 28 | if (existingUser.googleId) { 29 | throw new Error(`User with that email already exists. Sign in with Google.`); 30 | } 31 | throw new Error(`User with that email already exists.`); 32 | } 33 | 34 | const hashedPassword = await bcrypt.hash(password, 10); 35 | 36 | const emailConfirmationToken = await generateToken(); 37 | 38 | const user = await ctx.prisma.user.create({ 39 | data: { 40 | name: 'test', 41 | email, 42 | password: hashedPassword, 43 | emailConfirmationToken, 44 | }, 45 | }); 46 | 47 | verifyEnvironmentVariables(process.env.API_APP_SECRET, 'API_APP_SECRET'); 48 | const token = jwt.sign({ userId: user.id }, process.env.API_APP_SECRET); 49 | 50 | // NOTE: Need to specify domain in order for front-end to see cookie: https://github.com/apollographql/apollo-client/issues/4193#issuecomment-573195699 51 | ctx.response.cookie('token', token, { 52 | httpOnly: true, 53 | maxAge: cookieDuration, 54 | sameSite: 'none', 55 | secure: true, 56 | // domain: process.env.API_COOKIE_DOMAIN, 57 | }); 58 | 59 | analytics.track({ eventType: 'Signup', userId: user.id, eventProperties: { method: 'Password' } }); 60 | 61 | return user; 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /api/src/schema/mutations/updateCategory.ts: -------------------------------------------------------------------------------- 1 | import { mutationField, idArg, stringArg, intArg, arg, booleanArg } from '@nexus/schema'; 2 | import analytics from '../../utils/analytics'; 3 | import { verifyUserIsAuthenticated } from '../../utils/verifyUserIsAuthenticated'; 4 | 5 | export const updateCategoryMutationField = mutationField('updateCategory', { 6 | type: 'Category', 7 | args: { 8 | id: idArg({ required: true }), 9 | name: stringArg(), 10 | parent: stringArg({ 11 | required: false, 12 | }), 13 | slug: stringArg(), 14 | }, 15 | resolve: async (_, { id, name, parent, slug }, ctx) => { 16 | verifyUserIsAuthenticated(ctx.user); 17 | if (process.env.IS_DEMO_ACCOUNT === 'true') { 18 | throw Error('Sorry, you can\'t do update or delete in DEMO account'); 19 | } 20 | // Then create the Category entry in Prisma, storing the S3 file info 21 | const updatedCategory = await ctx.prisma.category.update({ 22 | where: { 23 | id, 24 | }, 25 | data: { 26 | name, 27 | parent, 28 | slug 29 | }, 30 | }); 31 | 32 | analytics.track({ 33 | eventType: 'Category updated', 34 | userId: ctx.user.id, 35 | eventProperties: { 36 | id: id, 37 | }, 38 | }); 39 | 40 | return updatedCategory; 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /api/src/schema/mutations/updateProduct.ts: -------------------------------------------------------------------------------- 1 | import { mutationField, idArg, stringArg, intArg, arg, booleanArg } from '@nexus/schema'; 2 | import analytics from '../../utils/analytics'; 3 | import { verifyUserIsAuthenticated } from '../../utils/verifyUserIsAuthenticated'; 4 | 5 | export const updateProductMutationField = mutationField('updateProduct', { 6 | type: 'Product', 7 | args: { 8 | id: idArg({ required: true }), 9 | name: stringArg({ required: true }), 10 | description: stringArg({ required: true }), 11 | price: intArg({ required: true }), 12 | discount: intArg({ required: true }), 13 | salePrice: intArg({ required: true }), 14 | sku: stringArg({ required: true }), 15 | unit: stringArg({ required: true }), 16 | categoryId: idArg({ required: true }), 17 | images: arg({ 18 | type: 'ProductImageCreateWithoutProductInput', 19 | list: true, 20 | required: true, 21 | }), 22 | alreadyUploadedImages: arg({ 23 | type: 'ProductImageCreateWithoutProductInput', 24 | list: true, 25 | required: true, 26 | }), 27 | }, 28 | // name description price discount salePrice sku unit category* user* 29 | resolve: async (_, { id, name, description, price, discount, salePrice, sku, unit, categoryId, images, alreadyUploadedImages }, ctx) => { 30 | verifyUserIsAuthenticated(ctx.user); 31 | if (process.env.IS_DEMO_ACCOUNT === 'true') { 32 | throw Error('Sorry, you can\'t do update or delete in DEMO account'); 33 | } 34 | const user = await ctx.prisma.user.findOne({ 35 | where: { id: ctx.user.id } 36 | }); 37 | 38 | const currentImages = await ctx.prisma.productImage.findMany({ 39 | where: { productId: id }, 40 | }); 41 | 42 | const imageIdsToDelete = currentImages.reduce((prev: string[], curr) => { 43 | if (curr.id && alreadyUploadedImages.every((image) => image.id !== curr.id)) { 44 | prev.push(curr.id); 45 | } 46 | return prev; 47 | }, []); 48 | 49 | const imagesToCreate = images.filter((image) => currentImages.every((c) => c.id !== image.id)); 50 | 51 | const product = await ctx.prisma.product.update({ 52 | where: { 53 | id, 54 | }, 55 | data: { 56 | User: { connect: { id: ctx.user.id } }, 57 | Category: { connect: { id: categoryId } }, 58 | name, 59 | description, 60 | price, 61 | discount, 62 | salePrice, 63 | sku, 64 | unit, 65 | ProductImages: { 66 | ...(imagesToCreate.length ? { create: imagesToCreate } : {}), 67 | ...(imageIdsToDelete.length 68 | ? { 69 | deleteMany: { 70 | id: { in: imageIdsToDelete }, 71 | }, 72 | } 73 | : {}), 74 | }, 75 | }, 76 | }); 77 | 78 | analytics.track({ 79 | eventType: 'Product updated', 80 | userId: ctx.user.id, 81 | eventProperties: { 82 | id: product.id, 83 | }, 84 | }); 85 | return product; 86 | }, 87 | }); 88 | -------------------------------------------------------------------------------- /api/src/schema/mutations/updateUser.ts: -------------------------------------------------------------------------------- 1 | import { mutationField, idArg, stringArg, intArg, arg, booleanArg } from '@nexus/schema'; 2 | import analytics from '../../utils/analytics'; 3 | import { verifyUserIsAuthenticated } from '../../utils/verifyUserIsAuthenticated'; 4 | 5 | export const updateUserMutationField = mutationField('updateUser', { 6 | type: 'User', 7 | args: { 8 | id: idArg({ required: true }), 9 | role: stringArg(), 10 | name: stringArg(), 11 | status: stringArg(), 12 | }, 13 | resolve: async (_, { id, name, role, status }, ctx) => { 14 | verifyUserIsAuthenticated(ctx.user); 15 | if (process.env.IS_DEMO_ACCOUNT === 'true') { 16 | throw Error('Sorry, you can\'t do update or delete in DEMO account'); 17 | } 18 | // Then create the User entry in Prisma, storing the S3 file info 19 | const updatedUser = await ctx.prisma.user.update({ 20 | where: { 21 | id, 22 | }, 23 | data: { 24 | name, 25 | // role, 26 | // status 27 | }, 28 | }); 29 | 30 | analytics.track({ 31 | eventType: 'User updated', 32 | userId: ctx.user.id, 33 | eventProperties: { 34 | id: id, 35 | }, 36 | }); 37 | 38 | return updatedUser; 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /api/src/schema/objectTypes.ts: -------------------------------------------------------------------------------- 1 | import { objectType } from '@nexus/schema'; 2 | 3 | export const Category = objectType({ 4 | name: 'Category', 5 | definition(t) { 6 | t.model.id(); 7 | t.model.createdAt(); 8 | t.model.name(); 9 | t.model.parent(); 10 | t.model.slug(); 11 | t.model.updatedAt(); 12 | }, 13 | }); 14 | 15 | export const ProductImage = objectType({ 16 | name: 'ProductImage', 17 | definition(t) { 18 | t.model.id(); 19 | t.model.createdAt(); 20 | t.model.updatedAt(); 21 | t.model.image(); 22 | t.model.Product(); 23 | t.model.productId(); 24 | }, 25 | }); 26 | 27 | export const Product = objectType({ 28 | name: 'Product', 29 | definition(t) { 30 | t.model.id(); 31 | t.model.name(); 32 | t.model.price(); 33 | t.model.salePrice(); 34 | t.model.sku(); 35 | t.model.unit(); 36 | t.model.User(); 37 | t.model.Category(); 38 | t.model.ProductImages({ ordering: { createdAt: true } }); 39 | t.model.description(); 40 | t.model.discount(); 41 | t.model.createdAt(); 42 | t.model.updatedAt(); 43 | }, 44 | }); 45 | 46 | export const User = objectType({ 47 | name: 'User', 48 | definition(t) { 49 | t.model.id(); 50 | t.model.email(); 51 | t.model.name(); 52 | t.model.role(); 53 | t.model.status(); 54 | t.model.hasCompletedOnboarding(); 55 | t.model.hasVerifiedEmail(); 56 | }, 57 | }); 58 | 59 | export const GoogleMapsLocation = objectType({ 60 | name: 'GoogleMapsLocation', 61 | definition(t) { 62 | 63 | t.model.id(); 64 | t.model.name(); 65 | t.model.googlePlacesId(); 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /api/src/schema/scarlarTypes.ts: -------------------------------------------------------------------------------- 1 | // TODO: Figure out how to get the file upload types to get passed down to resolvers 2 | 3 | import { scalarType } from '@nexus/schema'; 4 | import { GraphQLUpload } from 'graphql-upload'; 5 | 6 | // Related: https://github.com/prisma-labs/nexus/issues/113 7 | export const Upload = scalarType({ 8 | name: GraphQLUpload.name, 9 | asNexusMethod: 'upload', // We set this to be used as a method later as `t.upload()` if needed 10 | description: GraphQLUpload.description, 11 | serialize: GraphQLUpload.serialize, 12 | parseValue: GraphQLUpload.parseValue, 13 | parseLiteral: GraphQLUpload.parseLiteral, 14 | }); 15 | -------------------------------------------------------------------------------- /api/src/utils/analytics.ts: -------------------------------------------------------------------------------- 1 | import Amplitude from 'amplitude'; 2 | import uuid from 'uuid/v4'; 3 | 4 | if (process.env.API_AMPLITUDE_API_KEY === undefined) { 5 | throw Error('Missing API_AMPLITUDE_API_KEY environment variable'); 6 | } 7 | 8 | const analytics = new Amplitude(process.env.API_AMPLITUDE_API_KEY, { user_id: uuid() }); 9 | 10 | export default analytics; 11 | -------------------------------------------------------------------------------- /api/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // import { BillingFrequency } from '@prisma/client'; 2 | // import { verifyEnvironmentVariables } from './verifyEnvironmentVariables'; 3 | 4 | export const cookieDuration = 1000 * 60 * 60 * 24 * 365; // 1 year 5 | -------------------------------------------------------------------------------- /api/src/utils/generateToken.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { promisify } from 'util'; 3 | 4 | export const generateToken = async (): Promise => { 5 | const randomBytesPromisified = promisify(randomBytes); 6 | return (await randomBytesPromisified(20)).toString('hex'); 7 | }; 8 | -------------------------------------------------------------------------------- /api/src/utils/mail.ts: -------------------------------------------------------------------------------- 1 | // // Load the AWS SDK for Node.js 2 | // import AWS from 'aws-sdk'; 3 | // import { credentials } from './fileUpload'; 4 | 5 | // const ses = new AWS.SES({ apiVersion: '2010-12-01', credentials, region: 'us-east-1' }); 6 | 7 | // export const sendEmail = async ({ 8 | // toAddress, 9 | // subject, 10 | // text, 11 | // }: { 12 | // toAddress: string[]; 13 | // subject: string; 14 | // text: string; 15 | // }): Promise => { 16 | // const params = { 17 | // Destination: { 18 | // CcAddresses: [], 19 | // ToAddresses: toAddress, 20 | // }, 21 | // Message: { 22 | // Body: { 23 | // Html: { 24 | // Charset: 'UTF-8', 25 | // Data: ` 26 | //
33 | //

Hello There!

34 | //

${text}

35 | 36 | //

Admin

37 | //
38 | // `, 39 | // }, 40 | // }, 41 | // Subject: { 42 | // Charset: 'UTF-8', 43 | // Data: subject, 44 | // }, 45 | // }, 46 | // Source: 'info@nextgraphqladmin.com', 47 | // ReplyToAddresses: ['info@nextgraphqladmin.com'], 48 | // }; 49 | 50 | // try { 51 | // await ses.sendEmail(params).promise(); 52 | // } catch (e) { 53 | // throw e; 54 | // } 55 | // }; 56 | -------------------------------------------------------------------------------- /api/src/utils/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | 3 | /** 4 | * Stripe singleton instance 5 | */ 6 | export const stripe = new Stripe('sk_test_WEEHCPAAw4mYtS71SZcQ9LCn00VuDg7wVY'); 7 | -------------------------------------------------------------------------------- /api/src/utils/verifyEnvironmentVariables.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes sure that environment variables are defined 3 | */ 4 | export function verifyEnvironmentVariables(variable: string | undefined, name: string): asserts variable is string { 5 | if (variable === undefined) { 6 | throw Error(`Environment variable ${name} is not defined`); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /api/src/utils/verifyUserIsAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@prisma/client'; 2 | 3 | export function verifyUserIsAuthenticated(user: User | null): asserts user is User { 4 | if (user === null) { 5 | throw Error('User is not authenticated'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "lib": ["es2016", "esnext.asynciterable"], 8 | "noEmit": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "skipLibCheck": true, 11 | "strict": true 12 | }, 13 | "exclude": ["node_modules"], 14 | "include": ["src/typeDefs.ts", "**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /api/typings/amplitude.ts: -------------------------------------------------------------------------------- 1 | declare module 'amplitude' { 2 | type AmplitudeOptions = 3 | | { 4 | secretKey: string; 5 | userId: string; 6 | deviceId: string; 7 | sessionId: string; 8 | } 9 | | { 10 | secretKey: string; 11 | user_id: string; 12 | device_id: string; 13 | session_id: string; 14 | }; 15 | type TrackData = { 16 | eventType: string; 17 | userId?: string; 18 | eventProperties?: { 19 | [key: string]: string; 20 | }; 21 | userProperties?: { 22 | [key: string]: string; 23 | }; 24 | }; 25 | class Amplitude { 26 | constructor(token: string, options?: Partial); 27 | 28 | track(data: TrackData): any; 29 | } 30 | 31 | export = Amplitude; 32 | } 33 | -------------------------------------------------------------------------------- /api/typings/global.ts: -------------------------------------------------------------------------------- 1 | // A problem with typescript-eslint requires this to be ignored 2 | // https://github.com/typescript-eslint/typescript-eslint/issues/1596 3 | // https://github.com/typescript-eslint/typescript-eslint/issues/1856 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | import { User as PrismaClientUser } from '@prisma/client'; 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import { Response as ExpressResponse } from 'express-serve-static-core'; 8 | 9 | declare global { 10 | // eslint-disable-next-line @typescript-eslint/no-namespace 11 | namespace Express { 12 | export interface Request { 13 | userId?: string; 14 | prismaClientUser?: PrismaClientUser | null; 15 | cookie: ExpressResponse['cookie']; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web-app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": ["@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-nullish-coalescing-operator", "emotion"] 4 | } 5 | -------------------------------------------------------------------------------- /web-app/.dockerignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | .vscode 4 | .env -------------------------------------------------------------------------------- /web-app/.env.example: -------------------------------------------------------------------------------- 1 | COMMON_BACKEND_URL=http://local.api.nextgraphqladmin.com:4000 2 | COMMON_FRONTEND_URL=http://local.app.nextgraphqladmin.com:3000 3 | COMMON_STRIPE_YEARLY_PLAN_ID=plan_GTLU37cHfQQYg5 4 | COMMON_STRIPE_MONTHLY_PLAN_ID=plan_GpOi3czoSsBCuT 5 | 6 | WEB_APP_STRIPE_PUBLISHABLE_KEY=pk_test_QsP5rtpNMraN2DIBV97RSoLI001RhVZizR 7 | WEB_APP_GOOGLE_API_KEY=AIzaSyDqL38EK_ntILaR1Fg4LzV0dzw3DhOWoX0 8 | WEB_APP_MARKETING_SITE=https://nextgraphqladmin.com 9 | WEB_APP_SENTRY_DSN=https://3407a4103e984afd89b81ab34f8f4123@sentry.io/1967721 10 | 11 | IMAGE_UPLOAD_URL=https://api.cloudinary.com/v1_1/REPLACE_USER_NAME/image/upload 12 | IMAGE_UPLOAD_PRESET=YOUR_PROJECT_NAME -------------------------------------------------------------------------------- /web-app/.gitignore: -------------------------------------------------------------------------------- 1 | .now 2 | now.env* 3 | .next 4 | node_modules 5 | .env 6 | out 7 | .env.production 8 | -------------------------------------------------------------------------------- /web-app/.graphqlconfig.yaml: -------------------------------------------------------------------------------- 1 | projects: 2 | Web App: 3 | schemaPath: 4 | - ../backend/src/generated/schema.graphql 5 | extensions: 6 | endpoints: 7 | local: 'http://local.api.nextgraphqladmin.com:4000' 8 | -------------------------------------------------------------------------------- /web-app/.nvmrc: -------------------------------------------------------------------------------- 1 | 12.16.1 -------------------------------------------------------------------------------- /web-app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 4, 4 | semi: false, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /web-app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBorder": "#aaee0d", 4 | "activityBar.activeBackground": "#eb0404", 5 | "activityBar.background": "#792e11", 6 | "activityBar.foreground": "#83f500" 7 | }, 8 | "git.ignoreLimitWarning": true 9 | } -------------------------------------------------------------------------------- /web-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16.1-alpine3.11 2 | 3 | EXPOSE 3000 4 | 5 | ARG COMMON_BACKEND_URL 6 | ARG COMMON_FRONTEND_URL 7 | ARG COMMON_STRIPE_YEARLY_PLAN_ID 8 | ARG COMMON_STRIPE_MONTHLY_PLAN_ID 9 | ARG WEB_APP_STRIPE_PUBLISHABLE_KEY 10 | ARG WEB_APP_GOOGLE_API_KEY 11 | ARG WEB_APP_MARKETING_SITE 12 | ARG WEB_APP_SENTRY_DSN 13 | 14 | 15 | # Set environment variables 16 | ENV COMMON_BACKEND_URL=$COMMON_BACKEND_URL 17 | ENV COMMON_FRONTEND_URL=$COMMON_FRONTEND_URL 18 | ENV COMMON_STRIPE_YEARLY_PLAN_ID=$COMMON_STRIPE_YEARLY_PLAN_ID 19 | ENV COMMON_STRIPE_MONTHLY_PLAN_ID=$COMMON_STRIPE_MONTHLY_PLAN_ID 20 | ENV WEB_APP_STRIPE_PUBLISHABLE_KEY=$WEB_APP_STRIPE_PUBLISHABLE_KEY 21 | ENV WEB_APP_GOOGLE_API_KEY=$WEB_APP_GOOGLE_API_KEY 22 | ENV WEB_APP_MARKETING_SITE=$WEB_APP_MARKETING_SITE 23 | ENV WEB_APP_SENTRY_DSN=$WEB_APP_SENTRY_DSN 24 | 25 | # Setup working directory. All the paths will be relative to WORKDIR 26 | WORKDIR /usr/src/app 27 | 28 | # Add custom nginx config 29 | ADD nginx.conf ./ 30 | # Rename nginx.conf to nginx.conf.sigil, since that is what Dokku expects 31 | RUN mv nginx.conf nginx.conf.sigil 32 | 33 | # Install dependencies 34 | COPY package.json yarn.lock ./ 35 | RUN yarn 36 | 37 | # Copy all source files into docker container 38 | COPY . . 39 | 40 | # Build the app 41 | RUN yarn build 42 | 43 | # Run the app 44 | CMD [ "yarn", "start" ] -------------------------------------------------------------------------------- /web-app/README.md: -------------------------------------------------------------------------------- 1 | # Web App 2 | 3 | A Next.js application for the web application of Admin Panel . 4 | 5 | ## Getting started 6 | 7 | ### Pre-requisites 8 | 9 | The following must be installed locally in order to run the web application: 10 | 11 | - yarn (https://classic.yarnpkg.com/en/docs/install/#mac-stable) 12 | - node (https://nodejs.org/en/download/) 13 | 14 | ### Host file 15 | 16 | Add the following line to your `/etc/hosts` file in order to alias your localhost to local.app.nextgraphqladmin.com: 17 | 18 | ```text 19 | 127.0.0.1 local.app.nextgraphqladmin.com 20 | ``` 21 | ### Cloudinary setup and adding upload preset 22 | 23 | - Cloudinary is a cloud-based image and video hosting service 24 | - Signup for cloudinary using this [link](https://cloudinary.com/signup) 25 | - Watch this [video about setting up upload preset in cloudinary](https://vimeo.com/454599719) 26 | 27 | 28 | ### Environment variables 29 | 30 | ```bash 31 | IMAGE_UPLOAD_URL=https://api.cloudinary.com/v1_1/REPLACE_USER_NAME/image/upload 32 | IMAGE_UPLOAD_PRESET=YOUR_PROJECT_NAME 33 | ``` 34 | 35 | Local environment variables are configured in the `.env` file. Variables set for the `dev` and `prod` environment are configured using the NOW CLI in the `now.json` and `now.prod.json` file. Environment variables are injected into the Next.js app through the `nex.config.js` file. 36 | 37 | ### Starting the server 38 | (Don't delete `yarn.lock` file. Install with `yarn`) 39 | ```bash 40 | yarn # Install all dependencies 41 | > If you aree getting error The engine "node" is incompatible with this module. Expected version "12". Got "14.17.3" 42 | > nvm use --delete-prefix v12.0 43 | > nvm install v12.0 44 | yarn dev # Starts the development server at http://local.app.nextgraphqladmin.com:3000 45 | ``` 46 | 47 | ## Local deployments to Zeit Now 48 | 49 | In order to deploy to Zeit Now with the `yarn deploy:dev` or `yarn deploy:prod` commands, it's required to have a `now.env.dev` or `now.env.prod` file with the correct environment variables for `NOW_ORG_ID` and `NOW_PROJECT_ID`. 50 | -------------------------------------------------------------------------------- /web-app/apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | includes: ['{pages,components,graphql,utils}/**/*.{tsx,ts}'], 4 | localSchemaFile: 'schema.graphql', 5 | service: { 6 | localSchemaFile: '../api/src/generated/schema.graphql', 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /web-app/assets/icons/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /web-app/assets/icons/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Google icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /web-app/assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Info bubble 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web-app/assets/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n-dev27/-nextjs-graphql-adminpanel/fc9bfa44ac48c827c484d9e198e4c3fc5f6a227d/web-app/assets/icons/logo.png -------------------------------------------------------------------------------- /web-app/assets/icons/rightArrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /web-app/assets/icons/star-unfilled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-app/assets/images/earning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /web-app/assets/images/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web-app/assets/images/user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n-dev27/-nextjs-graphql-adminpanel/fc9bfa44ac48c827c484d9e198e4c3fc5f6a227d/web-app/assets/images/user.jpg -------------------------------------------------------------------------------- /web-app/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import Router from 'next/router'; 2 | import { NextComponentType, NextPageContext } from 'next'; 3 | import { useQuery } from '@apollo/react-hooks'; 4 | import { useEffect } from 'react'; 5 | import { PageProps } from '../../pages/_app'; 6 | import Layout from '../Layout/Layout'; 7 | import Loader from '../Loader/Loader'; 8 | import { CurrentUserQuery } from '../../graphql/generated/CurrentUserQuery'; 9 | import { currentUserQuery } from '../../graphql/queries'; 10 | import LoginLayout from '../LoginLayout/LoginLayout'; 11 | 12 | type Props = { 13 | component: NextComponentType; 14 | query: PageProps['query']; 15 | pathname: PageProps['pathname']; 16 | req?: NextPageContext['req']; 17 | }; 18 | 19 | export type ComponentPageProps = Pick & { 20 | user: CurrentUserQuery['me']; 21 | }; 22 | 23 | const unauthenticatedPathnames = ['/login', '/reset-password', '/signup']; 24 | 25 | const App = ({ component: Component, query, pathname, ...props }: Props): JSX.Element | null => { 26 | const { data: currentUserData, loading: currentUserLoading } = useQuery(currentUserQuery); 27 | 28 | const isAnUnauthenticatedPage = pathname !== undefined && unauthenticatedPathnames.includes(pathname); 29 | // Need to wrap calls of `Router.replace` in a use effect to prevent it being called on the server side 30 | // https://github.com/zeit/next.js/issues/6713 31 | useEffect(() => { 32 | // Redirect the user to the login page if not authenticated 33 | if (!isAnUnauthenticatedPage && currentUserData?.me === null) { 34 | Router.replace('/login'); 35 | } 36 | }, [currentUserData, isAnUnauthenticatedPage, pathname]); 37 | 38 | if (currentUserLoading) { 39 | return ; 40 | } 41 | 42 | if (!isAnUnauthenticatedPage && currentUserData?.me === null) { 43 | return null; 44 | } 45 | 46 | if (!currentUserData) { 47 | return null; 48 | } 49 | 50 | return pathname !== undefined && unauthenticatedPathnames.includes(pathname) ? ( 51 | 52 | 53 | 54 | ) : ( 55 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default App; 62 | -------------------------------------------------------------------------------- /web-app/components/Category/CategoryMutation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const CREATE_CATEGORY = gql` 4 | mutation createCategory( 5 | $name: String! 6 | $parent: String! 7 | $slug: String! 8 | ) { 9 | createCategory( 10 | name: $name 11 | parent: $parent 12 | slug: $slug 13 | ) { 14 | id 15 | name 16 | parent 17 | # creation_date 18 | slug 19 | # number_of_product 20 | } 21 | } 22 | `; 23 | 24 | export const UPDATE_CATEGORY_MUTATION = gql` 25 | mutation UPDATE_CATEGORY_MUTATION ( 26 | $id: ID! 27 | $name: String 28 | $slug: String 29 | $parent: String 30 | ) { 31 | updateCategory( 32 | id: $id 33 | name: $name 34 | slug: $slug 35 | parent: $parent 36 | ) { 37 | id 38 | name 39 | slug 40 | parent 41 | } 42 | } 43 | `; 44 | 45 | export const DELETE_CATEGORY_MUTATION = gql` 46 | mutation DELETE_CATEGORY_MUTATION($id: ID!) { 47 | deleteCategory(id: $id) { 48 | id 49 | } 50 | } 51 | `; -------------------------------------------------------------------------------- /web-app/components/Category/CategoryQuery.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const GET_CATEGORIES = gql` 4 | query categories( 5 | $orderBy: QueryCategoriesOrderByInput = { updatedAt: desc } 6 | $first: Int = 10000 7 | $skip: Int 8 | $parentQuery: String = "" 9 | ) { 10 | categories(first: $first, skip: $skip, orderBy: $orderBy, parentQuery: $parentQuery) { 11 | nodes { 12 | id, 13 | slug, 14 | name, 15 | parent 16 | } 17 | totalCount 18 | } 19 | } 20 | `; -------------------------------------------------------------------------------- /web-app/components/Category/ParentCategories.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FruitsVegetable, 3 | FacialCare, 4 | DressIcon, 5 | Handbag, 6 | BookIcon, 7 | FurnitureIcon 8 | } from '../../components/AllSvgIcon-2'; 9 | 10 | export const ParentCategories = { 11 | FruitsVegetable: 'Grocery', 12 | Makeup: 'Makeup', 13 | Bags: 'Bags', 14 | Clothing: 'Clothing', 15 | Furniture: 'Furniture', 16 | Book: 'Book', 17 | } 18 | 19 | export const ParentCategoriesWithIcon = [ 20 | { 21 | name: 'FruitsVegetable', 22 | icon: , 23 | label: 'Grocery', 24 | }, 25 | { 26 | name: 'Makeup', 27 | label: 'Makeup', 28 | icon: , 29 | }, 30 | { 31 | name: 'Bags', 32 | label: 'Bags', 33 | icon: , 34 | }, 35 | { 36 | name: 'Clothing', 37 | label: 'Clothing', 38 | icon: , 39 | }, 40 | { 41 | name: 'Furniture', 42 | label: 'Furniture', 43 | icon: , 44 | }, 45 | { 46 | name: 'Book', 47 | label: 'Book', 48 | icon: , 49 | }, 50 | ]; -------------------------------------------------------------------------------- /web-app/components/Dashboard/Customers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | 4 | import { 5 | Card, 6 | CardContent, 7 | CardActions, 8 | Grid, 9 | Typography, 10 | colors, 11 | makeStyles 12 | } from '@material-ui/core'; 13 | import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; 14 | 15 | const cardColor = colors.green['500'] 16 | 17 | const useStyles = makeStyles((theme) => ({ 18 | root: { 19 | height: '100%' 20 | }, 21 | avatar: { 22 | height: 56, 23 | width: 56 24 | }, 25 | differenceIcon: { 26 | color: colors.green[900] 27 | }, 28 | differenceValue: { 29 | color: colors.green[900], 30 | marginRight: theme.spacing(1) 31 | }, 32 | cardHeader: { 33 | color: cardColor, 34 | fontWeight: 400, 35 | textTransform: 'uppercase' 36 | }, 37 | amount: { 38 | fontWeight: 300 39 | }, 40 | bgCover: { 41 | backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,0.8) 0%,rgba(255,255,255,0.8) 100%), url(/images/customers.svg)", 42 | backgroundAttachment: "static", 43 | backgroundPosition: "right", 44 | backgroundRepeat: "no-repeat", 45 | backgroundSize: "30%", 46 | borderLeft: `5px solid ${cardColor}`, 47 | }, 48 | actionBar: { 49 | background: "linear-gradient(90deg, rgba(187,179,179,1) 0%, rgba(255,255,255,1) 100%)", 50 | borderLeft: `5px solid ${cardColor}` 51 | } 52 | 53 | })); 54 | 55 | type Props = { 56 | className: string 57 | }; 58 | 59 | const Customers: React.FC = ({ className, ...rest }) => { 60 | 61 | const classes = useStyles(); 62 | 63 | return ( 64 | 68 | 69 | 70 | 75 | 76 | 81 | customers 82 | 83 | 88 | $24,000 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 100 | 122% 101 | 102 | 106 | Since last month 107 | 108 | 109 | 110 | ); 111 | }; 112 | 113 | export default Customers; 114 | -------------------------------------------------------------------------------- /web-app/components/Dashboard/DailyVisitsInsight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import loadable from '@loadable/component' 4 | 5 | const Chart = loadable(() => import('react-apexcharts')); 6 | 7 | import { 8 | Box, 9 | Button, 10 | Card, 11 | CardContent, 12 | CardHeader, 13 | Divider, 14 | useTheme, 15 | makeStyles, 16 | colors 17 | } from '@material-ui/core'; 18 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; 19 | import ArrowRightIcon from '@material-ui/icons/ArrowRight'; 20 | 21 | const useStyles = makeStyles(() => ({ 22 | root: {} 23 | })); 24 | 25 | type Props = { 26 | className: string 27 | }; 28 | 29 | const DailyVisitsInsight: React.FC = ({ className, ...rest }) => { 30 | const classes = useStyles(); 31 | const theme = useTheme(); 32 | 33 | const options = { 34 | series: [{ 35 | name: 'Day', 36 | data: [31, 40, 28, 51, 42, 109, 100] 37 | }, { 38 | name: 'Night', 39 | data: [11, 32, 45, 32, 34, 52, 41] 40 | }], 41 | options: { 42 | chart: { 43 | height: 350, 44 | type: 'area' 45 | }, 46 | dataLabels: { 47 | enabled: false 48 | }, 49 | stroke: { 50 | curve: 'smooth' 51 | }, 52 | xaxis: { 53 | type: 'datetime', 54 | categories: ["2018-09-19T00:00:00.000Z", "2018-09-19T01:30:00.000Z", "2018-09-19T02:30:00.000Z", "2018-09-19T03:30:00.000Z", "2018-09-19T04:30:00.000Z", "2018-09-19T05:30:00.000Z", "2018-09-19T06:30:00.000Z"] 55 | }, 56 | tooltip: { 57 | x: { 58 | format: 'dd/MM/yy HH:mm' 59 | }, 60 | }, 61 | }, 62 | }; 63 | 64 | return ( 65 | 69 | } 73 | size="small" 74 | variant="text" 75 | > 76 | Last 7 days 77 | 78 | )} 79 | title="Daily Visits Insights" 80 | /> 81 | 82 | 83 | 87 | 88 | 94 | 95 | 96 | 97 | 98 | 99 | ); 100 | }; 101 | 102 | export default DailyVisitsInsight; -------------------------------------------------------------------------------- /web-app/components/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Container, 4 | Grid, 5 | makeStyles 6 | } from '@material-ui/core'; 7 | // import Page from 'src/components/Page'; 8 | import Sale from './Sale'; 9 | import LatestOrders from './LatestOrders'; 10 | import LatestProducts from './LatestProducts'; 11 | import DailyVisitsInsight from './DailyVisitsInsight'; 12 | import Resolution from './Resolution'; 13 | import TotalCustomers from './Customers'; 14 | import TotalProfit from './TotalProfit'; 15 | import TrafficByDevice from './TrafficByDevice'; 16 | import ProfitAnalysis from './ProfitAnalysis'; 17 | import SaleCategoryAnalysis from './SaleCategoryAnalysis'; 18 | 19 | const useStyles = makeStyles((theme) => ({ 20 | root: { 21 | backgroundColor: theme.palette.background.default, 22 | minHeight: '100%', 23 | paddingBottom: theme.spacing(3), 24 | paddingTop: theme.spacing(3) 25 | }, 26 | firstRow: { 27 | height: 'auto' 28 | } 29 | })); 30 | 31 | const Dashboard = () => { 32 | const classes = useStyles(); 33 | 34 | return ( 35 | 36 | 37 | 41 | 48 | 49 | 50 | 57 | 58 | 59 | 66 | 67 | 68 | 75 | 76 | 77 | 84 | 85 | 86 | 93 | 94 | 95 | 96 | 103 | 104 | 105 | 112 | 113 | 114 | 115 | 122 | 123 | 124 | 131 | 132 | 133 | 134 | 135 | 136 | ); 137 | }; 138 | 139 | export default Dashboard; 140 | -------------------------------------------------------------------------------- /web-app/components/Dashboard/LatestProducts.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import clsx from 'clsx'; 3 | import PropTypes from 'prop-types'; 4 | import { v4 as uuid } from 'uuid'; 5 | import moment from 'moment'; 6 | import { 7 | Box, 8 | Button, 9 | Card, 10 | CardHeader, 11 | Divider, 12 | IconButton, 13 | List, 14 | ListItem, 15 | ListItemAvatar, 16 | ListItemText, 17 | makeStyles 18 | } from '@material-ui/core'; 19 | import MoreVertIcon from '@material-ui/icons/MoreVert'; 20 | import ArrowRightIcon from '@material-ui/icons/ArrowRight'; 21 | 22 | const data = [ 23 | { 24 | id: uuid(), 25 | name: 'India gate rice', 26 | imageUrl: '/images/product/9.jpg', 27 | updatedAt: moment().subtract(1, 'days') 28 | }, 29 | { 30 | id: uuid(), 31 | name: 'Maggi 4 value pack', 32 | imageUrl: '/images/product/7.jpg', 33 | updatedAt: moment().subtract(5, 'hours') 34 | }, 35 | { 36 | id: uuid(), 37 | name: 'Brown rice', 38 | imageUrl: '/images/product/10.jpg', 39 | updatedAt: moment().subtract(6, 'hours') 40 | }, 41 | { 42 | id: uuid(), 43 | name: 'Mango', 44 | imageUrl: '/images/product/4.jpg', 45 | updatedAt: moment().subtract(8, 'hours') 46 | }, 47 | { 48 | id: uuid(), 49 | name: 'Strawberry', 50 | imageUrl: '/images/product/2.jpg', 51 | updatedAt: moment().subtract(9, 'hours') 52 | } 53 | ]; 54 | 55 | const useStyles = makeStyles(({ 56 | root: { 57 | height: '100%' 58 | }, 59 | image: { 60 | height: 48, 61 | width: 48 62 | } 63 | })); 64 | 65 | type Props = { 66 | className: string 67 | }; 68 | 69 | const LatestProducts: React.FC = ({ className, ...rest }) => { 70 | const classes = useStyles(); 71 | const [products] = useState(data); 72 | 73 | return ( 74 | 78 | 82 | 83 | 84 | {products.map((product, i) => ( 85 | 89 | 90 | Product 95 | 96 | 100 | 104 | 105 | 106 | 107 | ))} 108 | 109 | 110 | 115 | 123 | 124 | 125 | ); 126 | }; 127 | 128 | export default LatestProducts; 129 | -------------------------------------------------------------------------------- /web-app/components/Dashboard/ProfitAnalysis.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import loadable from '@loadable/component' 4 | 5 | const Chart = loadable(() => import('react-apexcharts')); 6 | 7 | import { 8 | Box, 9 | Button, 10 | Card, 11 | CardContent, 12 | CardHeader, 13 | Divider, 14 | useTheme, 15 | makeStyles, 16 | colors 17 | } from '@material-ui/core'; 18 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; 19 | import ArrowRightIcon from '@material-ui/icons/ArrowRight'; 20 | 21 | const useStyles = makeStyles(() => ({ 22 | root: {} 23 | })); 24 | 25 | type Props = { 26 | className: string 27 | }; 28 | 29 | const ProfitAnalysis: React.FC = ({ className, ...rest }) => { 30 | const classes = useStyles(); 31 | const theme = useTheme(); 32 | 33 | const options = { 34 | series: [{ 35 | name: 'Net Profit', 36 | data: [44, 55, 57, 56, 61, 58, 63, 60, 66] 37 | }, { 38 | name: 'Revenue', 39 | data: [76, 85, 101, 98, 87, 105, 91, 114, 94] 40 | }, { 41 | name: 'Free Cash Flow', 42 | data: [35, 41, 36, 26, 45, 48, 52, 53, 41] 43 | }], 44 | options: { 45 | chart: { 46 | type: 'bar', 47 | height: 350 48 | }, 49 | plotOptions: { 50 | bar: { 51 | horizontal: false, 52 | columnWidth: '55%', 53 | endingShape: 'rounded' 54 | }, 55 | }, 56 | dataLabels: { 57 | enabled: false 58 | }, 59 | stroke: { 60 | show: true, 61 | width: 2, 62 | colors: ['transparent'] 63 | }, 64 | xaxis: { 65 | categories: ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct'], 66 | }, 67 | yaxis: { 68 | title: { 69 | text: '$ (thousands)' 70 | } 71 | }, 72 | fill: { 73 | opacity: 1 74 | }, 75 | tooltip: { 76 | y: { 77 | formatter: function (val: any) { 78 | return "$ " + val + " thousands" 79 | } 80 | } 81 | } 82 | }, 83 | 84 | 85 | }; 86 | 87 | return ( 88 | 92 | } 96 | size="small" 97 | variant="text" 98 | > 99 | Last 7 days 100 | 101 | )} 102 | title="Profit Analysis" 103 | /> 104 | 105 | 106 | 110 | 111 | 117 | 118 | 119 | 120 | 121 | 122 | ); 123 | }; 124 | 125 | export default ProfitAnalysis; -------------------------------------------------------------------------------- /web-app/components/Dashboard/Resolution.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { 4 | Avatar, 5 | Box, 6 | Card, 7 | CardContent, 8 | CardActions, 9 | Grid, 10 | Typography, 11 | colors, 12 | makeStyles 13 | } from '@material-ui/core'; 14 | 15 | const cardColor = colors.orange['500'] 16 | 17 | const useStyles = makeStyles((theme) => ({ 18 | root: { 19 | height: '100%' 20 | }, 21 | avatar: { 22 | height: 56, 23 | width: 56 24 | }, 25 | differenceIcon: { 26 | color: colors.green[900] 27 | }, 28 | differenceValue: { 29 | color: colors.green[900], 30 | marginRight: theme.spacing(1) 31 | }, 32 | cardHeader: { 33 | color: cardColor, 34 | fontWeight: 400, 35 | textTransform: 'uppercase' 36 | }, 37 | amount: { 38 | fontWeight: 300 39 | }, 40 | bgCover: { 41 | backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,0.8) 0%,rgba(255,255,255,0.8) 100%), url(/images/conversation.svg)", 42 | backgroundAttachment: "static", 43 | backgroundPosition: "right", 44 | backgroundRepeat: "no-repeat", 45 | backgroundSize: "30%", 46 | borderLeft: `5px solid ${cardColor}`, 47 | }, 48 | actionBar: { 49 | background: "linear-gradient(90deg, rgba(187,179,179,1) 0%, rgba(255,255,255,1) 100%)", 50 | borderLeft: `5px solid ${cardColor}`, 51 | padding: '10px' 52 | } 53 | 54 | })); 55 | 56 | type Props = { 57 | className: string 58 | }; 59 | 60 | const Resolution: React.FC = ({ className, ...rest }) => { 61 | 62 | const classes = useStyles(); 63 | 64 | return ( 65 | 69 | 70 | 71 | 76 | 77 | 82 | RESOLUTION 83 | 84 | 89 | 50% 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 101 | Total customer resolution pending 102 | 103 | 104 | 105 | ); 106 | }; 107 | export default Resolution; 108 | -------------------------------------------------------------------------------- /web-app/components/Dashboard/Sale.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import PropTypes from 'prop-types'; 4 | import { 5 | Avatar, 6 | Box, 7 | Card, 8 | CardContent, 9 | CardActions, 10 | Grid, 11 | Typography, 12 | colors, 13 | makeStyles 14 | } from '@material-ui/core'; 15 | import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'; 16 | 17 | const cardColor = colors.purple['500'] 18 | 19 | const useStyles = makeStyles((theme) => ({ 20 | root: { 21 | height: '100%' 22 | }, 23 | avatar: { 24 | height: 56, 25 | width: 56 26 | }, 27 | differenceIcon: { 28 | color: colors.red[900] 29 | }, 30 | differenceValue: { 31 | color: colors.red[900], 32 | marginRight: theme.spacing(1) 33 | }, 34 | cardHeader: { 35 | color: cardColor, 36 | fontWeight: 400 37 | }, 38 | amount: { 39 | fontWeight: 300 40 | }, 41 | bgCover: { 42 | backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,0.8) 0%,rgba(255,255,255,0.8) 100%), url(/images/sales.svg)", 43 | backgroundAttachment: "static", 44 | backgroundPosition: "right", 45 | backgroundRepeat: "no-repeat", 46 | backgroundSize: "30%", 47 | borderLeft: `5px solid ${cardColor}`, 48 | }, 49 | actionBar: { 50 | background: "linear-gradient(90deg, rgba(187,179,179,1) 0%, rgba(255,255,255,1) 100%)", 51 | borderLeft: `5px solid ${cardColor}` 52 | } 53 | 54 | })); 55 | 56 | type Props = { 57 | className: string 58 | }; 59 | 60 | const Sale: React.FC = ({ className, ...rest }) => { 61 | 62 | const classes = useStyles(); 63 | 64 | return ( 65 | 69 | 70 | 71 | 76 | 77 | 82 | SALE 83 | 84 | 89 | $24,000 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 101 | 122% 102 | 103 | 107 | Since last month 108 | 109 | 110 | 111 | ); 112 | }; 113 | 114 | export default Sale; 115 | -------------------------------------------------------------------------------- /web-app/components/Dashboard/SaleCategoryAnalysis.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import loadable from '@loadable/component' 4 | 5 | const Chart = loadable(() => import('react-apexcharts')); 6 | 7 | import { 8 | Box, 9 | Button, 10 | Card, 11 | CardContent, 12 | CardHeader, 13 | Divider, 14 | useTheme, 15 | makeStyles, 16 | colors 17 | } from '@material-ui/core'; 18 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; 19 | import ArrowRightIcon from '@material-ui/icons/ArrowRight'; 20 | 21 | const useStyles = makeStyles(() => ({ 22 | root: {} 23 | })); 24 | 25 | type Props = { 26 | className: string 27 | }; 28 | 29 | const SaleCategoryAnalysis: React.FC = ({ className, ...rest }) => { 30 | const classes = useStyles(); 31 | const theme = useTheme(); 32 | 33 | const options = { 34 | series: [{ 35 | name: 'Grocery', 36 | data: [44, 55, 41, 67, 22, 43, 21, 49] 37 | }, { 38 | name: 'Clothing', 39 | data: [13, 23, 20, 8, 13, 27, 33, 12] 40 | }, { 41 | name: 'Furniture', 42 | data: [11, 17, 15, 15, 21, 14, 15, 13] 43 | }], 44 | options: { 45 | chart: { 46 | type: 'bar', 47 | height: 350, 48 | stacked: true, 49 | stackType: '100%' 50 | }, 51 | responsive: [{ 52 | breakpoint: 480, 53 | options: { 54 | legend: { 55 | position: 'bottom', 56 | offsetX: -10, 57 | offsetY: 0 58 | } 59 | } 60 | }], 61 | xaxis: { 62 | categories: ['2011 Q1', '2011 Q2', '2011 Q3', '2011 Q4', '2012 Q1', '2012 Q2', 63 | '2012 Q3', '2012 Q4' 64 | ], 65 | }, 66 | fill: { 67 | opacity: 1 68 | }, 69 | legend: { 70 | position: 'right', 71 | offsetX: 0, 72 | offsetY: 50 73 | }, 74 | }, 75 | 76 | 77 | }; 78 | 79 | return ( 80 | 84 | } 88 | size="small" 89 | variant="text" 90 | > 91 | Last 7 days 92 | 93 | )} 94 | title="Product Sales" 95 | /> 96 | 97 | 98 | 102 | 103 | 109 | 110 | 111 | 112 | 113 | 114 | ); 115 | }; 116 | 117 | export default SaleCategoryAnalysis; -------------------------------------------------------------------------------- /web-app/components/Dashboard/TotalProfit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { 4 | Avatar, 5 | Box, 6 | Card, 7 | CardContent, 8 | CardActions, 9 | Grid, 10 | Typography, 11 | colors, 12 | makeStyles 13 | } from '@material-ui/core'; 14 | 15 | 16 | const cardColor = colors.blue['500'] 17 | 18 | const useStyles = makeStyles((theme) => ({ 19 | root: { 20 | height: '100%' 21 | }, 22 | avatar: { 23 | height: 56, 24 | width: 56 25 | }, 26 | differenceIcon: { 27 | color: colors.green[900] 28 | }, 29 | differenceValue: { 30 | color: colors.green[900], 31 | marginRight: theme.spacing(1) 32 | }, 33 | cardHeader: { 34 | color: cardColor, 35 | fontWeight: 400, 36 | textTransform: 'uppercase' 37 | }, 38 | amount: { 39 | fontWeight: 300 40 | }, 41 | bgCover: { 42 | backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,0.8) 0%,rgba(255,255,255,0.8) 100%), url(/images/money-bag.svg)", 43 | backgroundAttachment: "static", 44 | backgroundPosition: "right", 45 | backgroundRepeat: "no-repeat", 46 | backgroundSize: "30%", 47 | borderLeft: `5px solid ${cardColor}`, 48 | }, 49 | actionBar: { 50 | background: "linear-gradient(90deg, rgba(187,179,179,1) 0%, rgba(255,255,255,1) 100%)", 51 | borderLeft: `5px solid ${cardColor}`, 52 | padding: '10px' 53 | } 54 | 55 | })); 56 | 57 | type Props = { 58 | className: string 59 | }; 60 | 61 | const Resolution: React.FC = ({ className, ...rest }) => { 62 | 63 | const classes = useStyles(); 64 | 65 | return ( 66 | 70 | 71 | 72 | 77 | 78 | 83 | PROFIT 84 | 85 | 90 | $128,678 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 102 | Total profit this year 103 | 104 | 105 | 106 | ); 107 | }; 108 | export default Resolution; 109 | -------------------------------------------------------------------------------- /web-app/components/Dashboard/TrafficByDevice.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import PropTypes from 'prop-types'; 4 | import loadable from '@loadable/component' 5 | const Chart = loadable(() => import('react-apexcharts')); 6 | 7 | import { 8 | Box, 9 | Card, 10 | CardContent, 11 | CardHeader, 12 | Divider, 13 | Typography, 14 | colors, 15 | makeStyles, 16 | useTheme 17 | } from '@material-ui/core'; 18 | import LaptopMacIcon from '@material-ui/icons/LaptopMac'; 19 | import PhoneIcon from '@material-ui/icons/Phone'; 20 | import TabletIcon from '@material-ui/icons/Tablet'; 21 | 22 | const useStyles = makeStyles(() => ({ 23 | root: { 24 | height: '100%' 25 | }, 26 | devicePercent: { 27 | fontWeight: 400, 28 | } 29 | })); 30 | 31 | type Props = { 32 | className: string 33 | }; 34 | 35 | const TrafficByDevice: React.FC = ({ className, ...rest }) => { 36 | const classes = useStyles(); 37 | const theme = useTheme(); 38 | 39 | const series = [30, 10, 60] 40 | const label = ['Tablet', 'Mobile', 'Laptop'] 41 | const options: any = { 42 | chart: { 43 | type: 'donut', 44 | }, 45 | plotOptions: { 46 | pie: { 47 | startAngle: -90, 48 | endAngle: 270 49 | } 50 | }, 51 | dataLabels: { 52 | enabled: true 53 | }, 54 | fill: { 55 | type: 'gradient', 56 | }, 57 | legend: { 58 | formatter: function (val: any, opts: any) { 59 | return label[opts.seriesIndex] 60 | } 61 | }, 62 | title: { 63 | text: '' 64 | }, 65 | responsive: [{ 66 | breakpoint: 2480, 67 | options: { 68 | chart: { 69 | width: 370 70 | }, 71 | legend: { 72 | position: 'bottom' 73 | } 74 | } 75 | }] 76 | } 77 | 78 | return ( 79 | 83 | 84 | 85 | 86 | 87 | 92 | 93 | 94 | 95 | 96 | ); 97 | }; 98 | 99 | export default TrafficByDevice; 100 | -------------------------------------------------------------------------------- /web-app/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | 4 | type LoaderProps = {}; 5 | 6 | const Container = styled.div` 7 | display: flex; 8 | height: 100%; 9 | width: 100%; 10 | align-items: center; 11 | justify-content: center; 12 | `; 13 | 14 | const Plane = styled.div` 15 | perspective: 120px; 16 | width: 40px; 17 | height: 40px; 18 | background-image: linear-gradient(to bottom right, #2e5bff, #d63649); 19 | animation: flip 1s infinite; 20 | 21 | @keyframes flip { 22 | 50% { 23 | transform: rotateY(180deg); 24 | } 25 | 100% { 26 | transform: rotateY(180deg) rotateX(180deg); 27 | } 28 | } 29 | `; 30 | 31 | const Loader: React.FC = () => { 32 | return ( 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default Loader; 40 | -------------------------------------------------------------------------------- /web-app/components/LoginLayout/LoginLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Box from '@material-ui/core/Box'; 3 | 4 | 5 | type Props = {}; 6 | 7 | const LoginLayout: React.FC = ({ children }) => { 8 | return ( 9 | 10 | { children} 11 | 12 | ); 13 | }; 14 | 15 | export default LoginLayout; 16 | -------------------------------------------------------------------------------- /web-app/components/Material/ErrorMessage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import Button from '@material-ui/core/Button'; 5 | import Snackbar from '@material-ui/core/Snackbar'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import CloseIcon from '@material-ui/icons/Close'; 8 | 9 | const ErrorStyles = styled.div` 10 | padding: 2rem; 11 | background: white; 12 | margin: 2rem 0; 13 | border: 1px solid rgba(0, 0, 0, 0.05); 14 | border-left: 5px solid red; 15 | p { 16 | margin: 0; 17 | font-weight: 100; 18 | } 19 | strong { 20 | margin-right: 1rem; 21 | } 22 | `; 23 | 24 | const DisplayError = ({ error }) => { 25 | 26 | const [open, setOpen] = React.useState(true); 27 | 28 | const handleClose = (event, reason) => { 29 | if (reason === 'clickaway') { 30 | return; 31 | } 32 | setOpen(false); 33 | }; 34 | 35 | if (!error || !error.message) return null; 36 | if (error.networkError && error.networkError.result && error.networkError.result.errors.length) { 37 | return error.networkError.result.errors.map((error, i) => ( 38 | 39 |

40 | Shoot! 41 | {error.message.replace('GraphQL error: ', '')} 42 |

43 |
44 | )); 45 | } 46 | return ( 47 | 48 |

49 | Shoot! 50 | {error.message.replace('GraphQL error: ', '')} 51 |

52 | 63 | 66 | 67 | 68 | 69 | 70 | } 71 | /> 72 |
73 | ); 74 | }; 75 | 76 | DisplayError.defaultProps = { 77 | error: {}, 78 | }; 79 | 80 | DisplayError.propTypes = { 81 | error: PropTypes.object, 82 | }; 83 | 84 | export default DisplayError; 85 | -------------------------------------------------------------------------------- /web-app/components/Material/Snackbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Snackbar from '@material-ui/core/Snackbar'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import CloseIcon from '@material-ui/icons/Close'; 6 | 7 | export default function ShowSnackbar() { 8 | 9 | const [open, setOpen] = React.useState(true); 10 | setOpen(true); 11 | const handleClose = (event: React.SyntheticEvent | React.MouseEvent, reason?: string) => { 12 | if (reason === 'clickaway') { 13 | return; 14 | } 15 | 16 | setOpen(false); 17 | }; 18 | 19 | return ( 20 |
21 | 32 | 35 | 36 | 37 | 38 | 39 | } 40 | /> 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /web-app/components/Material/SuccessMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Snackbar from '@material-ui/core/Snackbar'; 3 | import MuiAlert from '@material-ui/lab/Alert'; 4 | import { useApolloClient, useQuery, useMutation } from '@apollo/react-hooks'; 5 | import { SNACKBAR_STATE_QUERY } from '../../graphql/queries'; 6 | // import { TOGGLE_SNACKBAR_MUTATION } from '../../graphql/mutations'; 7 | import gql from 'graphql-tag'; 8 | 9 | function Alert(props) { 10 | return ; 11 | } 12 | 13 | const Message = () => { 14 | const { data, error, loading } = useQuery(SNACKBAR_STATE_QUERY); 15 | const [toggleSnackBar] = useMutation(TOGGLE_SNACKBAR_MUTATION); 16 | if (!loading && data) { 17 | return ( 18 | toggleSnackBar({ variables: { msg: '', type: 'success' } })} 26 | > toggleSnackBar({ variables: { msg: '', type: 'success' } })} 28 | severity={data.snackType}> 29 | {data.snackMsg} 30 | 31 | ) 32 | } else { 33 | return <> 34 | } 35 | }; 36 | 37 | const TOGGLE_SNACKBAR_MUTATION = gql` 38 | mutation toggleSnackBar{ 39 | toggleSnackBar(msg: $msg, type: $type) @client 40 | } 41 | `; 42 | export default Message; 43 | export { SNACKBAR_STATE_QUERY, TOGGLE_SNACKBAR_MUTATION }; -------------------------------------------------------------------------------- /web-app/components/Product/AddProductStyle.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | makeStyles, 3 | createStyles, 4 | Theme, 5 | } from '@material-ui/core' 6 | 7 | export const useStyles = makeStyles((theme: Theme) => 8 | createStyles({ 9 | root: { 10 | display: 'block', 11 | margin: '0 auto', 12 | }, 13 | uploadButton: { 14 | paddingLeft: '20px', 15 | paddingRight: '20px' 16 | }, 17 | textField: { 18 | '& > *': { 19 | width: '100%', 20 | }, 21 | }, 22 | submitButton: { 23 | marginTop: '24px', 24 | textAlign: 'right' 25 | }, 26 | FileContainer: { 27 | display: 'flex', 28 | flexDirection: 'column', 29 | fontFamily: 'sans-serif' 30 | }, 31 | thumbsContainer: { 32 | display: 'flex', 33 | flexDirection: 'row', 34 | flexWrap: 'wrap', 35 | marginTop: 16 36 | }, 37 | thumb: { 38 | display: 'inline-flex', 39 | borderRadius: 2, 40 | border: '1px solid #eaeaea', 41 | marginBottom: 8, 42 | marginRight: 8, 43 | width: 100, 44 | height: 100, 45 | padding: 4, 46 | boxSizing: 'border-box' 47 | }, 48 | thumbInner: { 49 | display: 'flex', 50 | minWidth: 0, 51 | overflow: 'hidden' 52 | }, 53 | img: { 54 | display: 'block', 55 | width: 'auto', 56 | height: '100%' 57 | }, 58 | media: { 59 | padding: '0 5px', 60 | height: '100px', 61 | textAlign: 'center', 62 | marginTop: '10px', 63 | margin: '0 auto', 64 | }, 65 | icon: { 66 | color: 'rgba(255, 255, 255, 0.54)', 67 | }, 68 | gridList: { 69 | width: 350, 70 | }, 71 | titleBar: { 72 | background: 73 | 'linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.3) 70%, rgba(0,0,0,0) 100%)', 74 | }, 75 | }) 76 | ) -------------------------------------------------------------------------------- /web-app/components/Product/ProductMutation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | export const CREATE_PRODUCT = gql` 3 | mutation createProduct( 4 | $name: String! 5 | $description: String! 6 | $price: Int! 7 | $discount: Int! 8 | $salePrice: Int! 9 | $sku: String! 10 | $unit: String! 11 | $categoryId: ID! 12 | $images: [ProductImageCreateWithoutProductInput!]! 13 | 14 | ) { 15 | createProduct( 16 | name: $name 17 | description: $description 18 | price: $price 19 | discount: $discount 20 | salePrice: $salePrice 21 | sku: $sku 22 | unit: $unit 23 | categoryId: $categoryId 24 | images: $images 25 | ) { 26 | id 27 | name 28 | description 29 | price 30 | discount 31 | salePrice 32 | sku 33 | unit 34 | Category { 35 | name 36 | parent 37 | } 38 | ProductImages { 39 | image 40 | } 41 | } 42 | } 43 | `; 44 | 45 | export const UPDATE_PRODUCT_MUTATION = gql` 46 | mutation UPDATE_PRODUCT_MUTATION ( 47 | $id: ID! 48 | $name: String! 49 | $description: String! 50 | $price: Int! 51 | $discount: Int! 52 | $salePrice: Int! 53 | $sku: String! 54 | $unit: String! 55 | $categoryId: ID! 56 | $images: [ProductImageCreateWithoutProductInput!]! 57 | $alreadyUploadedImages: [ProductImageCreateWithoutProductInput!]! 58 | ) { 59 | updateProduct( 60 | id: $id 61 | name: $name 62 | description: $description 63 | discount: $discount 64 | salePrice: $salePrice 65 | sku: $sku 66 | price: $price 67 | unit: $unit 68 | categoryId: $categoryId 69 | images: $images 70 | alreadyUploadedImages: $alreadyUploadedImages 71 | ) { 72 | id 73 | name 74 | description 75 | price 76 | discount 77 | salePrice 78 | sku 79 | unit 80 | Category { 81 | name 82 | parent 83 | } 84 | ProductImages { 85 | id 86 | image 87 | } 88 | } 89 | } 90 | `; -------------------------------------------------------------------------------- /web-app/components/Product/ProductQuery.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const GET_PRODUCTS = gql` 4 | query products( 5 | $orderBy: ProductOrderByInput = {updatedAt: desc}, 6 | $first: Int=1, 7 | $skip: Int, 8 | $nameQuery: String, 9 | $discount: String 10 | ) { 11 | products(first: $first, skip: $skip, orderBy: $orderBy, nameQuery: $nameQuery, discountRange: $discount) { 12 | nodes { 13 | id 14 | name 15 | price 16 | discount 17 | salePrice 18 | sku 19 | unit 20 | description 21 | Category { 22 | id 23 | name 24 | parent 25 | } 26 | ProductImages{ 27 | id 28 | image 29 | } 30 | 31 | } 32 | totalCount 33 | } 34 | } 35 | `; -------------------------------------------------------------------------------- /web-app/components/Product/ProductStyle.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; 2 | import { red } from '@material-ui/core/colors'; 3 | 4 | export const useStyles = makeStyles((theme: Theme) => 5 | createStyles({ 6 | root: { 7 | width: 200, 8 | }, 9 | formControl: { 10 | margin: theme.spacing(1), 11 | minWidth: 120, 12 | }, 13 | title: { 14 | fontSize: '14px', 15 | height: '35px', 16 | overflow: 'hidden', 17 | lineHeight: '1.3em', 18 | textOverflow: 'ellipsis', 19 | color: '#161f6a', 20 | // color: theme.palette.primary.dark, 21 | fontWeight: theme.typography.fontWeightMedium, 22 | textAlign: 'center' 23 | }, 24 | subheader: { 25 | fontSize: '12px', 26 | marginTop: '5px', 27 | textAlign: 'center' 28 | }, 29 | cardContent: { 30 | paddingTop: 0, 31 | }, 32 | discountedPrice: { 33 | paddingTop: 0, 34 | color: theme.palette.primary.dark 35 | }, 36 | price: { 37 | marginLeft: '10px', 38 | color: theme.palette.grey[500], 39 | fontSize: '12px', 40 | textDecoration: 'line-through' 41 | }, 42 | media: { 43 | // height: '240px', 44 | height: '120px', 45 | width: 120, 46 | textAlign: 'center', 47 | marginTop: '10px', 48 | margin: '0 auto', 49 | // paddingTop: '56.25%', // 16:9 50 | }, 51 | expand: { 52 | transform: 'rotate(0deg)', 53 | marginLeft: 'auto', 54 | transition: theme.transitions.create('transform', { 55 | duration: theme.transitions.duration.shortest, 56 | }), 57 | }, 58 | expandOpen: { 59 | transform: 'rotate(180deg)', 60 | }, 61 | avatar: { 62 | backgroundColor: red[500], 63 | }, 64 | parentImageContainer: { 65 | position: 'relative' 66 | }, 67 | discountInPercent: { 68 | // ...$theme.typography.fontBold12, 69 | color: '#ffffff', 70 | lineHeight: '1.7', 71 | backgroundColor: theme.palette.secondary.main, 72 | paddingLeft: '7px', 73 | paddingRight: '7px', 74 | display: 'inline-block', 75 | position: 'absolute', 76 | bottom: '10px', 77 | right: '0', 78 | fontSize: '12px', 79 | fontWeight: theme.typography.fontWeightMedium, 80 | '&::before': { 81 | content: '""', 82 | position: 'absolute', 83 | left: '-8px', 84 | top: '0', 85 | width: '0', 86 | height: '0', 87 | borderStyle: 'solid', 88 | borderWidth: '0 8px 12px 0', 89 | borderColor: `transparent ${theme.palette.secondary.main} transparent transparent`, 90 | }, 91 | 92 | '&::after': { 93 | content: '""', 94 | position: 'absolute', 95 | left: '-8px', 96 | bottom: ' 0', 97 | width: ' 0', 98 | height: '0', 99 | borderStyle: 'solid', 100 | borderWidth: '0 0 12px 8px', 101 | borderColor: `transparent transparent ${theme.palette.secondary.main}`, 102 | }, 103 | }, 104 | appBar: { 105 | position: 'relative', 106 | }, 107 | dialogTitle: { 108 | marginLeft: theme.spacing(2), 109 | flex: 1, 110 | }, 111 | drawerOpen: { 112 | color: 'red' 113 | }, 114 | drawerClose: { 115 | position: 'relative!important' as any 116 | }, 117 | newProduct: { 118 | bottom: theme.spacing(2), 119 | position: 'fixed', 120 | right: theme.spacing(2) 121 | }, 122 | filterBox: { 123 | // marginTop: theme.spacing(2), 124 | marginBottom: theme.spacing(2), 125 | // padding: theme.spacing(2) 126 | }, 127 | pagination: { 128 | padding: theme.spacing(2) 129 | }, 130 | searchInput: { 131 | padding: theme.spacing(1) 132 | }, 133 | searchBar: { 134 | background: theme.palette.grey[100], 135 | borderWidth: '1px', 136 | boxShadow: theme.shadows[0], 137 | marginBottom: theme.spacing(2), 138 | borderColor: theme.palette.primary.main, 139 | borderRadius: '5px', 140 | }, 141 | searchLegend: { 142 | ...theme.typography.subtitle1, 143 | background: theme.palette.primary.main, 144 | color: theme.palette.primary.contrastText, 145 | paddingLeft: theme.spacing(1), 146 | paddingRight: theme.spacing(1), 147 | // borderRadius: '10%', 148 | fontWeight: theme.typography.fontWeightRegular 149 | } 150 | }), 151 | ); -------------------------------------------------------------------------------- /web-app/components/User/UserMutation.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const UPDATE_USER_MUTATION = gql` 4 | mutation UPDATE_USER_MUTATION ( 5 | $id: ID! 6 | $name: String! 7 | $status: String! 8 | $role: String! 9 | ) { 10 | updateUser( 11 | id: $id 12 | name: $name 13 | status: $status 14 | role: $role 15 | ) { 16 | id 17 | name 18 | email 19 | status 20 | role 21 | } 22 | } 23 | `; 24 | 25 | export const DELETE_USER_MUTATION = gql` 26 | mutation DELETE_USER_MUTATION($id: ID!) { 27 | deleteAccount(id: $id) { 28 | id 29 | } 30 | } 31 | `; -------------------------------------------------------------------------------- /web-app/components/User/UserQuery.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const USER_PERMISSIONS = gql` 4 | 5 | query users( 6 | $orderBy: QueryUsersOrderByInput = { name: desc } 7 | $first: Int = 10000 8 | $skip: Int 9 | ) { 10 | users(first: $first, skip: $skip, orderBy: $orderBy) { 11 | nodes { 12 | id, 13 | name, 14 | email, 15 | status, 16 | role 17 | } 18 | totalCount 19 | } 20 | } 21 | 22 | `; -------------------------------------------------------------------------------- /web-app/graphql/generated/CompleteOnboardingMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: CompleteOnboardingMutation 8 | // ==================================================== 9 | 10 | export interface CompleteOnboardingMutation { 11 | completeOnboarding: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /web-app/graphql/generated/CurrentUserQuery.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | import { User_role } from "./graphql-global-types"; 7 | 8 | // ==================================================== 9 | // GraphQL query operation: CurrentUserQuery 10 | // ==================================================== 11 | 12 | export interface CurrentUserQuery_me { 13 | __typename: "User"; 14 | id: string; 15 | email: string; 16 | name: string; 17 | role: User_role; 18 | hasVerifiedEmail: boolean | null; 19 | hasCompletedOnboarding: boolean; 20 | } 21 | 22 | export interface CurrentUserQuery { 23 | me: CurrentUserQuery_me | null; 24 | } 25 | -------------------------------------------------------------------------------- /web-app/graphql/generated/DELETE_CATEGORY_MUTATION.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: DELETE_CATEGORY_MUTATION 8 | // ==================================================== 9 | 10 | export interface DELETE_CATEGORY_MUTATION_deleteCategory { 11 | __typename: "Category"; 12 | id: string; 13 | } 14 | 15 | export interface DELETE_CATEGORY_MUTATION { 16 | deleteCategory: DELETE_CATEGORY_MUTATION_deleteCategory; 17 | } 18 | 19 | export interface DELETE_CATEGORY_MUTATIONVariables { 20 | id: string; 21 | } 22 | -------------------------------------------------------------------------------- /web-app/graphql/generated/DELETE_USER_MUTATION.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: DELETE_USER_MUTATION 8 | // ==================================================== 9 | 10 | export interface DELETE_USER_MUTATION_deleteAccount { 11 | __typename: "User"; 12 | id: string; 13 | } 14 | 15 | export interface DELETE_USER_MUTATION { 16 | deleteAccount: DELETE_USER_MUTATION_deleteAccount; 17 | } 18 | 19 | export interface DELETE_USER_MUTATIONVariables { 20 | id: string; 21 | } 22 | -------------------------------------------------------------------------------- /web-app/graphql/generated/DeleteAccountMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: DeleteAccountMutation 8 | // ==================================================== 9 | 10 | export interface DeleteAccountMutation_deleteAccount { 11 | __typename: "User"; 12 | id: string; 13 | } 14 | 15 | export interface DeleteAccountMutation { 16 | deleteAccount: DeleteAccountMutation_deleteAccount; 17 | } 18 | 19 | export interface DeleteAccountMutationVariables { 20 | id: string; 21 | } 22 | -------------------------------------------------------------------------------- /web-app/graphql/generated/LoginMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: LoginMutation 8 | // ==================================================== 9 | 10 | export interface LoginMutation_login { 11 | __typename: "User"; 12 | email: string; 13 | } 14 | 15 | export interface LoginMutation { 16 | login: LoginMutation_login; 17 | } 18 | 19 | export interface LoginMutationVariables { 20 | email: string; 21 | password: string; 22 | } 23 | -------------------------------------------------------------------------------- /web-app/graphql/generated/LogoutMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: LogoutMutation 8 | // ==================================================== 9 | 10 | export interface LogoutMutation { 11 | logout: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /web-app/graphql/generated/RequestResetPasswordMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: RequestResetPasswordMutation 8 | // ==================================================== 9 | 10 | export interface RequestResetPasswordMutation { 11 | requestPasswordReset: boolean; 12 | } 13 | 14 | export interface RequestResetPasswordMutationVariables { 15 | email: string; 16 | } 17 | -------------------------------------------------------------------------------- /web-app/graphql/generated/ResetPasswordMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: ResetPasswordMutation 8 | // ==================================================== 9 | 10 | export interface ResetPasswordMutation_resetPassword { 11 | __typename: "User"; 12 | email: string; 13 | } 14 | 15 | export interface ResetPasswordMutation { 16 | resetPassword: ResetPasswordMutation_resetPassword; 17 | } 18 | 19 | export interface ResetPasswordMutationVariables { 20 | resetToken: string; 21 | password: string; 22 | confirmPassword: string; 23 | } 24 | -------------------------------------------------------------------------------- /web-app/graphql/generated/SignupMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: SignupMutation 8 | // ==================================================== 9 | 10 | export interface SignupMutation_signup { 11 | __typename: "User"; 12 | email: string; 13 | } 14 | 15 | export interface SignupMutation { 16 | signup: SignupMutation_signup; 17 | } 18 | 19 | export interface SignupMutationVariables { 20 | name: string; 21 | email: string; 22 | password: string; 23 | confirmPassword: string; 24 | } 25 | -------------------------------------------------------------------------------- /web-app/graphql/generated/UPDATE_CATEGORY_MUTATION.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: UPDATE_CATEGORY_MUTATION 8 | // ==================================================== 9 | 10 | export interface UPDATE_CATEGORY_MUTATION_updateCategory { 11 | __typename: "Category"; 12 | id: string; 13 | name: string; 14 | slug: string; 15 | parent: string; 16 | } 17 | 18 | export interface UPDATE_CATEGORY_MUTATION { 19 | updateCategory: UPDATE_CATEGORY_MUTATION_updateCategory; 20 | } 21 | 22 | export interface UPDATE_CATEGORY_MUTATIONVariables { 23 | id: string; 24 | name?: string | null; 25 | slug?: string | null; 26 | parent?: string | null; 27 | } 28 | -------------------------------------------------------------------------------- /web-app/graphql/generated/UPDATE_PRODUCT_MUTATION.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | import { ProductImageCreateWithoutProductInput } from "./graphql-global-types"; 7 | 8 | // ==================================================== 9 | // GraphQL mutation operation: UPDATE_PRODUCT_MUTATION 10 | // ==================================================== 11 | 12 | export interface UPDATE_PRODUCT_MUTATION_updateProduct_Category { 13 | __typename: "Category"; 14 | name: string; 15 | parent: string; 16 | } 17 | 18 | export interface UPDATE_PRODUCT_MUTATION_updateProduct_ProductImages { 19 | __typename: "ProductImage"; 20 | image: string; 21 | } 22 | 23 | export interface UPDATE_PRODUCT_MUTATION_updateProduct { 24 | __typename: "Product"; 25 | id: string; 26 | name: string; 27 | description: string; 28 | price: number; 29 | discount: number; 30 | salePrice: number; 31 | sku: string; 32 | unit: string; 33 | Category: UPDATE_PRODUCT_MUTATION_updateProduct_Category; 34 | ProductImages: UPDATE_PRODUCT_MUTATION_updateProduct_ProductImages[]; 35 | } 36 | 37 | export interface UPDATE_PRODUCT_MUTATION { 38 | updateProduct: UPDATE_PRODUCT_MUTATION_updateProduct; 39 | } 40 | 41 | export interface UPDATE_PRODUCT_MUTATIONVariables { 42 | id: string; 43 | name: string; 44 | description: string; 45 | price: number; 46 | discount: number; 47 | salePrice: number; 48 | sku: string; 49 | unit: string; 50 | categoryId: string; 51 | images: ProductImageCreateWithoutProductInput[]; 52 | } 53 | -------------------------------------------------------------------------------- /web-app/graphql/generated/UPDATE_USER_MUTATION.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | import { User_status, User_role } from "./graphql-global-types"; 7 | 8 | // ==================================================== 9 | // GraphQL mutation operation: UPDATE_USER_MUTATION 10 | // ==================================================== 11 | 12 | export interface UPDATE_USER_MUTATION_updateUser { 13 | __typename: "User"; 14 | id: string; 15 | name: string; 16 | email: string; 17 | status: User_status; 18 | role: User_role; 19 | } 20 | 21 | export interface UPDATE_USER_MUTATION { 22 | updateUser: UPDATE_USER_MUTATION_updateUser; 23 | } 24 | 25 | export interface UPDATE_USER_MUTATIONVariables { 26 | id: string; 27 | name: string; 28 | status: string; 29 | role: string; 30 | } 31 | -------------------------------------------------------------------------------- /web-app/graphql/generated/categories.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | import { QueryCategoriesOrderByInput } from "./graphql-global-types"; 7 | 8 | // ==================================================== 9 | // GraphQL query operation: categories 10 | // ==================================================== 11 | 12 | export interface categories_categories_nodes { 13 | __typename: "Category"; 14 | id: string; 15 | slug: string; 16 | name: string; 17 | parent: string; 18 | } 19 | 20 | export interface categories_categories { 21 | __typename: "QueryCategories_Connection"; 22 | /** 23 | * Flattened list of Category type 24 | */ 25 | nodes: categories_categories_nodes[]; 26 | totalCount: number; 27 | } 28 | 29 | export interface categories { 30 | categories: categories_categories; 31 | } 32 | 33 | export interface categoriesVariables { 34 | orderBy?: QueryCategoriesOrderByInput | null; 35 | first?: number | null; 36 | skip?: number | null; 37 | parentQuery?: string | null; 38 | } 39 | -------------------------------------------------------------------------------- /web-app/graphql/generated/createCategory.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: createCategory 8 | // ==================================================== 9 | 10 | export interface createCategory_createCategory { 11 | __typename: "Category"; 12 | id: string; 13 | name: string; 14 | parent: string; 15 | slug: string; 16 | } 17 | 18 | export interface createCategory { 19 | createCategory: createCategory_createCategory; 20 | } 21 | 22 | export interface createCategoryVariables { 23 | name: string; 24 | parent: string; 25 | slug: string; 26 | } 27 | -------------------------------------------------------------------------------- /web-app/graphql/generated/createProduct.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | import { ProductImageCreateWithoutProductInput } from "./graphql-global-types"; 7 | 8 | // ==================================================== 9 | // GraphQL mutation operation: createProduct 10 | // ==================================================== 11 | 12 | export interface createProduct_createProduct_Category { 13 | __typename: "Category"; 14 | name: string; 15 | parent: string; 16 | } 17 | 18 | export interface createProduct_createProduct_ProductImages { 19 | __typename: "ProductImage"; 20 | image: string; 21 | } 22 | 23 | export interface createProduct_createProduct { 24 | __typename: "Product"; 25 | id: string; 26 | name: string; 27 | description: string; 28 | price: number; 29 | discount: number; 30 | salePrice: number; 31 | sku: string; 32 | unit: string; 33 | Category: createProduct_createProduct_Category; 34 | ProductImages: createProduct_createProduct_ProductImages[]; 35 | } 36 | 37 | export interface createProduct { 38 | createProduct: createProduct_createProduct; 39 | } 40 | 41 | export interface createProductVariables { 42 | name: string; 43 | description: string; 44 | price: number; 45 | discount: number; 46 | salePrice: number; 47 | sku: string; 48 | unit: string; 49 | categoryId: string; 50 | images: ProductImageCreateWithoutProductInput[]; 51 | } 52 | -------------------------------------------------------------------------------- /web-app/graphql/generated/graphql-global-types.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | //============================================================== 7 | // START Enums and Input Objects 8 | //============================================================== 9 | 10 | export enum OrderByArg { 11 | asc = "asc", 12 | desc = "desc", 13 | } 14 | 15 | export enum User_role { 16 | ADMIN = "ADMIN", 17 | MANAGER = "MANAGER", 18 | USER = "USER", 19 | } 20 | 21 | export enum User_status { 22 | ACTIVE = "ACTIVE", 23 | BLOCKED = "BLOCKED", 24 | INACTIVE = "INACTIVE", 25 | } 26 | 27 | export interface ProductImageCreateWithoutProductInput { 28 | createdAt?: any | null; 29 | id?: string | null; 30 | image: string; 31 | updatedAt?: any | null; 32 | } 33 | 34 | export interface ProductOrderByInput { 35 | name?: OrderByArg | null; 36 | price?: OrderByArg | null; 37 | updatedAt?: OrderByArg | null; 38 | } 39 | 40 | export interface QueryCategoriesOrderByInput { 41 | name?: OrderByArg | null; 42 | updatedAt?: OrderByArg | null; 43 | } 44 | 45 | export interface QueryUsersOrderByInput { 46 | name?: OrderByArg | null; 47 | status?: OrderByArg | null; 48 | } 49 | 50 | //============================================================== 51 | // END Enums and Input Objects 52 | //============================================================== 53 | -------------------------------------------------------------------------------- /web-app/graphql/generated/isLeftDrawerOpen.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: isLeftDrawerOpen 8 | // ==================================================== 9 | 10 | export interface isLeftDrawerOpen { 11 | isLeftDrawerOpen: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /web-app/graphql/generated/products.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | import { ProductOrderByInput } from "./graphql-global-types"; 7 | 8 | // ==================================================== 9 | // GraphQL query operation: products 10 | // ==================================================== 11 | 12 | export interface products_products_nodes_Category { 13 | __typename: "Category"; 14 | id: string; 15 | name: string; 16 | parent: string; 17 | } 18 | 19 | export interface products_products_nodes_ProductImages { 20 | __typename: "ProductImage"; 21 | image: string; 22 | } 23 | 24 | export interface products_products_nodes { 25 | __typename: "Product"; 26 | id: string; 27 | name: string; 28 | price: number; 29 | discount: number; 30 | salePrice: number; 31 | sku: string; 32 | unit: string; 33 | description: string; 34 | Category: products_products_nodes_Category; 35 | ProductImages: products_products_nodes_ProductImages[]; 36 | } 37 | 38 | export interface products_products { 39 | __typename: "QueryProducts_Connection"; 40 | /** 41 | * Flattened list of Product type 42 | */ 43 | nodes: products_products_nodes[]; 44 | totalCount: number; 45 | } 46 | 47 | export interface products { 48 | products: products_products; 49 | } 50 | 51 | export interface productsVariables { 52 | orderBy?: ProductOrderByInput | null; 53 | first?: number | null; 54 | skip?: number | null; 55 | nameQuery?: string | null; 56 | discount?: string | null; 57 | } 58 | -------------------------------------------------------------------------------- /web-app/graphql/generated/snackbar.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: snackbar 8 | // ==================================================== 9 | 10 | export interface snackbar { 11 | snackBarOpen: boolean | null; 12 | snackMsg: string | null; 13 | snackType: string | null; 14 | } 15 | -------------------------------------------------------------------------------- /web-app/graphql/generated/users.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | import { QueryUsersOrderByInput, User_status, User_role } from "./graphql-global-types"; 7 | 8 | // ==================================================== 9 | // GraphQL query operation: users 10 | // ==================================================== 11 | 12 | export interface users_users_nodes { 13 | __typename: "User"; 14 | id: string; 15 | name: string; 16 | email: string; 17 | status: User_status; 18 | role: User_role; 19 | } 20 | 21 | export interface users_users { 22 | __typename: "QueryUsers_Connection"; 23 | /** 24 | * Flattened list of User type 25 | */ 26 | nodes: users_users_nodes[]; 27 | totalCount: number; 28 | } 29 | 30 | export interface users { 31 | users: users_users; 32 | } 33 | 34 | export interface usersVariables { 35 | orderBy?: QueryUsersOrderByInput | null; 36 | first?: number | null; 37 | skip?: number | null; 38 | } 39 | -------------------------------------------------------------------------------- /web-app/graphql/mutations/index.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const deleteAccountMutation = gql` 4 | mutation DeleteAccountMutation($id: ID!) { 5 | deleteAccount(id: $id) { 6 | id 7 | } 8 | } 9 | `; 10 | 11 | export const loginMutation = gql` 12 | mutation LoginMutation($email: String!, $password: String!) { 13 | login(email: $email, password: $password) { 14 | email 15 | } 16 | } 17 | `; 18 | 19 | export const logoutMutation = gql` 20 | mutation LogoutMutation { 21 | logout 22 | } 23 | `; 24 | 25 | export const signupMutation = gql` 26 | mutation SignupMutation($name: String!, $email: String!, $password: String!, $confirmPassword: String!) { 27 | signup(name: $name, email: $email, password: $password, confirmPassword: $confirmPassword) { 28 | email 29 | } 30 | } 31 | `; 32 | 33 | export const requestResetPasswordMutation = gql` 34 | mutation RequestResetPasswordMutation($email: String!) { 35 | requestPasswordReset(email: $email) 36 | } 37 | `; 38 | 39 | export const resetPasswordMutation = gql` 40 | mutation ResetPasswordMutation($resetToken: String!, $password: String!, $confirmPassword: String!) { 41 | resetPassword(resetToken: $resetToken, password: $password, confirmPassword: $confirmPassword) { 42 | email 43 | } 44 | } 45 | `; 46 | 47 | export const completeOnboardingMutation = gql` 48 | mutation CompleteOnboardingMutation { 49 | completeOnboarding 50 | } 51 | `; 52 | 53 | // export const TOGGLE_SNACKBAR_MUTATION = gql` 54 | // mutation toggleSnackBar{ 55 | // toggleSnackBar(msg: $msg, type: $type) @client 56 | // } 57 | // `; 58 | 59 | // export const TOGGLE_LEFT_DRAWER_MUTATION = gql` 60 | // mutation toggleLeftDrawer{ 61 | // toggleLeftDrawer(status: "") @client 62 | // } 63 | // `; -------------------------------------------------------------------------------- /web-app/graphql/queries/index.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const currentUserQuery = gql` 4 | query CurrentUserQuery { 5 | me { 6 | id 7 | email 8 | name 9 | role 10 | hasVerifiedEmail 11 | hasCompletedOnboarding 12 | } 13 | } 14 | `; 15 | 16 | export const SNACKBAR_STATE_QUERY = gql` 17 | query snackbar { 18 | snackBarOpen @client 19 | snackMsg @client 20 | snackType @client 21 | } 22 | `; 23 | 24 | export const IS_LEFT_DRAWER_OPEN = gql` 25 | query isLeftDrawerOpen { 26 | isLeftDrawerOpen @client 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /web-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | // / 2 | // / 3 | -------------------------------------------------------------------------------- /web-app/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: (config, { isServer }) => { 3 | config.module.rules.push({ 4 | test: /\.svg$/, 5 | use: ['@svgr/webpack'], 6 | }); 7 | 8 | config.module.rules.push({ 9 | test: /\.(jpe?g|png|gif|ico|webp)$/, 10 | use: [ 11 | { 12 | loader: require.resolve('url-loader'), 13 | options: { 14 | fallback: require.resolve('file-loader'), 15 | outputPath: `${isServer ? '../' : ''}static/images/`, 16 | // limit: config.inlineImageLimit, 17 | // publicPath: `${config.assetPrefix}/_next/static/images/`, 18 | // esModule: config.esModule || false, 19 | name: '[name]-[hash].[ext]', 20 | }, 21 | }, 22 | ], 23 | }); 24 | 25 | // Fixes npm packages that depend on `fs` module 26 | // https://github.com/webpack-contrib/css-loader/issues/447 27 | // https://github.com/zeit/next.js/issues/7755 28 | if (!isServer) { 29 | config.node = { 30 | fs: 'empty', 31 | }; 32 | } 33 | 34 | return config; 35 | }, 36 | // This makes the environment variables available at runtime 37 | // Make sure to set the environment variables in now.json in order to have the environment variables in a deployed environment 38 | env: { 39 | COMMON_BACKEND_URL: process.env.COMMON_BACKEND_URL, 40 | COMMON_FRONTEND_URL: process.env.COMMON_FRONTEND_URL, 41 | COMMON_STRIPE_YEARLY_PLAN_ID: process.env.COMMON_STRIPE_YEARLY_PLAN_ID, 42 | COMMON_STRIPE_MONTHLY_PLAN_ID: process.env.COMMON_STRIPE_MONTHLY_PLAN_ID, 43 | WEB_APP_STRIPE_PUBLISHABLE_KEY: process.env.WEB_APP_STRIPE_PUBLISHABLE_KEY, 44 | WEB_APP_GOOGLE_API_KEY: process.env.WEB_APP_GOOGLE_API_KEY, 45 | WEB_APP_MARKETING_SITE: process.env.WEB_APP_MARKETING_SITE, 46 | WEB_APP_SENTRY_DSN: process.env.WEB_APP_SENTRY_DSN, 47 | IMAGE_UPLOAD_URL: process.env.IMAGE_UPLOAD_URL, 48 | IMAGE_UPLOAD_PRESET: process.env.IMAGE_UPLOAD_PRESET, 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /web-app/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Html, Head, Main, NextScript } from 'next/document'; 3 | import * as Sentry from '@sentry/browser'; 4 | import { ServerStyleSheets } from '@material-ui/core/styles'; 5 | import theme from '../theme'; 6 | 7 | process.on('unhandledRejection', (err) => { 8 | Sentry.captureException(err); 9 | }); 10 | 11 | process.on('uncaughtException', (err) => { 12 | Sentry.captureException(err); 13 | }); 14 | 15 | export default class MyDocument extends Document { 16 | render(): JSX.Element { 17 | return ( 18 | 19 | 20 | {/* PWA primary color */} 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | {/*