├── .env.example ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── schemas │ ├── ActionsYAML.schema.json │ ├── AllowlistYAML.schema.json │ ├── CronTriggerYAML.schema.json │ ├── FunctionsYAML.schema.json │ ├── HasuraMetadataV2.schema.json │ ├── MetadataExport.schema.json │ ├── RemoteSchemasYAML.schema.json │ ├── SingleTableYAML.schema.json │ └── TablesYAML.schema.json └── settings.json ├── Architecture.md ├── Contributing.md ├── README.md ├── Setup.md ├── docker-compose.yaml ├── hasura-ecommerce-brief-tour.mp4 ├── hasura ├── config.yaml ├── metadata │ ├── actions.graphql │ ├── actions.yaml │ ├── allow_list.yaml │ ├── cron_triggers.yaml │ ├── databases │ │ ├── databases.yaml │ │ └── default │ │ │ └── tables │ │ │ ├── public_address.yaml │ │ │ ├── public_order.yaml │ │ │ ├── public_order_product.yaml │ │ │ ├── public_order_status.yaml │ │ │ ├── public_product.yaml │ │ │ ├── public_product_category_enum.yaml │ │ │ ├── public_product_review.yaml │ │ │ ├── public_site_admin.yaml │ │ │ ├── public_user.yaml │ │ │ └── tables.yaml │ ├── query_collections.yaml │ ├── remote_schemas.yaml │ ├── rest_endpoints.yaml │ └── version.yaml ├── migrations │ └── default │ │ └── 1646834482402_init │ │ └── up.sql └── seeds │ └── default │ ├── 01_A_user_seeds.sql │ ├── 01_B_user_address_seeds.sql │ ├── 02_product_seeds_sanitized.sql │ ├── 03_A_order_seeds.sql │ ├── 03_B_order_product_seeds.sql │ └── 04_default_user_login_seeds.sql ├── seed-data-generator ├── .gitignore ├── package.json ├── readme.md ├── src │ ├── graphql-client-sdk-node.ts │ └── main.ts ├── tsconfig.json └── yarn.lock ├── sketch └── html │ ├── account.html │ ├── admin-customers.html │ ├── admin-orders.html │ ├── admin-product-add.html │ ├── admin-products.html │ ├── admin.html │ ├── books.png │ ├── cart.html │ ├── cc-amex.svg │ ├── cc-discover.svg │ ├── cc-mastercard.svg │ ├── cc-visa.svg │ ├── checkmark.svg │ ├── checkout-success.html │ ├── checkout.html │ ├── chevron.svg │ ├── core.css │ ├── ellipses.svg │ ├── header-bg.png │ ├── home.html │ ├── index.html │ ├── kitchen.png │ ├── login.html │ ├── logo.svg │ ├── new-arrivals.png │ ├── order.html │ ├── product.html │ ├── signup.html │ └── style.css └── www ├── .dockerignore ├── .editorconfig ├── .env ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── components ├── AccountAddresses.js ├── AdminChart.js ├── AdminNewCustomers.tsx ├── AdminNewOrders.tsx ├── AdminStoreSummary.js ├── AllOffers.tsx ├── CardAddress.js ├── Cart.js ├── FilterBrands.js ├── FilterDebug.js ├── FilterOptions.js ├── FilterPrice.js ├── FormLogin.js ├── FormNewAddress.js ├── FormSignup.tsx ├── FormValidations.js ├── Header.js ├── HeaderAdmin.js ├── HeaderControls.js ├── HeaderControlsAuthed.js ├── HeaderControlsUnAuthed.js ├── LayoutAdmin.tsx ├── LayoutProduct.js ├── LayoutStore.js ├── Offer.js ├── OfferSimilar.js ├── OffersSimilar.js ├── QuantitySelect.js ├── Rating.js ├── Review.js ├── ReviewSummary.js ├── Sidebar.js └── StripeCheckoutForm.tsx ├── hooks └── useOnClickOutside.js ├── next-env.d.ts ├── next.config.js ├── next.config.types.ts ├── package.json ├── pages ├── README.md ├── _app.tsx ├── account │ ├── index.tsx │ ├── login.tsx │ └── signup.tsx ├── admin │ ├── account │ │ ├── index.tsx │ │ ├── login.tsx │ │ └── signup.tsx │ ├── add-product.tsx │ ├── customers.tsx │ ├── index.tsx │ ├── orders.tsx │ └── products.tsx ├── api │ ├── actions │ │ ├── admin-login │ │ │ ├── example-and-test-request.http │ │ │ └── index.ts │ │ ├── admin-signup │ │ │ ├── example-and-test-request.http │ │ │ └── index.ts │ │ ├── create-payment-intent │ │ │ ├── example-and-test-request.http │ │ │ └── index.ts │ │ ├── image-upload │ │ │ ├── example-and-test-request.http │ │ │ ├── index.ts │ │ │ └── stock-laptop-image.jpg │ │ ├── login │ │ │ ├── example-and-test-request.http │ │ │ └── index.ts │ │ ├── refresh-token │ │ │ ├── example-and-test-request.http │ │ │ └── index.ts │ │ └── signup │ │ │ ├── example-and-test-request.http │ │ │ └── index.ts │ ├── healthz.ts │ ├── logout.ts │ └── webhooks │ │ └── stripe.ts ├── category │ └── [category].js ├── checkout.tsx ├── index.tsx └── products │ └── [product].tsx ├── public ├── checkmark.svg ├── chevron.svg ├── ellipses.svg ├── favicon.ico ├── logo.svg └── vercel.svg ├── state ├── AccountState.js ├── BrandState.js ├── CartState.js ├── CategoryState.js ├── FilterState.js ├── MenuState.js ├── OfferState.js ├── README.md └── persistence.js ├── store ├── index.ts └── model.ts ├── styles ├── Home.module.css ├── card-section.module.css ├── core.css ├── globals.css ├── quantity-select.module.css └── style.css ├── tsconfig.json ├── utils ├── FluidGraphQL.md ├── README-MINIO.md ├── access-and-refresh-tokens.ts ├── apollo-client.ts ├── auth │ └── jwt.ts ├── constants.ts ├── cookies │ └── index.ts ├── generated │ ├── client.ts │ └── graphql-client-sdk.ts ├── gql-zeus-query-hooks.ts ├── react-make-state-handler.ts ├── s3-client.ts └── stripe-client-serverside.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | STRIPE_SECRET_KEY=sk_test_ 2 | STRIPE_PUBLISHABLE_KEY=pk_test_ 3 | STRIPE_WEBHOOK_SECRET=whsec_ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Ulysses-* 2 | .New Filter.Ulysses -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["redhat.vscode-yaml", "bradymholt.pgformatter"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/schemas/ActionsYAML.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "actions": { 5 | "type": "array", 6 | "items": { 7 | "$ref": "./HasuraMetadataV2.schema.json#definitions/Action" 8 | } 9 | }, 10 | "custom_types": { 11 | "type": "object", 12 | "$ref": "./HasuraMetadataV2.schema.json#definitions/CustomTypes" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/schemas/AllowlistYAML.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": { 4 | "$ref": "./HasuraMetadataV2.schema.json#definitions/AllowList" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/schemas/CronTriggerYAML.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": { 4 | "$ref": "./HasuraMetadataV2.schema.json#definitions/CronTrigger" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/schemas/FunctionsYAML.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": { 4 | "$ref": "./HasuraMetadataV2.schema.json#definitions/Function" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/schemas/MetadataExport.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "$ref": "./HasuraMetadataV2.schema.json#definitions/HasuraMetadataV2" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schemas/RemoteSchemasYAML.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": { 4 | "$ref": "./HasuraMetadataV2.schema.json#definitions/RemoteSchema" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/schemas/SingleTableYAML.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "$ref": "./HasuraMetadataV2.schema.json#definitions/TableEntry" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schemas/TablesYAML.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "array", 3 | "items": { 4 | "$ref": "./HasuraMetadataV2.schema.json#definitions/TableEntry" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "json.schemas": [ 3 | { 4 | "fileMatch": ["**/metadata.json"], 5 | "url": "./.vscode/schemas/MetadataExport.schema.json" 6 | } 7 | ], 8 | "yaml.schemas": { 9 | "./.vscode/schemas/ActionsYAML.schema.json": "**/actions.yaml", 10 | "./.vscode/schemas/AllowListYAML.schema.json": "**/allow_list.yaml", 11 | "./.vscode/schemas/CronTriggerYAML.schema.json": "**/cron_triggers.yaml", 12 | "./.vscode/schemas/FunctionsYAML.schema.json": "**/functions.yaml", 13 | "./.vscode/schemas/QueryCollectionsYAML.schema.json": "**/query_collections.yaml", 14 | "./.vscode/schemas/RemoteSchemasYAML.schema.json": "**/remote_schemas.yaml", 15 | "./.vscode/schemas/TablesYAML.schema.json": "**/tables.yaml", 16 | "./.vscode/schemas/SingleTableYAML.schema.json": "**/table.yaml" 17 | }, 18 | "pgFormatter.commaBreak": true, 19 | "pgFormatter.wrapLimit": 80 20 | } 21 | -------------------------------------------------------------------------------- /Architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | The project is composed in two primary components. The Hasura backend and the the web client (`www`). Under the hood, Docker will create a postgres and minio instance to support the client and the backend. For information on uploading files, see the information on uploading files with Hasura, see the [Minio guide](www/utils/README-MINIO.md) in the utils directory of the web client. 3 | 4 | The Hasura instance is configured to auto apply migration data upon init. This provides a smooth developer experience to track any changes to the Hasura project and allows you to check the metadata into version control. 5 | 6 | You can scaffold new tables and models as well using a Yaml schema definition and then combine them through the use of some clever make scripts. Documentation on that flow [can be found here.](hasura/models/README.md) 7 | 8 | The high-level architecture uses Hasura as a persistent store for users, products, and orders. We use the client's primitives to store view states that we then use to query the content with subscriptions directly from Hasura. It would be trivial to support persisting carts as well, but for the purpose of this demo, we've left that functionality out. 9 | 10 | ## The Hasura flow follows roughly three parts: 11 | 1. Metadata describes the project you're creating in respect to permissions, functions, configurations, tables and the rest. 12 | 2. Models are broken into sub directories here by convetion and used to describe the the shape (`sql` files) and configuration (`yaml` file) for each model you want to create. These are then compiled by `make` commands into a composite table definition that is added to the migration folder as individual tables. 13 | 3. Migrations reflect any real-time edits performed in the console you initiated with the `hasura console` command. 14 | 15 | ## The NextJs Flow 16 | Explaining the details of NextJs is out of the scope of this demo. You can replace this client with any framework of your choosing, the Hasura migration pattern is a powerful primitive on its own that is agnostic of the consuming technology. For a quick primer on NextJs, it's a "config included" framework for React that supports the best of static site generation, server-side-rendered content, and protected execeution environments. More information can be read in the [pages guide.](www/pages/README.md) -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project has a very wide scope, with various portions being deeper than others. If you find an area you'd like to contribute to improve the readability, best-practices, or idiomatic patterns of either Postgres or React, create a pull request simply explaining in story format what you are wanting to change and why. 4 | 5 | We love community engagement! -------------------------------------------------------------------------------- /Setup.md: -------------------------------------------------------------------------------- 1 | # Hasura E-Commerce Demo 2 | 3 | Running this demo requires very very dependencies. 4 | 5 | 1. Docker and Docker Compose installed. [Install instructions.](https://docs.docker.com/get-docker/) 6 | 2. Stripe Secret and Publishable keys. _The account doesn't need to be verified as no transactions will be happening._ [Instructions here.](https://stripe.com/docs/keys) 7 | 3. Hasura CLI installed. [Instructions here.](https://hasura.io/docs/latest/graphql/core/hasura-cli/install-hasura-cli.html) 8 | 9 | ## Running the Project 10 | 11 | ```sh-session 12 | $ git clone https://github.com/hasura/hasura-ecommerce 13 | 14 | $ docker-compose up -d 15 | $ cd hasura 16 | 17 | $ hasura seeds apply 18 | 19 | Visit http://localhost:3000 for Next.js frontend 20 | Visit http://localhost:8080 for Hasura console (admin secret = "my-secret") 21 | Visit http://localhost:9000 for Minio dashboard (login = "minio:minio123") 22 | ``` 23 | 24 | 1. Clone the repo to your local machine. 25 | 2. Navigate inside the top level directory. 26 | 3. (Optional) In the `.env.example` file, replace `` and `` and ` with your stripe keys. 27 | 4. Run the project with `docker-compose up -d` - the `-d` tells docker to run in detached mode, which will not put out any console information. If you'd like to see what's happening, omit the `-d` and it will run in the forefront. 28 | 5. After confirming you can view the website at `localhost:3000` - navigate into the `hasura` directory. Run the command `hasura seeds apply` - this will populate product and user data into your hasura instance. 29 | 6. To play with the hasura console, run `hasura console` which will open a hasura console for you to operate. 30 | 31 | For more details, see the [architecture guide.](Architecture.md) 32 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | 3 | services: 4 | postgres: 5 | image: postgres:13 6 | restart: always 7 | volumes: 8 | - hasura_ci_db_data:/var/lib/postgresql/data 9 | ports: 10 | # Expose the port for tooling (SQL language server in IDE, connecting with GUI's etc) 11 | - 5432:5432 12 | environment: 13 | POSTGRES_PASSWORD: postgrespassword 14 | 15 | graphql-engine: 16 | image: hasura/graphql-engine:v2.3.1.cli-migrations-v3 17 | volumes: 18 | - ./hasura/migrations:/hasura-migrations 19 | - ./hasura/metadata:/hasura-metadata 20 | ports: 21 | - 8080:8080 22 | depends_on: 23 | - "postgres" 24 | restart: always 25 | environment: 26 | NEXTJS_SERVER_URL: http://nextjs:3000 27 | HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres 28 | PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres 29 | ## enable the console served by server 30 | HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console 31 | ## enable debugging mode. It is recommended to disable this in production 32 | HASURA_GRAPHQL_DEV_MODE: "true" 33 | HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log 34 | HASURA_GRAPHQL_ADMIN_SECRET: my-secret 35 | HASURA_GRAPHQL_JWT_SECRET: '{ "type": "HS256", "key": "this-is-a-generic-HS256-secret-key-and-you-should-really-change-it" }' 36 | HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous 37 | 38 | nextjs: 39 | build: 40 | context: ./www 41 | dockerfile: Dockerfile 42 | restart: always 43 | volumes: 44 | - ./www:/app/ 45 | - /app/node_modules 46 | - /app/.next 47 | ports: 48 | - 3000:3000 49 | environment: 50 | NEXT_PUBLIC_HASURA_URL_SERVERSIDE: http://graphql-engine:8080 51 | NEXT_PUBLIC_HASURA_URL_CLIENTSIDE: http://localhost:8080 52 | HASURA_ADMIN_SECRET: my-secret 53 | HASURA_JWT_SECRET_TYPE: HS256 54 | HASURA_JWT_SECRET_KEY: this-is-a-generic-HS256-secret-key-and-you-should-really-change-it 55 | S3_SERVER_URL: http://minio:9000 56 | S3_ACCESS_KEY: minio 57 | S3_SECRET_ACCESS_KEY: minio123 58 | # Bucket name where all images should be uploaded and stored 59 | # [NOTE] The following rules apply for naming S3 buckets: 60 | # Bucket names must be between 3 and 63 characters long. 61 | # Bucket names can consist only of lowercase letters, numbers, dots (.), and hyphens (-). 62 | # Bucket names must begin and end with a letter or number. 63 | S3_BUCKET_NAME: site-images 64 | STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} 65 | STRIPE_PUBLISHABLE_KEY: ${STRIPE_PUBLISHABLE_KEY} 66 | STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET} 67 | 68 | # MinIO is an OSS self-hosted S3 compatible object storage platform (See: https://github.com/minio/minio) 69 | # It works with all standard S3 client libraries, and comes with an admin GUI to manage the stored files 70 | # 71 | # You can access the admin panel at http://localhost:9000/minio/login after starting the service 72 | # 73 | # (The "Access Key" is MINIO_ROOT_USER, and "Secret Key" is MINIO_ROOT_PASSWORD) 74 | # (So you can login with "minio:minio123" as credentials) 75 | # 76 | # It is used to generate presigned image upload/download urls for the frontend (this is done via Hasura Action REST API handlers in Next.js) 77 | minio: 78 | image: minio/minio:RELEASE.2021-06-17T00-10-46Z 79 | volumes: 80 | - minio_data:/data 81 | ports: 82 | - 9000:9000 83 | environment: 84 | MINIO_ROOT_USER: minio 85 | MINIO_ROOT_PASSWORD: minio123 86 | command: server /data 87 | healthcheck: 88 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 89 | interval: 30s 90 | timeout: 20s 91 | retries: 3 92 | 93 | volumes: 94 | minio_data: 95 | hasura_ci_db_data: 96 | -------------------------------------------------------------------------------- /hasura-ecommerce-brief-tour.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/hasura-ecommerce/1090f8a9f2b16729516c86861a5647352b2f6314/hasura-ecommerce-brief-tour.mp4 -------------------------------------------------------------------------------- /hasura/config.yaml: -------------------------------------------------------------------------------- 1 | version: 3 2 | endpoint: http://localhost:8080 3 | admin_secret: my-secret 4 | metadata_directory: metadata 5 | actions: 6 | kind: synchronous 7 | handler_webhook_baseurl: http://localhost:3000 8 | -------------------------------------------------------------------------------- /hasura/metadata/actions.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | adminLogin( 3 | params: AdminLoginInput! 4 | ): JWT 5 | } 6 | 7 | type Mutation { 8 | adminSignup( 9 | params: AdminSignupInput! 10 | ): JWT 11 | } 12 | 13 | type Mutation { 14 | createPaymentIntent( 15 | params: CreatePaymentIntentInput! 16 | ): PaymentIntentClientSecret 17 | } 18 | 19 | type Mutation { 20 | login( 21 | params: LoginInput! 22 | ): JWT 23 | } 24 | 25 | type Query { 26 | refreshToken( 27 | params: RefreshTokenInput! 28 | ): RefreshTokenJWT 29 | } 30 | 31 | type Mutation { 32 | signup( 33 | params: SignupInput! 34 | ): JWT 35 | } 36 | 37 | input SignupInput { 38 | name: String! 39 | email: String! 40 | password: String! 41 | } 42 | 43 | input LoginInput { 44 | email: String! 45 | password: String! 46 | } 47 | 48 | input AdminLoginInput { 49 | email: String! 50 | password: String! 51 | } 52 | 53 | input AdminSignupInput { 54 | name: String! 55 | email: String! 56 | password: String! 57 | } 58 | 59 | input CreatePaymentIntentInput { 60 | paymentAmount: Float! 61 | } 62 | 63 | input RefreshTokenInput { 64 | refreshToken: String! 65 | } 66 | 67 | type PaymentIntentClientSecret { 68 | clientSecret: String! 69 | } 70 | 71 | type JWT { 72 | name: String! 73 | email: String! 74 | token: String! 75 | refreshToken: String! 76 | } 77 | 78 | type RefreshTokenJWT { 79 | token: String! 80 | } 81 | 82 | -------------------------------------------------------------------------------- /hasura/metadata/actions.yaml: -------------------------------------------------------------------------------- 1 | actions: 2 | - name: adminLogin 3 | definition: 4 | kind: "" 5 | handler: "{{NEXTJS_SERVER_URL}}/api/actions/admin-login" 6 | permissions: 7 | - role: anonymous 8 | - name: adminSignup 9 | definition: 10 | kind: synchronous 11 | handler: "{{NEXTJS_SERVER_URL}}/api/actions/admin-signup" 12 | permissions: 13 | - role: site-admin 14 | - name: createPaymentIntent 15 | definition: 16 | kind: synchronous 17 | handler: "{{NEXTJS_SERVER_URL}}/api/actions/create-payment-intent" 18 | permissions: 19 | - role: anonymous 20 | - role: site-admin 21 | - role: user 22 | - name: login 23 | definition: 24 | kind: synchronous 25 | handler: "{{NEXTJS_SERVER_URL}}/api/actions/login" 26 | permissions: 27 | - role: anonymous 28 | - name: refreshToken 29 | definition: 30 | kind: "" 31 | handler: "{{NEXTJS_SERVER_URL}}/api/actions/refresh-token" 32 | permissions: 33 | - role: anonymous 34 | - role: site-admin 35 | - role: user 36 | - name: signup 37 | definition: 38 | kind: synchronous 39 | handler: "{{NEXTJS_SERVER_URL}}/api/actions/signup" 40 | permissions: 41 | - role: anonymous 42 | custom_types: 43 | enums: [] 44 | input_objects: 45 | - name: SignupInput 46 | - name: LoginInput 47 | - name: AdminLoginInput 48 | - name: AdminSignupInput 49 | - name: CreatePaymentIntentInput 50 | - name: RefreshTokenInput 51 | objects: 52 | - name: PaymentIntentClientSecret 53 | - name: JWT 54 | - name: RefreshTokenJWT 55 | scalars: [] 56 | -------------------------------------------------------------------------------- /hasura/metadata/allow_list.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/cron_triggers.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/databases/databases.yaml: -------------------------------------------------------------------------------- 1 | - name: default 2 | kind: postgres 3 | configuration: 4 | connection_info: 5 | database_url: 6 | from_env: PG_DATABASE_URL 7 | isolation_level: read-committed 8 | use_prepared_statements: false 9 | tables: "!include default/tables/tables.yaml" 10 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_address.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: address 3 | schema: public 4 | object_relationships: 5 | - name: user 6 | using: 7 | foreign_key_constraint_on: user_id 8 | array_relationships: 9 | - name: orders_with_billing_address 10 | using: 11 | foreign_key_constraint_on: 12 | column: billing_address_id 13 | table: 14 | name: order 15 | schema: public 16 | - name: orders_with_shipping_address 17 | using: 18 | foreign_key_constraint_on: 19 | column: shipping_address_id 20 | table: 21 | name: order 22 | schema: public 23 | insert_permissions: 24 | - permission: 25 | check: {} 26 | columns: "*" 27 | role: site-admin 28 | - permission: 29 | check: 30 | user_id: 31 | _eq: X-Hasura-User-Id 32 | columns: "*" 33 | role: user 34 | select_permissions: 35 | - permission: 36 | columns: "*" 37 | filter: {} 38 | role: site-admin 39 | - permission: 40 | columns: "*" 41 | filter: 42 | user_id: 43 | _eq: X-Hasura-User-Id 44 | role: user 45 | update_permissions: 46 | - permission: 47 | check: null 48 | columns: "*" 49 | filter: {} 50 | role: site-admin 51 | - permission: 52 | check: null 53 | columns: "*" 54 | filter: 55 | user_id: 56 | _eq: X-Hasura-User-Id 57 | role: user 58 | delete_permissions: 59 | - permission: 60 | filter: {} 61 | role: site-admin 62 | - permission: 63 | filter: 64 | user_id: 65 | _eq: X-Hasura-User-Id 66 | role: user 67 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_order.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: order 3 | schema: public 4 | object_relationships: 5 | - name: billing_address 6 | using: 7 | foreign_key_constraint_on: billing_address_id 8 | - name: order_status 9 | using: 10 | foreign_key_constraint_on: status 11 | - name: shipping_address 12 | using: 13 | foreign_key_constraint_on: shipping_address_id 14 | - name: user 15 | using: 16 | foreign_key_constraint_on: user_id 17 | array_relationships: 18 | - name: products 19 | using: 20 | foreign_key_constraint_on: 21 | column: order_id 22 | table: 23 | name: order_product 24 | schema: public 25 | insert_permissions: 26 | - permission: 27 | check: {} 28 | columns: "*" 29 | role: site-admin 30 | - permission: 31 | check: 32 | user_id: 33 | _eq: X-Hasura-User-Id 34 | columns: "*" 35 | role: user 36 | select_permissions: 37 | - permission: 38 | columns: "*" 39 | filter: {} 40 | role: site-admin 41 | - permission: 42 | columns: "*" 43 | filter: 44 | user_id: 45 | _eq: X-Hasura-User-Id 46 | role: user 47 | update_permissions: 48 | - permission: 49 | check: null 50 | columns: "*" 51 | filter: {} 52 | role: site-admin 53 | delete_permissions: 54 | - permission: 55 | filter: {} 56 | role: site-admin 57 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_order_product.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: order_product 3 | schema: public 4 | object_relationships: 5 | - name: order 6 | using: 7 | foreign_key_constraint_on: order_id 8 | - name: product 9 | using: 10 | foreign_key_constraint_on: product_id 11 | insert_permissions: 12 | - permission: 13 | check: {} 14 | columns: "*" 15 | role: site-admin 16 | - permission: 17 | check: 18 | order: 19 | user_id: 20 | _eq: X-Hasura-User-Id 21 | columns: "*" 22 | role: user 23 | select_permissions: 24 | - permission: 25 | columns: "*" 26 | filter: {} 27 | role: site-admin 28 | - permission: 29 | columns: "*" 30 | filter: 31 | order: 32 | user_id: 33 | _eq: X-Hasura-User-Id 34 | role: user 35 | update_permissions: 36 | - permission: 37 | check: null 38 | columns: "*" 39 | filter: {} 40 | role: site-admin 41 | delete_permissions: 42 | - permission: 43 | filter: {} 44 | role: site-admin 45 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_order_status.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: order_status 3 | schema: public 4 | is_enum: true 5 | array_relationships: 6 | - name: orders 7 | using: 8 | foreign_key_constraint_on: 9 | column: status 10 | table: 11 | name: order 12 | schema: public 13 | insert_permissions: 14 | - permission: 15 | check: {} 16 | columns: "*" 17 | role: site-admin 18 | select_permissions: 19 | - permission: 20 | columns: "*" 21 | filter: {} 22 | role: site-admin 23 | - permission: 24 | columns: "*" 25 | filter: {} 26 | role: user 27 | update_permissions: 28 | - permission: 29 | check: null 30 | columns: "*" 31 | filter: {} 32 | role: site-admin 33 | delete_permissions: 34 | - permission: 35 | filter: {} 36 | role: site-admin 37 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_product.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: product 3 | schema: public 4 | object_relationships: 5 | - name: category 6 | using: 7 | foreign_key_constraint_on: category_display_name 8 | array_relationships: 9 | - name: orders 10 | using: 11 | foreign_key_constraint_on: 12 | column: product_id 13 | table: 14 | name: order_product 15 | schema: public 16 | - name: product_reviews 17 | using: 18 | foreign_key_constraint_on: 19 | column: product_id 20 | table: 21 | name: product_review 22 | schema: public 23 | insert_permissions: 24 | - permission: 25 | check: {} 26 | columns: "*" 27 | role: site-admin 28 | select_permissions: 29 | - permission: 30 | columns: "*" 31 | filter: {} 32 | role: anonymous 33 | - permission: 34 | columns: "*" 35 | filter: {} 36 | role: site-admin 37 | - permission: 38 | columns: "*" 39 | filter: {} 40 | role: user 41 | update_permissions: 42 | - permission: 43 | check: null 44 | columns: "*" 45 | filter: {} 46 | role: site-admin 47 | delete_permissions: 48 | - permission: 49 | filter: {} 50 | role: site-admin 51 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_product_category_enum.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: product_category_enum 3 | schema: public 4 | is_enum: true 5 | array_relationships: 6 | - name: products 7 | using: 8 | foreign_key_constraint_on: 9 | column: category_display_name 10 | table: 11 | name: product 12 | schema: public 13 | insert_permissions: 14 | - permission: 15 | check: {} 16 | columns: "*" 17 | role: site-admin 18 | select_permissions: 19 | - permission: 20 | columns: "*" 21 | filter: {} 22 | role: anonymous 23 | - permission: 24 | columns: "*" 25 | filter: {} 26 | role: site-admin 27 | - permission: 28 | columns: "*" 29 | filter: {} 30 | role: user 31 | update_permissions: 32 | - permission: 33 | check: null 34 | columns: "*" 35 | filter: {} 36 | role: site-admin 37 | delete_permissions: 38 | - permission: 39 | filter: {} 40 | role: site-admin 41 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_product_review.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: product_review 3 | schema: public 4 | object_relationships: 5 | - name: product 6 | using: 7 | foreign_key_constraint_on: product_id 8 | - name: user 9 | using: 10 | foreign_key_constraint_on: user_id 11 | insert_permissions: 12 | - permission: 13 | check: {} 14 | columns: "*" 15 | role: site-admin 16 | - permission: 17 | check: 18 | user: 19 | id: 20 | _eq: X-Hasura-User-Id 21 | orders: 22 | products: 23 | id: 24 | _ceq: product_id 25 | columns: 26 | - product_id 27 | - rating 28 | - comment 29 | set: 30 | user_id: X-Hasura-User-Id 31 | role: user 32 | select_permissions: 33 | - permission: 34 | columns: "*" 35 | filter: {} 36 | role: anonymous 37 | - permission: 38 | columns: "*" 39 | filter: {} 40 | role: site-admin 41 | - permission: 42 | columns: "*" 43 | filter: {} 44 | role: user 45 | update_permissions: 46 | - permission: 47 | check: null 48 | columns: "*" 49 | filter: {} 50 | role: site-admin 51 | - permission: 52 | check: null 53 | columns: 54 | - product_id 55 | - rating 56 | - comment 57 | filter: 58 | user: 59 | id: 60 | _eq: X-Hasura-User-Id 61 | orders: 62 | products: 63 | id: 64 | _ceq: product_id 65 | set: 66 | user_id: X-Hasura-User-Id 67 | role: user 68 | delete_permissions: 69 | - permission: 70 | filter: {} 71 | role: site-admin 72 | - permission: 73 | filter: 74 | user: 75 | id: 76 | _eq: X-Hasura-User-Id 77 | orders: 78 | products: 79 | id: 80 | _ceq: product_id 81 | role: user 82 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_site_admin.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: site_admin 3 | schema: public 4 | insert_permissions: 5 | - permission: 6 | check: {} 7 | columns: "*" 8 | role: site-admin 9 | select_permissions: 10 | - permission: 11 | columns: "*" 12 | filter: {} 13 | role: site-admin 14 | update_permissions: 15 | - permission: 16 | check: null 17 | columns: "*" 18 | filter: {} 19 | role: site-admin 20 | delete_permissions: 21 | - permission: 22 | filter: {} 23 | role: site-admin 24 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/public_user.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | name: user 3 | schema: public 4 | array_relationships: 5 | - name: addresses 6 | using: 7 | foreign_key_constraint_on: 8 | column: user_id 9 | table: 10 | name: address 11 | schema: public 12 | - name: orders 13 | using: 14 | foreign_key_constraint_on: 15 | column: user_id 16 | table: 17 | name: order 18 | schema: public 19 | - name: product_reviews 20 | using: 21 | foreign_key_constraint_on: 22 | column: user_id 23 | table: 24 | name: product_review 25 | schema: public 26 | insert_permissions: 27 | - permission: 28 | check: {} 29 | columns: "*" 30 | role: site-admin 31 | select_permissions: 32 | - permission: 33 | columns: "*" 34 | filter: {} 35 | role: site-admin 36 | - permission: 37 | columns: "*" 38 | filter: 39 | id: 40 | _eq: X-Hasura-User-Id 41 | role: user 42 | update_permissions: 43 | - permission: 44 | check: null 45 | columns: "*" 46 | filter: {} 47 | role: site-admin 48 | delete_permissions: 49 | - permission: 50 | filter: {} 51 | role: site-admin 52 | -------------------------------------------------------------------------------- /hasura/metadata/databases/default/tables/tables.yaml: -------------------------------------------------------------------------------- 1 | - "!include public_address.yaml" 2 | - "!include public_order.yaml" 3 | - "!include public_order_product.yaml" 4 | - "!include public_order_status.yaml" 5 | - "!include public_product.yaml" 6 | - "!include public_product_category_enum.yaml" 7 | - "!include public_product_review.yaml" 8 | - "!include public_site_admin.yaml" 9 | - "!include public_user.yaml" 10 | -------------------------------------------------------------------------------- /hasura/metadata/query_collections.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/remote_schemas.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/rest_endpoints.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /hasura/metadata/version.yaml: -------------------------------------------------------------------------------- 1 | version: 3 2 | -------------------------------------------------------------------------------- /hasura/seeds/default/04_default_user_login_seeds.sql: -------------------------------------------------------------------------------- 1 | -- "password" bcrypted = $2y$10$4fLjiqiJ.Rh0F/qYfIfCeOy7a9nLJN49YzUEJbYnj2ZsiwVhGsOxK 2 | INSERT INTO public.user 3 | (name, email, password) 4 | VALUES 5 | ('Person', 'user@site.com', '$2y$10$4fLjiqiJ.Rh0F/qYfIfCeOy7a9nLJN49YzUEJbYnj2ZsiwVhGsOxK'); 6 | 7 | INSERT INTO public.site_admin 8 | (name, email, password) 9 | VALUES 10 | ('Admin', 'admin@site.com', '$2y$10$4fLjiqiJ.Rh0F/qYfIfCeOy7a9nLJN49YzUEJbYnj2ZsiwVhGsOxK'); 11 | -------------------------------------------------------------------------------- /seed-data-generator/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /seed-data-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seed-data-generator", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "ts-node ./src/main.ts", 8 | "generate-gql-client": "zeus http://localhost:8080/v1/graphql ./src --ts --node --header 'X-Hasura-Admin-Secret:my-secret' --output graphql-client-sdk-node" 9 | }, 10 | "dependencies": { 11 | "faker": "^5.4.0", 12 | "graphql-zeus": "^2.8.6", 13 | "node-fetch": "^2.6.1" 14 | }, 15 | "devDependencies": { 16 | "@types/faker": "^5.1.7", 17 | "@types/node": "^14.14.31", 18 | "@types/node-fetch": "^2.5.8", 19 | "ts-node": "^9.1.1", 20 | "typescript": "^4.1.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /seed-data-generator/readme.md: -------------------------------------------------------------------------------- 1 | This Node script can generate seed data for the tables in the database 2 | 3 | It was written because we had a database of users, addresses, products, etc from public domain, but we needed to create orders with products in them for users. 4 | This is a little complicated, so this script generates valid relational seed data records for this purpose and is configurable. 5 | 6 | Probably it would have been better and easier to use *Synth*, but we weren't aware of it then =( 7 | - https://github.com/getsynth/synth 8 | -------------------------------------------------------------------------------- /seed-data-generator/src/main.ts: -------------------------------------------------------------------------------- 1 | import faker from "faker"; 2 | 3 | import { Chain, order_insert_input, user } from "./graphql-client-sdk-node"; 4 | import type { 5 | product, 6 | order_product_insert_input, 7 | } from "./graphql-client-sdk-node"; 8 | 9 | // TODO: This script should probably just be able to generate data for all of the core tables 10 | 11 | /////////////////////////////////////////////////////////// 12 | 13 | const NUM_ORDERS_PER_USER = 5; 14 | const NUM_PRODUCTS_PER_ORDER_MIN = 1; 15 | const NUM_PRODUCTS_PER_ORDER_MAX = 5; 16 | 17 | const client = Chain("http://localhost:8080/v1/graphql", { 18 | headers: { "X-Hasura-Admin-Secret": "my-secret" }, 19 | }); 20 | 21 | /////////////////////////////////////////////////////////// 22 | 23 | async function main() { 24 | const productsQuery = await client.query({ 25 | product: [{}, { id: true }], 26 | }); 27 | 28 | const userQuery = await client.query({ 29 | user: [ 30 | {}, 31 | { 32 | id: true, 33 | addresses: [{}, { id: true }], 34 | }, 35 | ], 36 | }); 37 | 38 | const orderData = userQuery.user.flatMap((user) => { 39 | return generateOrdersForUser({ 40 | user: user as any, 41 | amount: NUM_ORDERS_PER_USER, 42 | products: productsQuery.product, 43 | }); 44 | }); 45 | 46 | // Batch it into inserts of 100 47 | for (const orderDataBatch of chunk(orderData, 100)) { 48 | console.count("Inserting 100 orders"); 49 | await client.mutation({ 50 | insert_order: [{ objects: orderDataBatch }, { affected_rows: true }], 51 | }); 52 | } 53 | } 54 | 55 | main().catch(console.log); 56 | 57 | /////////////////////////////////////////////////////////// 58 | 59 | const chunk = (array, size) => 60 | Array.from({ length: Math.ceil(array.length / size) }, (_, index) => 61 | array.slice(index * size, index * size + size) 62 | ); 63 | 64 | function generateOrderProductData({ 65 | amount, 66 | products, 67 | }: { 68 | amount: number; 69 | products: Pick[]; 70 | }): order_product_insert_input[] { 71 | return Array.from({ length: amount }, () => ({ 72 | product_id: faker.random.arrayElement(products.map((it) => it.id)), 73 | quantity: faker.random.number(5), 74 | })); 75 | } 76 | 77 | function generateOrdersForUser({ 78 | amount, 79 | user, 80 | products, 81 | }: { 82 | amount: number; 83 | user: Pick; 84 | products: Pick[]; 85 | }): order_insert_input[] { 86 | return Array.from({ length: amount }, () => ({ 87 | user_id: user.id, 88 | billing_address_id: faker.random.arrayElement( 89 | user.addresses.map((it) => it.id) 90 | ), 91 | shipping_address_id: faker.random.arrayElement( 92 | user.addresses.map((it) => it.id) 93 | ), 94 | products: { 95 | data: generateOrderProductData({ 96 | products, 97 | amount: faker.random.number({ 98 | min: NUM_PRODUCTS_PER_ORDER_MIN, 99 | max: NUM_PRODUCTS_PER_ORDER_MAX, 100 | }), 101 | }), 102 | }, 103 | })); 104 | } 105 | -------------------------------------------------------------------------------- /sketch/html/admin-product-add.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 |
21 | Summary 22 | Customers 23 | Orders 24 | Products 25 | Back to Store 26 |
27 | 28 | 29 |
30 |

Add Product

31 |
32 |
33 |
34 | 35 |
36 |
37 |

Product Image

38 |
41 |
42 |
43 |

Product Name

44 | 45 | 46 |

Description

47 | 48 | 49 |
50 |
51 |

Price

52 |
53 | $ 54 | 55 |
56 |
57 |
58 |

Sale Price

59 |
60 | $ 61 | 62 |
63 |
64 |
65 |
66 |
67 | 68 |
69 | 70 | 71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 | 80 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /sketch/html/books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/hasura-ecommerce/1090f8a9f2b16729516c86861a5647352b2f6314/sketch/html/books.png -------------------------------------------------------------------------------- /sketch/html/cc-amex.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sketch/html/cc-discover.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sketch/html/cc-visa.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sketch/html/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | checkmark 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sketch/html/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | chevron-down-outline 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sketch/html/ellipses.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | ellipses 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sketch/html/header-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/hasura-ecommerce/1090f8a9f2b16729516c86861a5647352b2f6314/sketch/html/header-bg.png -------------------------------------------------------------------------------- /sketch/html/kitchen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/hasura-ecommerce/1090f8a9f2b16729516c86861a5647352b2f6314/sketch/html/kitchen.png -------------------------------------------------------------------------------- /sketch/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 | Account 31 | Orders 32 | Cart 33 |
34 |
35 | 36 | 52 | 53 | 54 |
55 |
56 |
57 |
58 |
59 |

Login

60 | 61 |
62 |

Username

63 | 64 | 65 |

Password

66 | 67 | 68 | 69 | 70 |
71 |
72 |
73 |
74 |
75 | 76 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /sketch/html/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sketch/html/new-arrivals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/hasura-ecommerce/1090f8a9f2b16729516c86861a5647352b2f6314/sketch/html/new-arrivals.png -------------------------------------------------------------------------------- /www/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules -------------------------------------------------------------------------------- /www/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | tab_width = 2 3 | -------------------------------------------------------------------------------- /www/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_HASURA_URL=http://graphql-engine:8080 2 | HASURA_ADMIN_SECRET=my-secret 3 | HASURA_JWT_SECRET_TYPE=HS256 4 | HASURA_JWT_SECRET_KEY=this-is-a-generic-HS256-secret-key-and-you-should-really-change-it 5 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /www/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | RUN mkdir -p /app 4 | WORKDIR /app 5 | 6 | COPY package*.json /app 7 | RUN yarn install 8 | COPY . /app 9 | 10 | EXPOSE 3000 11 | CMD ["yarn", "dev"] -------------------------------------------------------------------------------- /www/Makefile: -------------------------------------------------------------------------------- 1 | start_stripe_webhook_listener: 2 | docker run --net=host --rm -it stripe/stripe-cli:latest listen \ 3 | --api-key sk_test_XaYURKl3k5bVtc5EHl0vWpcY00rbF6cuHq \ 4 | --forward-to http://localhost:3000/api/webhooks/stripe \ 5 | --forward-connect-to http://localhost:3000/api/webhooks/stripe 6 | 7 | trigger_stripe_payment_intent_webhook: 8 | docker run --net=host --rm -it stripe/stripe-cli trigger payment_intent.created \ 9 | --api-key sk_test_XaYURKl3k5bVtc5EHl0vWpcY00rbF6cuHq 10 | 11 | trigger_stripe_payment_intent_succeeded_webhook: 12 | docker run --net=host --rm -it stripe/stripe-cli trigger payment_intent.succeeded \ 13 | --api-key sk_test_XaYURKl3k5bVtc5EHl0vWpcY00rbF6cuHq 14 | 15 | trigger_stripe_payment_intent_payment_failed_webhook: 16 | docker run --net=host --rm -it stripe/stripe-cli trigger payment_intent.payment_failed \ 17 | --api-key sk_test_XaYURKl3k5bVtc5EHl0vWpcY00rbF6cuHq 18 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /www/components/AccountAddresses.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import FormNewAddress from "./FormNewAddress"; 3 | import CardAddress from "./CardAddress"; 4 | 5 | import { 6 | useTypedLazyQuery, 7 | useTypedMutation, 8 | $, 9 | } from "../utils/gql-zeus-query-hooks"; 10 | 11 | const AccountAddresses = ({ id }) => { 12 | const [fetchAddresses, { data, loading, error, refetch }] = useTypedLazyQuery( 13 | { 14 | address: [ 15 | {}, 16 | { 17 | id: true, 18 | name: true, 19 | address_line_one: true, 20 | address_line_two: true, 21 | city: true, 22 | state: true, 23 | zipcode: true, 24 | orders_with_billing_address: { 25 | id: true, 26 | }, 27 | orders_with_shipping_address: { 28 | id: true, 29 | }, 30 | }, 31 | ], 32 | } 33 | ); 34 | 35 | const [removeAddress] = useTypedMutation( 36 | { 37 | delete_address: [ 38 | { 39 | where: { 40 | id: { 41 | _eq: $`id`, 42 | }, 43 | }, 44 | }, 45 | { 46 | returning: { 47 | id: true, 48 | }, 49 | }, 50 | ], 51 | }, 52 | { 53 | onCompleted: () => { 54 | refetch(); 55 | }, 56 | } 57 | ); 58 | 59 | React.useEffect(() => { 60 | fetchAddresses(); 61 | }, []); 62 | 63 | return ( 64 | 65 | 66 | {data?.address.length && 67 | data.address.map((address, i) => ( 68 | removeAddress({ variables: { id: address.id } })} 76 | /> 77 | ))} 78 | 79 | ); 80 | }; 81 | 82 | export default AccountAddresses; 83 | -------------------------------------------------------------------------------- /www/components/AdminChart.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Chart from "chart.js"; 3 | 4 | const AdminChart = ({ data }) => { 5 | const chart = React.useRef(null); 6 | 7 | React.useEffect(() => { 8 | let chartSales = new Chart(chart.current, { 9 | type: "bar", 10 | data: { 11 | labels: data.map((r) => r.key.slice(0, 5)).reverse(), 12 | datasets: [ 13 | { 14 | label: "Total", 15 | backgroundColor: "#1E40AF", 16 | data: data.map((r) => r.value.total.toFixed(2)).reverse(), 17 | }, 18 | ], 19 | }, 20 | maintainAspectRatio: false, 21 | options: { 22 | legend: { 23 | display: false, 24 | position: "bottom", 25 | }, 26 | tooltips: { 27 | mode: "index", 28 | intersect: false, 29 | callbacks: { 30 | label: function (t, d) { 31 | var xLabel = d.datasets[t.datasetIndex].label; 32 | var yLabel = t.yLabel; 33 | return xLabel + ": $" + yLabel; 34 | }, 35 | }, 36 | }, 37 | elements: { 38 | point: { 39 | radius: 0, 40 | }, 41 | }, 42 | scales: { 43 | xAxes: [ 44 | { 45 | stacked: true, 46 | scaleLabel: { 47 | display: true, 48 | labelString: "Date (2 Weeks)", 49 | }, 50 | gridLines: { 51 | color: "rgba(0, 0, 0, 0)", 52 | }, 53 | }, 54 | ], 55 | yAxes: [ 56 | { 57 | stacked: true, 58 | scaleLabel: { 59 | display: true, 60 | labelString: "$ Sales", 61 | }, 62 | gridLines: {}, 63 | callback: function (value, index, values) { 64 | return value + "%"; 65 | }, 66 | }, 67 | ], 68 | }, 69 | }, 70 | }); 71 | }, [data]); 72 | 73 | return ( 74 |
75 |
76 |

Sales - Last 2 Weeks

77 | 82 |
83 |
84 | ); 85 | }; 86 | 87 | export default AdminChart; 88 | -------------------------------------------------------------------------------- /www/components/AdminNewCustomers.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useTypedSubscription } from "../utils/gql-zeus-query-hooks"; 3 | 4 | const NewCustomers = () => { 5 | const { data, error, loading } = useTypedSubscription({ 6 | user: [ 7 | { 8 | limit: 12, 9 | where: { 10 | addresses: { 11 | address_line_one: { 12 | _is_null: false, 13 | }, 14 | }, 15 | created_at: { 16 | _gte: "2021-01-16 16:00:00 UTC", 17 | }, 18 | }, 19 | }, 20 | { 21 | id: true, 22 | name: true, 23 | email: true, 24 | created_at: true, 25 | orders: [ 26 | { 27 | limit: 1, 28 | }, 29 | { 30 | created_at: true, 31 | }, 32 | ], 33 | addresses: [ 34 | { 35 | limit: 1, 36 | }, 37 | { 38 | city: true, 39 | state: true, 40 | address_line_one: true, 41 | address_line_two: true, 42 | zipcode: true, 43 | created_at: true, 44 | }, 45 | ], 46 | }, 47 | ], 48 | }); 49 | 50 | return ( 51 | 52 |
53 |

Recent Customers

54 | 57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {loading &&

Loading...

} 76 | {error &&

Error: {error.message}

} 77 | {data && data.user && ( 78 | 79 | {data.user.map((user, index) => { 80 | const { orders, addresses, id, name, email, created_at } = user; 81 | const address = addresses.length ? addresses[0] : {}; 82 | const { 83 | city = "--", 84 | state = "--", 85 | address_line_one = "", 86 | address_line_two = "--", 87 | zipcode = "--", 88 | } = address; 89 | 90 | const order = orders.length ? orders[0] : {}; 91 | const { created_at: created_at_order = "--" } = order; 92 | 93 | const joined = new Date(created_at); 94 | const joinedFormatted = joined.toLocaleDateString(); 95 | 96 | const lastPurchase = new Date(created_at_order); 97 | const lastPurchaseformatted = joined.toLocaleDateString(); 98 | 99 | return ( 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 116 | 117 | ); 118 | })} 119 | 120 | )} 121 |
IDCustomer NameEmailCityStateAddress 1Address 2Zip CodeJoined DateLast PurchaseOrders
{id}{name}{email}{city}{state}{address_line_one}{address_line_two}{zipcode}{joinedFormatted}{lastPurchaseformatted} 112 | 113 | View Orders 114 | 115 |
122 |
123 |
124 | ); 125 | }; 126 | 127 | export default NewCustomers; 128 | -------------------------------------------------------------------------------- /www/components/AdminNewOrders.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import { useTypedSubscription } from "../utils/gql-zeus-query-hooks"; 4 | 5 | const NewOrders = () => { 6 | const { data, error, loading } = useTypedSubscription({ 7 | order: [ 8 | { 9 | limit: 12, 10 | where: { 11 | products: { 12 | product: { 13 | name: { 14 | _is_null: false, 15 | }, 16 | }, 17 | }, 18 | created_at: { 19 | _gte: "2021-01-16 16:00:00 UTC", 20 | }, 21 | }, 22 | }, 23 | { 24 | id: true, 25 | created_at: true, 26 | products: [ 27 | {}, 28 | { 29 | product: { 30 | id: true, 31 | name: true, 32 | price: true, 33 | }, 34 | }, 35 | ], 36 | user: { 37 | id: true, 38 | name: true, 39 | }, 40 | }, 41 | ], 42 | }); 43 | 44 | return ( 45 | 46 |
47 |

Recent Orders

48 | 51 |
52 |
53 | {loading &&

Loading...

} 54 | {error &&

Error: {error.message}

} 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 68 | {data && 69 | data.order && 70 | data.order.map((order, index) => { 71 | const { products, user, id, created_at } = order; 72 | const date = new Date(created_at); 73 | const formatted = date.toLocaleDateString(); 74 | 75 | const price = products.reduce((collector, current) => { 76 | return collector + current.product.price; 77 | }, 0); 78 | return ( 79 | 80 | 81 | 84 | 85 | 91 | 92 | 93 | 108 | 109 | ); 110 | })} 111 | 112 |
IDCustomer IDCustomer NameProductsDatePrice 65 |
{id} 82 | {user?.id} 83 | {user?.name} 86 | {products && 87 | products.map(({ product }, index) => ( 88 |
  • {product.name}
  • 89 | ))} 90 |
    {formatted}${price.toFixed(2)} 94 | product.id), 99 | }, 100 | }} 101 | > 102 | 103 | View Products{" "} 104 | 105 | 106 | 107 |
    113 |
    114 |
    115 | ); 116 | }; 117 | 118 | export default NewOrders; 119 | -------------------------------------------------------------------------------- /www/components/AdminStoreSummary.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import crossfilter from "crossfilter2"; 3 | import AdminChart from "./AdminChart"; 4 | import { useTypedSubscription } from "../utils/gql-zeus-query-hooks"; 5 | 6 | const AdminStoreSummary = () => { 7 | const [today, setToday] = React.useState(new Date()); 8 | const [timeSlice, setTimeSlice] = React.useState(14); 9 | const [analysis, setAnalysis] = React.useState(null); 10 | const [total, setTotal] = React.useState(0); 11 | const [totalOrders, setTotalOrders] = React.useState(0); 12 | const [totalUsers, setTotalUsers] = React.useState(0); 13 | 14 | const range = []; 15 | 16 | for (let day = 1; day <= timeSlice; day++) { 17 | const payload = {}; 18 | const d = new Date(); 19 | const h = new Date(d.setDate(today.getDate() - day)); 20 | payload.created_at = h.toISOString(); 21 | payload.products = []; 22 | payload.user_id = null; 23 | range.push(payload); 24 | } 25 | 26 | const created_at_filter = { 27 | created_at: { 28 | _gt: "2021-02-05T10:19:14.054Z", 29 | }, 30 | }; 31 | 32 | const { data, error, loading } = useTypedSubscription({ 33 | order: [ 34 | { 35 | order_by: [{ created_at: "desc" }], 36 | where: { 37 | ...created_at_filter, 38 | products: { 39 | product: { 40 | name: { 41 | _is_null: false, 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | { 48 | created_at: true, 49 | user_id: true, 50 | products: { 51 | product: { 52 | price: true, 53 | }, 54 | }, 55 | }, 56 | ], 57 | }); 58 | 59 | React.useEffect(() => { 60 | const sumProducts = (products) => 61 | products.length 62 | ? products.reduce((b, { product }) => b + product.price, 0) 63 | : 0; 64 | 65 | function reduceAdd(p, v, nf) { 66 | const d = new Date(v.created_at); 67 | const total = sumProducts(v.products); 68 | p.total += total; 69 | p.date = d.toLocaleDateString(); 70 | return p; 71 | } 72 | 73 | function reduceRemove(p, v, nf) { 74 | const total = sumProducts(v.products); 75 | p.total -= total; 76 | p.date = p.date; 77 | return p; 78 | } 79 | 80 | function reduceInitial() { 81 | return { date: "", total: 0 }; 82 | } 83 | 84 | function orderValue(p) { 85 | return p.date; 86 | } 87 | 88 | if (data) { 89 | const cf = crossfilter([...data.order, ...range]); 90 | 91 | const paymentsByDay = cf.dimension((input) => { 92 | let d = new Date(input.created_at); 93 | return d.toLocaleDateString(); 94 | }); 95 | 96 | const allUsers = cf.dimension((input) => { 97 | return input.user_id; 98 | }); 99 | 100 | const totalsByUser = allUsers 101 | .group() 102 | .reduce(reduceAdd, reduceRemove, reduceInitial) 103 | .top(Infinity); 104 | 105 | const grandTotal = paymentsByDay 106 | .groupAll() 107 | .reduceSum((d) => sumProducts(d.products)); 108 | setTotal(grandTotal.value().toFixed(2)); 109 | 110 | setTotalUsers( 111 | totalsByUser.filter((n) => { 112 | if (n.key) return n; 113 | }).length 114 | ); 115 | 116 | setTotalOrders( 117 | totalsByUser.filter((n) => { 118 | if (n.key && n.value.total > 0) return n; 119 | }).length 120 | ); 121 | 122 | const totalsByDay = paymentsByDay 123 | .group() 124 | .reduce(reduceAdd, reduceRemove, reduceInitial) 125 | .order(orderValue); 126 | setAnalysis(totalsByDay.top(timeSlice)); 127 | } 128 | }, [data]); 129 | 130 | return ( 131 | 132 |
    133 |

    Store Summary

    134 |

    Last 2 Weeks

    135 |
    136 |
    137 | {analysis && } 138 |
    139 |
    140 |
    141 |
    142 |

    Sales

    143 |

    {`$ ${total}`}

    144 |
    145 |
    146 |
    147 |
    148 |

    Orders

    149 |

    {totalOrders}

    150 |
    151 |
    152 |

    Customers

    153 |

    {totalUsers}

    154 |
    155 |
    156 |
    157 |
    158 |
    159 |
    160 | ); 161 | }; 162 | 163 | export default AdminStoreSummary; 164 | -------------------------------------------------------------------------------- /www/components/AllOffers.tsx: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | import * as React from "react"; 5 | import { searchState } from "../state/FilterState"; 6 | import { useTypedQuery } from "../utils/gql-zeus-query-hooks"; 7 | import Offer from "./Offer"; 8 | 9 | const AllOffers = () => { 10 | const [search] = useAtom(searchState); 11 | const router = useRouter(); 12 | 13 | const page: number = Number(router.query.page) || 1; 14 | const numItemsPerPage: number = Number(router.query.numItemsPerPage) || 12; 15 | 16 | function goToPage(page: number) { 17 | router.push({ pathname: "./", query: { page } }); 18 | } 19 | 20 | const { data, error, loading } = useTypedQuery({ 21 | product: [ 22 | { 23 | limit: numItemsPerPage, 24 | ...(page > 1 && { offset: page * numItemsPerPage }), 25 | where: { 26 | name: { 27 | _ilike: `%${search.searchString}%` || null, 28 | }, 29 | category: { 30 | name: { 31 | _eq: search.category, 32 | }, 33 | }, 34 | price: { 35 | _is_null: false, 36 | _gte: Number(0), 37 | _lte: Number(1000), 38 | }, 39 | ...(search.selectedBrands?.length && { 40 | brand: { _in: search.selectedBrands }, 41 | }), 42 | }, 43 | }, 44 | { 45 | id: true, 46 | name: true, 47 | description: true, 48 | price: true, 49 | image_urls: [{}, true], 50 | brand: true, 51 | }, 52 | ], 53 | }); 54 | 55 | const PageNavigationButtons = () => { 56 | const staticPreviousPage = ( 57 | 61 | ); 62 | 63 | const previousPage = ( 64 | 65 | ); 66 | 67 | const currentPage = ; 68 | 69 | const nextPage = ( 70 | 71 | ); 72 | 73 | const staticNextPage = ( 74 | 77 | ); 78 | 79 | return ( 80 |
    81 | {[ 82 | staticPreviousPage, 83 | previousPage, 84 | currentPage, 85 | nextPage, 86 | staticNextPage, 87 | ]} 88 |
    89 | ); 90 | }; 91 | 92 | return ( 93 |
    94 |

    Best Sellers

    95 |

    Best sellers from around the web.

    96 |
    97 |
    98 | {loading &&

    Loading...

    } 99 | {error &&

    Error: {error.message}

    } 100 | {data?.product?.map((product, index) => ( 101 | 102 | 103 | 104 | 105 | 106 | ))} 107 |
    108 |
    109 | 110 |
    111 | ); 112 | }; 113 | 114 | export default AllOffers; 115 | -------------------------------------------------------------------------------- /www/components/CardAddress.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const CardAddress = ({ 4 | name = "", 5 | address_line_one = "", 6 | address_line_two = "", 7 | city = "", 8 | state = "", 9 | zipcode = "", 10 | remove, 11 | locked, 12 | }) => { 13 | return ( 14 |
    15 | {!locked && ( 16 | 23 | )} 24 | 25 |

    {name}

    26 |

    {address_line_one}

    27 | {address_line_two &&

    {address_line_two}

    } 28 |

    29 | {city}, {state} 30 |

    31 |

    {zipcode}

    32 |
    33 | ); 34 | }; 35 | 36 | export default CardAddress; 37 | -------------------------------------------------------------------------------- /www/components/Cart.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { useAtom } from "jotai"; 3 | import Link from "next/link"; 4 | 5 | import QuantitySelect from "./QuantitySelect"; 6 | 7 | import { cartIsOpen, cart } from "../state/CartState"; 8 | 9 | const Cart = () => { 10 | const [cartOpen, setCartOpen] = useAtom(cartIsOpen); 11 | const [cartState, setCartState] = useAtom(cart); 12 | 13 | const updateCart = (id, index) => (value) => { 14 | const product = cartState.find((prod) => prod.id === id); 15 | const updatedProduct = { ...product }; 16 | updatedProduct.quantity = value; 17 | const updatedCart = [...cartState]; 18 | updatedCart[index] = updatedProduct; 19 | setCartState(updatedCart); 20 | }; 21 | 22 | return ( 23 | 24 | 68 |
    setCartOpen(false)} 70 | id="sidebar-cart-bg" 71 | className={`${cartOpen ? "active" : ""}`} 72 | /> 73 | 74 | ); 75 | }; 76 | 77 | export default Cart; 78 | -------------------------------------------------------------------------------- /www/components/FilterBrands.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useAtom } from "jotai"; 3 | 4 | import { selectedBrands, selectedCategory } from "../state/FilterState"; 5 | 6 | import { 7 | brands, 8 | brandsLoading, 9 | brandsLoadingError, 10 | fetchBrandData, 11 | } from "../state/BrandState"; 12 | 13 | const SLICE_SIZE = 10; 14 | 15 | const FilterBrands = () => { 16 | // Handlers 17 | const brandChange = (e, brand) => { 18 | if (selectedBrandsState.includes(brand)) { 19 | setSelectedBrandsState((values) => 20 | values.filter((item) => item !== brand) 21 | ); 22 | } else { 23 | setSelectedBrandsState((values) => [brand, ...values]); 24 | } 25 | }; 26 | 27 | const filter = (input) => { 28 | if (search.length) { 29 | return input.brand.toLowerCase().includes(search.toLowerCase()); 30 | } 31 | return true; 32 | }; 33 | 34 | // Global State 35 | const [_, fetchBrands] = useAtom(fetchBrandData); 36 | const [selectedBrandsState, setSelectedBrandsState] = useAtom(selectedBrands); 37 | const [brandState] = useAtom(brands); 38 | const [brandsLoadingState] = useAtom(brandsLoading); 39 | 40 | const [selectedCategoryState] = useAtom(selectedCategory); 41 | 42 | // React Life Cycle 43 | React.useEffect(() => { 44 | fetchBrands("http://localhost:3000/api/brands"); 45 | }, [selectedCategoryState]); 46 | 47 | // Local State 48 | const [search, setSearch] = React.useState(""); 49 | const [sliced, setSliced] = React.useState(true); 50 | 51 | // Derived Local State 52 | const brandList = brandState.filter(filter); 53 | const paginate = brandList.length > SLICE_SIZE; 54 | const slice = sliced ? SLICE_SIZE : undefined; 55 | 56 | return ( 57 | 58 | 59 |
    60 | 61 | 62 | 63 | setSearch(e.target.value)} 67 | /> 68 |
    69 | {brandsLoadingState &&

    Loading…

    } 70 | {!brandList.length &&

    No Items...

    } 71 | {brandList.length > 1 && 72 | brandList 73 | .filter(filter) 74 | .slice(0, slice) 75 | .map(({ brand, slug }, index) => ( 76 |
    77 | brandChange(e, brand)} 82 | checked={selectedBrandsState.includes(brand)} 83 | /> 84 | 85 |
    86 | ))} 87 | {sliced && paginate && ( 88 | 93 | )} 94 | {paginate && !sliced && ( 95 | 100 | )} 101 |
    102 | ); 103 | }; 104 | 105 | export default FilterBrands; 106 | -------------------------------------------------------------------------------- /www/components/FilterDebug.js: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | 3 | import { searchState } from "../state/FilterState"; 4 | import { fetchData } from "../state/OfferState"; 5 | 6 | const FilterDebug = () => { 7 | const [searchStateObj] = useAtom(searchState); 8 | const [_, fetchOffers] = useAtom(fetchData); 9 | return ( 10 | <> 11 |
    12 |         {JSON.stringify(searchStateObj, "", 4)}
    13 |       
    14 | 17 | 18 | ); 19 | }; 20 | 21 | export default FilterDebug; 22 | -------------------------------------------------------------------------------- /www/components/FilterOptions.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useAtom } from "jotai"; 3 | 4 | import { 5 | onlyFreeShipping, 6 | includeOutOfStock, 7 | onlyOnSale, 8 | onlyFinalClearance, 9 | } from "../state/FilterState"; 10 | 11 | const FilterOptions = () => { 12 | const [onlyFreeShippingState, setOnlyFreeShippingState] = useAtom( 13 | onlyFreeShipping 14 | ); 15 | const [includeOutOfStockState, setIncludeOutOfStockState] = useAtom( 16 | includeOutOfStock 17 | ); 18 | const [onlyOnSaleState, setOnlyOnSaleState] = useAtom(onlyOnSale); 19 | const [onlyFinalClearanceState, setOnlyFinalClearanceState] = useAtom( 20 | onlyFinalClearance 21 | ); 22 | 23 | return ( 24 | 25 | 26 |
    27 | { 32 | setOnlyFreeShippingState((v) => !v); 33 | }} 34 | /> 35 | 36 |
    37 | 38 |
    39 | setOnlyOnSaleState((v) => !v)} 44 | /> 45 | 46 |
    47 |
    48 | setOnlyFinalClearanceState((v) => !v)} 53 | /> 54 | 57 |
    58 | 59 |
    60 | setIncludeOutOfStockState((v) => !v)} 65 | /> 66 | 67 |
    68 |
    69 | ); 70 | }; 71 | 72 | export default FilterOptions; 73 | -------------------------------------------------------------------------------- /www/components/FilterPrice.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useAtom } from "jotai"; 3 | 4 | import { priceLow, priceHigh } from "../state/FilterState"; 5 | 6 | const MIN_PRICE = "0"; 7 | const MAX_PRICE = "1000000"; 8 | 9 | const priceBrackets = [ 10 | { 11 | label: "All", 12 | slug: "all", 13 | low: MIN_PRICE, 14 | high: MAX_PRICE, 15 | }, 16 | { 17 | label: "Under $25", 18 | slug: "under-25", 19 | low: MIN_PRICE, 20 | high: "25", 21 | }, 22 | { 23 | label: "$25 - $50", 24 | slug: "25-50", 25 | low: "25", 26 | high: "50", 27 | }, 28 | { 29 | label: "$50 - $100", 30 | slug: "50-100", 31 | low: "50", 32 | high: "100", 33 | }, 34 | { 35 | label: "$100 - $200", 36 | slug: "100-200", 37 | low: "100", 38 | high: "200", 39 | }, 40 | { 41 | label: "$200 & Above", 42 | slug: "200-above", 43 | low: "200", 44 | high: MAX_PRICE, 45 | }, 46 | ]; 47 | 48 | const FilterPrice = () => { 49 | const [priceLowState, setPriceLowState] = useAtom(priceLow); 50 | const [priceHighState, setPriceHighState] = useAtom(priceHigh); 51 | 52 | const [inputState, setInputState] = React.useState("clean"); 53 | 54 | const setHighPrice = (e) => { 55 | if (e.target.value.length) { 56 | setInputState("dirty"); 57 | } 58 | setPriceHighState(e.target.value); 59 | }; 60 | 61 | const setLowPrice = (e) => { 62 | if (e.target.value.length) { 63 | setInputState("dirty"); 64 | } 65 | setPriceLowState(e.target.value); 66 | }; 67 | 68 | const priceChange = (e) => { 69 | const { low, high } = priceBrackets.find((pb) => pb.slug === e.target.id); 70 | 71 | setPriceHighState(high); 72 | setPriceLowState(low); 73 | }; 74 | 75 | const inputDirty = inputState === "dirty"; 76 | 77 | return ( 78 | 79 | 80 | 81 | {priceBrackets.map((bracket, index) => { 82 | let checked = false; 83 | 84 | if (priceLowState === bracket.low && priceHighState === bracket.high) { 85 | checked = true; 86 | } 87 | 88 | return ( 89 |
    90 | 97 | 98 |
    99 | ); 100 | })} 101 | 102 |
    103 | $ 104 | 109 |
    110 |
    111 | $ 112 | 117 |
    118 |
    119 | ); 120 | }; 121 | 122 | export default FilterPrice; 123 | -------------------------------------------------------------------------------- /www/components/FormLogin.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useAtom, atom } from "jotai"; 3 | import FormValidations from "./FormValidations"; 4 | 5 | const user = atom({ 6 | email: "", 7 | password: "", 8 | }); 9 | 10 | const formValid = atom((get) => { 11 | const userObj = get(user); 12 | const vals = Object.values(userObj).reduce((prev, next) => { 13 | if (prev && next.length > 0) return true; 14 | return false; 15 | }, true); 16 | return vals; 17 | }); 18 | 19 | const compareCheck = (compare) => (predicate) => compare === predicate; 20 | 21 | const FormSignup = ({ handleSubmit }) => { 22 | const [userState, setUser] = useAtom(user); 23 | const [validForm] = useAtom(formValid); 24 | 25 | const updateUser = (e) => { 26 | const { name, value } = e.target; 27 | 28 | setUser((user) => { 29 | const updatedUser = user; 30 | updatedUser[name] = value; 31 | return { ...user, ...updatedUser }; 32 | }); 33 | }; 34 | 35 | const submitForm = (e) => { 36 | e.preventDefault(); 37 | handleSubmit(userState); 38 | }; 39 | 40 | return ( 41 |
    42 |
    43 |
    44 |
    45 |

    Login

    46 |
    47 |
    48 |

    Email

    49 | 58 |

    Password

    59 | 67 | 75 | 78 |
    79 |
    80 |
    81 |
    82 |
    83 | ); 84 | }; 85 | 86 | export default FormSignup; 87 | -------------------------------------------------------------------------------- /www/components/FormSignup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useAtom, atom } from "jotai"; 3 | import FormValidations from "./FormValidations"; 4 | 5 | const user = atom({ 6 | name: "", 7 | email: "", 8 | password: "", 9 | passwordver: "", 10 | }); 11 | 12 | const formValid = atom((get) => { 13 | const userObj = get(user); 14 | const vals = Object.values(userObj).reduce((prev, next) => { 15 | if (prev && next.length > 0) return true; 16 | return false; 17 | }, true); 18 | 19 | const matchingPass = userObj.password === userObj.passwordver; 20 | return vals && matchingPass; 21 | }); 22 | 23 | const compareCheck = (compare) => (predicate) => compare === predicate; 24 | 25 | const FormSignup = ({ handleSubmit }) => { 26 | const [userState, setUser] = useAtom(user); 27 | const [validForm] = useAtom(formValid); 28 | 29 | const [validPassword, setValidPassword] = React.useState(false); 30 | const [formState, setFormState] = React.useState("pristine"); 31 | const formCheck = compareCheck(formState); 32 | 33 | const updateUser = (e) => { 34 | const { name, value } = e.target; 35 | 36 | if (formCheck("pristine")) { 37 | setFormState("dirty"); 38 | } 39 | 40 | setUser((user) => { 41 | const updatedUser = user; 42 | updatedUser[name] = value; 43 | return { ...user, ...updatedUser }; 44 | }); 45 | }; 46 | 47 | const submitForm = (e) => { 48 | e.preventDefault(); 49 | handleSubmit(userState); 50 | }; 51 | 52 | return ( 53 |
    54 |
    55 |
    56 |
    57 |

    Sign-Up

    58 |
    59 |
    60 |

    Username

    61 | 69 |

    Email

    70 | 79 |

    Password

    80 | 88 |

    Repeat Password

    89 | 97 |
    98 | {!formCheck("pristine") && ( 99 | 103 | )} 104 | 114 | 117 | 118 |
    119 |
    120 |
    121 |
    122 | ); 123 | }; 124 | 125 | export default FormSignup; 126 | -------------------------------------------------------------------------------- /www/components/FormValidations.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const FormValidations = ({ input, onValid }) => { 4 | const [validPassword, setValidPassword] = React.useState(false); 5 | const [validations, setValidations] = React.useState([ 6 | { 7 | label: "1 Lowercase Character", 8 | passes: false, 9 | pattern: /[a-z]/, 10 | }, 11 | { 12 | label: "1 Uppercase Character (A-Z)", 13 | passes: false, 14 | pattern: /[A-Z]/, 15 | }, 16 | { 17 | label: "1 Number (0-9)", 18 | passes: false, 19 | pattern: /[0-9]/, 20 | }, 21 | { 22 | label: "1 Special Character ($@#&!-)", 23 | passes: false, 24 | pattern: /[\$\@#&\!-]/, 25 | }, 26 | ]); 27 | 28 | React.useEffect(() => { 29 | setValidPassword(true); 30 | 31 | const updatedValidations = validations.map((validation) => { 32 | let check = new RegExp(validation.pattern); 33 | validation.passes = check.test(input); 34 | if (!validation.passes && validPassword) { 35 | setValidPassword(false); 36 | } 37 | return validation; 38 | }); 39 | 40 | if (validPassword) { 41 | onValid(true); 42 | } 43 | setValidations(updatedValidations); 44 | }, [input]); 45 | 46 | return ( 47 | 48 |

    Your password must contain at least:

    49 |
    50 | {validations.map((validation, index) => { 51 | const { label, passes } = validation; 52 | return ( 53 |

    54 | {passes && } 55 | {!passes && } 56 | {label} 57 |

    58 | ); 59 | })} 60 |
    61 |
    62 | ); 63 | }; 64 | 65 | export default FormValidations; 66 | -------------------------------------------------------------------------------- /www/components/Header.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Link from "next/link"; 4 | import { useRouter } from "next/router"; 5 | 6 | import { useAtom } from "jotai"; 7 | 8 | import { menuIsOpen } from "../state/MenuState"; 9 | 10 | import { 11 | categories, 12 | categoriesLoading, 13 | categoriesLoadingError, 14 | fetchCategoryData, 15 | } from "../state/CategoryState"; 16 | 17 | import { searchString } from "../state/FilterState"; 18 | 19 | import HeaderControls from "./HeaderControls"; 20 | 21 | const NUM_DISPLAYED_CATEGORIES = 10; 22 | 23 | const Header = () => { 24 | const [, setMenuOpen] = useAtom(menuIsOpen); 25 | const router = useRouter(); 26 | const { query } = router; 27 | 28 | const [searchValue, setSearchValue] = useAtom(searchString); 29 | const [searchIsOpen, setSearchIsOpen] = React.useState(false); 30 | 31 | const [_, fetchCategories] = useAtom(fetchCategoryData); 32 | const [categoriesState] = useAtom(categories); 33 | const [categoriesLoadingState] = useAtom(categoriesLoading); 34 | 35 | React.useEffect(() => { 36 | fetchCategories(); 37 | }, []); 38 | 39 | return ( 40 | <> 41 |
    42 |
    43 |
    44 | 45 |
    46 |
    47 |
    48 |
    49 | setSearchIsOpen(true)} 54 | onBlur={() => setSearchIsOpen(false)} 55 | onChange={(e) => setSearchValue(e.target.value)} 56 | /> 57 | 60 |
    61 | 88 |
    1 && searchIsOpen ? "active" : "" 92 | }`} 93 | /> 94 |
    95 | 96 |
    97 |
    98 | 106 | {categoriesState.length && 107 | categoriesState 108 | .slice(0, NUM_DISPLAYED_CATEGORIES) 109 | .map((category, index) => { 110 | const active = query.category === category.name; 111 | return ( 112 | 113 | 114 | 117 | {category.display_name} 118 | 119 | 120 | 121 | ); 122 | })} 123 |
    124 | 125 | ); 126 | }; 127 | 128 | export default Header; 129 | -------------------------------------------------------------------------------- /www/components/HeaderAdmin.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | 5 | const HeaderAdmin = () => { 6 | const router = useRouter(); 7 | 8 | const nav = [ 9 | { 10 | label: "Summary", 11 | path: "/admin", 12 | }, 13 | { 14 | label: "Customers", 15 | path: "/admin/customers", 16 | }, 17 | { 18 | label: "Orders", 19 | path: "/admin/orders", 20 | }, 21 | { 22 | label: "Products", 23 | path: "/admin/products", 24 | }, 25 | // { 26 | // label: "Discounts", 27 | // path: "/admin/discounts", 28 | // }, 29 | ]; 30 | 31 | return ( 32 | 33 |
    34 |
    35 | 36 |
    37 | 38 | 39 | Menu 40 | 41 | 42 | {nav.map((n, i) => { 43 | const active = router.pathname === n.path; 44 | return ( 45 | 46 | 47 | 52 | {n.label} 53 | 54 | 55 | 56 | ); 57 | })} 58 | 59 | 60 | Back to Store 61 | 62 | 63 |
    64 | 76 |