├── .gitignore
├── LICENSE
├── README.md
├── backend
├── .env.template
├── .gitignore
├── .medusa
│ └── types
│ │ ├── index.d.ts
│ │ └── remote-query-entry-points.d.ts
├── .yarnrc.yml
├── README.md
├── data
│ └── medusa-eats-seed-data.json
├── jest.config.js
├── medusa-config.ts
├── package.json
├── src
│ ├── admin
│ │ ├── components
│ │ │ ├── actions-menu.tsx
│ │ │ ├── delivery-actions-menu.tsx
│ │ │ ├── delivery-items.tsx
│ │ │ ├── delivery-row.tsx
│ │ │ ├── driver-actions-menu.tsx
│ │ │ └── icons.tsx
│ │ ├── hooks
│ │ │ └── index.tsx
│ │ ├── routes
│ │ │ ├── deliveries
│ │ │ │ └── page.tsx
│ │ │ ├── drivers
│ │ │ │ └── page.tsx
│ │ │ └── restaurants
│ │ │ │ └── page.tsx
│ │ └── tsconfig.json
│ ├── api
│ │ ├── README.md
│ │ ├── middlewares.ts
│ │ ├── store
│ │ │ ├── deliveries
│ │ │ │ ├── [id]
│ │ │ │ │ ├── accept
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── claim
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── complete
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── decline
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── pass
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── pick-up
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── prepare
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── ready
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── route.ts
│ │ │ │ │ └── subscribe
│ │ │ │ │ │ └── route.ts
│ │ │ │ ├── route.ts
│ │ │ │ └── subscribe
│ │ │ │ │ └── route.ts
│ │ │ ├── drivers
│ │ │ │ ├── [id]
│ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ ├── restaurants
│ │ │ │ ├── [id]
│ │ │ │ │ ├── admins
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── products
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ ├── route.ts
│ │ │ │ │ └── status
│ │ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ └── users
│ │ │ │ ├── me
│ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ └── types.ts
│ ├── jobs
│ │ └── README.md
│ ├── links
│ │ ├── delivery-cart.ts
│ │ ├── delivery-order.ts
│ │ ├── restaurant-deliveries.ts
│ │ └── restaurant-products.ts
│ ├── loaders
│ │ └── README.md
│ ├── modules
│ │ ├── delivery
│ │ │ ├── index.ts
│ │ │ ├── migrations
│ │ │ │ ├── .snapshot-medusa-TGe-.json
│ │ │ │ └── Migration20240822141125.ts
│ │ │ ├── models
│ │ │ │ ├── delivery-driver.ts
│ │ │ │ ├── delivery.ts
│ │ │ │ ├── driver.ts
│ │ │ │ └── index.ts
│ │ │ ├── service.ts
│ │ │ └── types
│ │ │ │ ├── common.ts
│ │ │ │ └── mutations.ts
│ │ └── restaurant
│ │ │ ├── index.ts
│ │ │ ├── migrations
│ │ │ ├── .snapshot-medusa-eats.json
│ │ │ └── Migration20240925143313.ts
│ │ │ ├── models
│ │ │ ├── index.ts
│ │ │ ├── restaurant-admin.ts
│ │ │ └── restaurant.ts
│ │ │ ├── service.ts
│ │ │ └── types
│ │ │ ├── common.ts
│ │ │ └── mutations.ts
│ ├── scripts
│ │ └── seed.ts
│ ├── subscribers
│ │ └── README.md
│ ├── utils
│ │ ├── create-variant-price-set.ts
│ │ └── index.ts
│ └── workflows
│ │ ├── delivery
│ │ ├── steps
│ │ │ ├── await-delivery.ts
│ │ │ ├── await-pick-up.ts
│ │ │ ├── await-preparation.ts
│ │ │ ├── await-start-preparation.ts
│ │ │ ├── create-delivery.ts
│ │ │ ├── create-fulfillment.ts
│ │ │ ├── create-order.ts
│ │ │ ├── delete-delivery-drivers.ts
│ │ │ ├── find-driver.ts
│ │ │ ├── index.ts
│ │ │ ├── notify-restaurant.ts
│ │ │ ├── set-transaction-id.ts
│ │ │ └── update-delivery.ts
│ │ └── workflows
│ │ │ ├── claim-delivery.ts
│ │ │ ├── create-delivery.ts
│ │ │ ├── handle-delivery.ts
│ │ │ ├── index.ts
│ │ │ ├── pass-delivery.ts
│ │ │ └── update-delivery.ts
│ │ ├── restaurant
│ │ ├── steps
│ │ │ ├── create-restaurant.ts
│ │ │ └── index.ts
│ │ └── workflows
│ │ │ ├── create-restaurant-products.ts
│ │ │ ├── create-restaurant.ts
│ │ │ └── index.ts
│ │ ├── user
│ │ ├── steps
│ │ │ ├── create-user.ts
│ │ │ ├── index.ts
│ │ │ └── update-user.ts
│ │ └── workflows
│ │ │ ├── create-user.ts
│ │ │ └── index.ts
│ │ └── util
│ │ └── steps
│ │ ├── index.ts
│ │ ├── set-step-failed.ts
│ │ └── set-step-success.ts
├── tsconfig.json
└── yarn.lock
└── frontend
├── .env.template
├── .eslintrc.json
├── .gitignore
├── README.md
├── next.config.mjs
├── package.json
├── postcss.config.js
├── public
├── _1ed747de-53f1-4915-aaba-92c046039beb.jpeg
├── _35841975-1e89-4c07-adb8-208cc8c59e17.jpeg
├── _411c260a-d6e5-4989-9a00-1cd8b80265fb.jpeg
├── _4476ce30-b852-402d-b65f-e3db54165e4c.jpeg
├── _7a9bad3a-6f04-4030-a584-a622a1bb8c68.jpeg
├── _ca16b6a2-9c4c-4a6a-98fa-620f12f6123f.jpeg
├── _de4759b3-096f-47f6-936a-ea291df53f9f.jpeg
├── medusa-logo.svg
├── next.svg
├── notification.mp3
├── pizza-loading.gif
└── vercel.svg
├── src
├── app
│ ├── (checkout)
│ │ ├── checkout
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ └── your-order
│ │ │ └── page.tsx
│ ├── (store)
│ │ ├── layout.tsx
│ │ ├── login
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ ├── restaurant
│ │ │ └── [handle]
│ │ │ │ └── page.tsx
│ │ └── signup
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ ├── api
│ │ └── subscribe
│ │ │ └── route.ts
│ ├── dashboard
│ │ ├── driver
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ └── restaurant
│ │ │ ├── menu
│ │ │ └── page.tsx
│ │ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ └── layout.tsx
├── components
│ ├── common
│ │ ├── demo-modal.tsx
│ │ ├── footer.tsx
│ │ ├── icons.tsx
│ │ └── profile-badge.tsx
│ ├── dashboard
│ │ ├── account-badge.tsx
│ │ ├── completed-grid.tsx
│ │ ├── delivery-card.tsx
│ │ ├── delivery-column.tsx
│ │ ├── driver
│ │ │ ├── create-category-drawer.tsx
│ │ │ ├── delivery-buttons.tsx
│ │ │ └── delivery-status-badge.tsx
│ │ ├── login-form.tsx
│ │ ├── menu
│ │ │ ├── create-category-drawer.tsx
│ │ │ ├── create-category-form.tsx
│ │ │ ├── create-product-drawer.tsx
│ │ │ ├── create-product-form.tsx
│ │ │ ├── menu-actions.tsx
│ │ │ └── menu-product-actions.tsx
│ │ ├── realtime-client.tsx
│ │ ├── restaurant
│ │ │ ├── delivery-buttons.tsx
│ │ │ ├── delivery-status-badge.tsx
│ │ │ └── restaurant-status.tsx
│ │ └── signup-form.tsx
│ └── store
│ │ ├── cart
│ │ ├── cart-button.tsx
│ │ ├── cart-counter.tsx
│ │ ├── cart-modal.tsx
│ │ └── nav-cart.tsx
│ │ ├── checkout
│ │ ├── checkout-form.tsx
│ │ └── order-summary.tsx
│ │ ├── order
│ │ ├── lottie-player.tsx
│ │ ├── order-status-content-tab.tsx
│ │ └── order-status.tsx
│ │ └── restaurant
│ │ ├── dish-card.tsx
│ │ ├── restaurant-categories.tsx
│ │ └── restaurant-category.tsx
├── lib
│ ├── actions
│ │ ├── carts.ts
│ │ ├── checkout.ts
│ │ ├── deliveries.ts
│ │ ├── index.ts
│ │ ├── restaurants.ts
│ │ └── users.ts
│ ├── config.ts
│ ├── data
│ │ ├── carts.ts
│ │ ├── categories.ts
│ │ ├── cookies.ts
│ │ ├── deliveries.ts
│ │ ├── drivers.ts
│ │ ├── index.ts
│ │ ├── restaurants.ts
│ │ ├── sessions.ts
│ │ └── users.ts
│ ├── types.ts
│ └── util
│ │ ├── constants.ts
│ │ └── get-numeric-status.ts
└── middleware.ts
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.DS_Store
2 | **/dump.rdb
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Medusajs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/backend/.env.template:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://postgres@localhost/medusa-eats
2 | FRONTEND_URL=http://localhost:3000
3 | JWT_SECRET=supersecret
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | .env
3 | .DS_Store
4 | /uploads
5 | /node_modules
6 | yarn-error.log
7 |
8 | .idea
9 |
10 | coverage
11 |
12 | !src/**
13 |
14 | ./tsconfig.tsbuildinfo
15 | package-lock.json
16 | medusa-db.sql
17 | build
18 | .cache
19 | gh-private-key.pem
20 |
21 | .yarn/*
22 | !.yarn/patches
23 | !.yarn/plugins
24 | !.yarn/releases
25 | !.yarn/sdks
26 | !.yarn/versions
27 |
28 | .medusa/*
29 | .medusa/
--------------------------------------------------------------------------------
/backend/.medusa/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export * as RemoteQueryEntryPointsTypes from './remote-query-entry-points'
--------------------------------------------------------------------------------
/backend/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
--------------------------------------------------------------------------------
/backend/jest.config.js:
--------------------------------------------------------------------------------
1 | const { loadEnv } = require('@medusajs/utils')
2 | loadEnv('test', process.cwd())
3 |
4 | module.exports = {
5 | transform: {
6 | "^.+\\.[jt]s$": [
7 | "@swc/jest",
8 | {
9 | jsc: {
10 | parser: { syntax: "typescript", decorators: true },
11 | },
12 | },
13 | ],
14 | },
15 | testEnvironment: "node",
16 | moduleFileExtensions: ["js", "ts", "json"],
17 | modulePathIgnorePatterns: ["dist/"],
18 | }
19 |
20 | if (process.env.TEST_TYPE === "integration:http") {
21 | module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"]
22 | } else if (process.env.TEST_TYPE === "integration:modules") {
23 | module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"]
24 | } else if (process.env.TEST_TYPE === "unit") {
25 | module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"]
26 | }
--------------------------------------------------------------------------------
/backend/medusa-config.ts:
--------------------------------------------------------------------------------
1 | import { loadEnv, defineConfig, Modules } from "@medusajs/framework/utils";
2 |
3 | loadEnv(process.env.NODE_ENV || "development", process.cwd());
4 |
5 | module.exports = defineConfig({
6 | admin: {
7 | backendUrl: "https://medusa-eats.medusajs.app",
8 | },
9 | projectConfig: {
10 | databaseUrl: process.env.DATABASE_URL,
11 | http: {
12 | storeCors: process.env.STORE_CORS,
13 | adminCors: process.env.ADMIN_CORS,
14 | authCors: process.env.AUTH_CORS,
15 | jwtSecret: process.env.JWT_SECRET || "supersecret",
16 | cookieSecret: process.env.COOKIE_SECRET || "supersecret",
17 | },
18 | },
19 | modules: {
20 | restaurantModuleService: {
21 | resolve: "./modules/restaurant",
22 | },
23 | deliveryModuleService: {
24 | resolve: "./modules/delivery",
25 | },
26 | [Modules.FULFILLMENT]: {
27 | options: {
28 | providers: [
29 | {
30 | resolve: "@medusajs/fulfillment-manual",
31 | id: "manual-provider",
32 | },
33 | ],
34 | },
35 | },
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "medusa-eats-backend",
3 | "version": "0.0.1",
4 | "description": "A Medusa backend for Medusa Eats.",
5 | "author": "Medusa (https://medusajs.com)",
6 | "license": "MIT",
7 | "keywords": [
8 | "sqlite",
9 | "postgres",
10 | "typescript",
11 | "ecommerce",
12 | "headless",
13 | "medusa",
14 | "medusa-eats"
15 | ],
16 | "scripts": {
17 | "build": "medusa build",
18 | "seed": "medusa exec ./src/scripts/seed.ts",
19 | "start": "medusa start",
20 | "dev": "medusa develop",
21 | "setup-db": "npx medusa build && npx medusa migrations run && npx medusa links sync && medusa exec ./src/scripts/seed.ts && npx medusa user -e admin@email.com -p supersecret",
22 | "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
23 | "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit",
24 | "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit"
25 | },
26 | "dependencies": {
27 | "@medusajs/admin-sdk": "2.0.0",
28 | "@medusajs/cli": "2.0.0",
29 | "@medusajs/framework": "2.0.0",
30 | "@medusajs/medusa": "2.0.0",
31 | "@mikro-orm/core": "5.9.7",
32 | "@mikro-orm/knex": "5.9.7",
33 | "@mikro-orm/migrations": "5.9.7",
34 | "@mikro-orm/postgresql": "5.9.7",
35 | "awilix": "^8.0.1",
36 | "cors": "^2.8.5",
37 | "dotenv": "16.3.1",
38 | "express": "^4.17.2",
39 | "jsonwebtoken": "^9.0.2",
40 | "lucide-react": "^0.379.0",
41 | "pg": "^8.13.0",
42 | "prism-react-renderer": "^2.0.4",
43 | "prop-types": "^15.8.1"
44 | },
45 | "devDependencies": {
46 | "@medusajs/test-utils": "2.0.0",
47 | "@mikro-orm/cli": "5.9.7",
48 | "@mikro-orm/core": "5.9.7",
49 | "@mikro-orm/migrations": "5.9.7",
50 | "@mikro-orm/postgresql": "5.9.7",
51 | "@stdlib/number-float64-base-normalize": "0.0.8",
52 | "@swc/core": "1.5.7",
53 | "@swc/jest": "^0.2.36",
54 | "@types/express": "^4.17.13",
55 | "@types/jest": "^29.5.13",
56 | "@types/jsonwebtoken": "^9.0.6",
57 | "@types/mime": "1.3.5",
58 | "@types/node": "^20.0.0",
59 | "@types/react": "^18.3.2",
60 | "@types/react-dom": "^18.2.25",
61 | "jest": "^29.7.0",
62 | "prop-types": "^15.8.1",
63 | "react": "^18.2.0",
64 | "react-dom": "^18.2.0",
65 | "ts-node": "^10.9.2",
66 | "typescript": "^5.6.2",
67 | "vite": "^5.2.11"
68 | },
69 | "engines": {
70 | "node": ">=20"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/backend/src/admin/components/actions-menu.tsx:
--------------------------------------------------------------------------------
1 | import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons";
2 | import { DropdownMenu, IconButton } from "@medusajs/ui";
3 |
4 | const ActionsMenu = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Edit
16 |
17 |
18 |
19 |
20 | Delete
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default ActionsMenu;
28 |
--------------------------------------------------------------------------------
/backend/src/admin/components/delivery-actions-menu.tsx:
--------------------------------------------------------------------------------
1 | import { ArrrowRight, EllipsisHorizontal, Trash } from "@medusajs/icons";
2 | import { DropdownMenu, IconButton } from "@medusajs/ui";
3 | import { DeliveryDTO } from "src/types/delivery/common";
4 |
5 | const DeliveryActionsMenu = ({ delivery }: { delivery: DeliveryDTO }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | View customer
17 |
18 | {delivery.driver_id && (
19 |
20 |
21 |
22 | View driver
23 |
24 |
25 | )}
26 | {delivery.order?.id && (
27 |
28 |
29 |
30 | View order
31 |
32 |
33 | )}
34 | {delivery.restaurant.id && (
35 |
36 |
37 |
38 | View restaurant
39 |
40 |
41 | )}
42 |
43 |
44 |
45 | Cancel order
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default DeliveryActionsMenu;
53 |
--------------------------------------------------------------------------------
/backend/src/admin/components/delivery-items.tsx:
--------------------------------------------------------------------------------
1 | import { Heading, Text, Container } from "@medusajs/ui";
2 | import { DeliveryDTO } from "../../modules/delivery/types/common";
3 | import { useEffect, useRef, useState } from "react";
4 |
5 | const DeliveryItems = ({ delivery }: { delivery: DeliveryDTO }) => {
6 | const [hovered, setHovered] = useState(false);
7 |
8 | // on hovering over the items count, set hovered to true
9 | const handleMouseEnter = () => {
10 | setHovered(true);
11 | };
12 |
13 | // on leaving the items count, set hovered to false
14 | const handleMouseLeave = () => {
15 | setHovered(false);
16 | };
17 |
18 | return (
19 | <>
20 |
25 |
26 | {delivery.items.length}
27 |
28 |
29 | {hovered && (
30 |
31 | {delivery.items.map((item) => (
32 |
33 | ))}
34 |
35 | )}
36 | >
37 | );
38 | };
39 |
40 | const DeliveryItem = ({ item }: { item: Record }) => {
41 | const thumbnail = item.thumbnail;
42 |
43 | return (
44 |
45 |
52 |
53 |
54 |
55 | {item.title}
56 |
57 |
58 | {item.quantity} x ${item.unit_price}
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default DeliveryItems;
67 |
--------------------------------------------------------------------------------
/backend/src/admin/components/delivery-row.tsx:
--------------------------------------------------------------------------------
1 | import { Table, Badge } from "@medusajs/ui";
2 | import { DeliveryDTO } from "../../modules/delivery/types/common";
3 | import { useRestaurants } from "../hooks";
4 | import DeliveryActionsMenu from "./delivery-actions-menu";
5 | import DeliveryItems from "./delivery-items";
6 |
7 | const DeliveryRow = ({ delivery }: { delivery: DeliveryDTO }) => {
8 | const { data, loading } = useRestaurants({
9 | id: delivery.restaurant.id,
10 | });
11 |
12 | if (loading) {
13 | return ;
14 | }
15 |
16 | const restaurant = data?.restaurants[0];
17 |
18 | return (
19 |
20 | {delivery.id.slice(-4)}
21 |
22 | {new Date(delivery.created_at).toLocaleString(undefined, {
23 | hour: "numeric",
24 | minute: "numeric",
25 | year: "numeric",
26 | month: "long",
27 | day: "numeric",
28 | })}
29 |
30 |
31 |
32 | {delivery.delivery_status}
33 |
34 |
35 |
36 | {!delivery.delivered_at ? "ETA " : ""}
37 | {new Date(delivery.delivered_at || delivery.eta).toLocaleTimeString(
38 | undefined,
39 | {
40 | hour: "numeric",
41 | minute: "numeric",
42 | }
43 | )}
44 |
45 | {restaurant.name}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default DeliveryRow;
57 |
--------------------------------------------------------------------------------
/backend/src/admin/components/driver-actions-menu.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | EllipsisHorizontal,
3 | PencilSquare,
4 | Trash,
5 | XCircleSolid,
6 | } from "@medusajs/icons";
7 | import { DropdownMenu, IconButton } from "@medusajs/ui";
8 | import { DriverDTO } from "src/types/delivery/common";
9 |
10 | const DriverActionsMenu = ({ driver }: { driver: DriverDTO }) => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Edit
22 |
23 |
24 |
25 |
26 | Fire {driver.first_name}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default DriverActionsMenu;
34 |
--------------------------------------------------------------------------------
/backend/src/admin/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import { Bike, Pizza, Store } from "lucide-react";
2 |
3 | import React, {
4 | ForwardRefExoticComponent,
5 | PropsWithChildren,
6 | RefAttributes,
7 | } from "react";
8 |
9 | export const PizzaIcon: ForwardRefExoticComponent<
10 | RefAttributes
11 | > = React.forwardRef((props: PropsWithChildren, ref) => (
12 |
13 | ));
14 |
15 | export const BikeIcon: ForwardRefExoticComponent> =
16 | React.forwardRef((props: PropsWithChildren, ref) => (
17 |
18 | ));
19 |
20 | export const StoreIcon: ForwardRefExoticComponent<
21 | RefAttributes
22 | > = React.forwardRef((props: PropsWithChildren, ref) => (
23 |
24 | ));
25 |
--------------------------------------------------------------------------------
/backend/src/admin/hooks/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { DeliveryDTO, DriverDTO } from "../../modules/delivery/types/common";
3 | import { RestaurantDTO } from "../../modules/restaurant/types/common";
4 |
5 | export const useDrivers = (
6 | query?: Record
7 | ): {
8 | data: { drivers: DriverDTO[] } | null;
9 | loading: boolean;
10 | } => {
11 | const [data, setData] = useState(null);
12 | const [loading, setLoading] = useState(true);
13 | const filterQuery = new URLSearchParams(query).toString();
14 |
15 | useEffect(() => {
16 | const fetchDrivers = async () => {
17 | try {
18 | const response = await fetch(
19 | "/admin/drivers" + (query ? `?${filterQuery}` : "")
20 | );
21 | const result = await response.json();
22 | setData(result);
23 | } catch (error) {
24 | console.error("Error fetching the data", error);
25 | } finally {
26 | setLoading(false);
27 | }
28 | };
29 |
30 | fetchDrivers();
31 | }, []);
32 |
33 | return { data, loading };
34 | };
35 |
36 | export const useDeliveries = (
37 | query?: Record
38 | ): {
39 | data: { deliveries: DeliveryDTO[] } | null;
40 | loading: boolean;
41 | } => {
42 | const [data, setData] = useState(null);
43 | const [loading, setLoading] = useState(true);
44 |
45 | const filterQuery = new URLSearchParams(query).toString();
46 |
47 | useEffect(() => {
48 | const fetchDeliveries = async () => {
49 | try {
50 | const response = await fetch(
51 | "/admin/deliveries" + (query ? `?${filterQuery}` : "")
52 | );
53 | const result = await response.json();
54 | setData(result);
55 | } catch (error) {
56 | console.error("Error fetching the data", error);
57 | } finally {
58 | setLoading(false);
59 | }
60 | };
61 |
62 | fetchDeliveries();
63 | }, []);
64 |
65 | return { data, loading };
66 | };
67 |
68 | export const useRestaurants = (
69 | query?: Record
70 | ): {
71 | data: { restaurants: RestaurantDTO[] } | null;
72 | loading: boolean;
73 | } => {
74 | const [data, setData] = useState(null);
75 | const [loading, setLoading] = useState(true);
76 | const filterQuery = new URLSearchParams(query).toString();
77 |
78 | useEffect(() => {
79 | const fetchRestaurants = async () => {
80 | try {
81 | const response = await fetch(
82 | "/admin/restaurants" + (query ? `?${filterQuery}` : "")
83 | );
84 | const result = await response.json();
85 | setData(result);
86 | } catch (error) {
87 | console.error("Error fetching the data", error);
88 | } finally {
89 | setLoading(false);
90 | }
91 | };
92 |
93 | fetchRestaurants();
94 | }, []);
95 |
96 | return { data, loading };
97 | };
98 |
--------------------------------------------------------------------------------
/backend/src/admin/routes/deliveries/page.tsx:
--------------------------------------------------------------------------------
1 | import { defineRouteConfig } from "@medusajs/admin-sdk";
2 | import { Container, Heading, Table, Text } from "@medusajs/ui";
3 | import DeliveryRow from "../../components/delivery-row";
4 | import { PizzaIcon } from "../../components/icons";
5 | import { useDeliveries } from "../../hooks";
6 |
7 | const Deliveries = () => {
8 | const { data, loading } = useDeliveries();
9 |
10 | return (
11 |
12 |
13 | Deliveries
14 |
15 | {loading && Loading... }
16 | {data?.deliveries && (
17 |
18 |
19 |
20 | ID
21 | Created
22 | Status
23 | Delivery
24 | Restaurant
25 | Items
26 | Action
27 |
28 |
29 |
30 | {data.deliveries.map((delivery) => (
31 |
32 | ))}
33 |
34 |
35 | )}
36 |
37 |
38 | );
39 | };
40 |
41 | export const config = defineRouteConfig({
42 | label: "Deliveries",
43 | icon: PizzaIcon,
44 | });
45 |
46 | export default Deliveries;
47 |
--------------------------------------------------------------------------------
/backend/src/admin/routes/drivers/page.tsx:
--------------------------------------------------------------------------------
1 | import { defineRouteConfig } from "@medusajs/admin-sdk";
2 | import { Container, Heading, Table, Text } from "@medusajs/ui";
3 | import { DriverDTO } from "../../../modules/delivery/types/common";
4 | import DriverActionsMenu from "../../components/driver-actions-menu";
5 | import { BikeIcon } from "../../components/icons";
6 | import { useDeliveries, useDrivers } from "../../hooks";
7 |
8 | const Drivers = () => {
9 | const { data, loading } = useDrivers();
10 |
11 | return (
12 |
13 |
14 | Drivers
15 |
16 |
17 |
18 |
19 | Name
20 | Phone
21 | Email
22 | Completed deliveries
23 | Actions
24 |
25 |
26 | {loading && Loading... }
27 | {data?.drivers && (
28 |
29 | {data.drivers.map((driver) => {
30 | return (
31 |
32 |
33 | {driver.avatar_url && (
34 |
38 | )}
39 | {driver.first_name} {driver.last_name}
40 |
41 | {driver.phone}
42 | {driver.email}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | })}
52 |
53 | )}
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | const DeliveryCount = ({ driver }: { driver: DriverDTO }) => {
61 | const { data, loading } = useDeliveries({
62 | driver_id: driver.id,
63 | });
64 |
65 | return {loading ? "Loading..." : data?.deliveries.length} ;
66 | };
67 |
68 | export const config = defineRouteConfig({
69 | label: "Drivers",
70 | icon: BikeIcon,
71 | });
72 |
73 | export default Drivers;
74 |
--------------------------------------------------------------------------------
/backend/src/admin/routes/restaurants/page.tsx:
--------------------------------------------------------------------------------
1 | import { defineRouteConfig } from "@medusajs/admin-sdk";
2 | import { Container, Heading, StatusBadge, Table, Text } from "@medusajs/ui";
3 | import ActionsMenu from "../../components/actions-menu";
4 | import { StoreIcon } from "../../components/icons";
5 | import { useRestaurants } from "../../hooks";
6 |
7 | const Restaurants = () => {
8 | const { data, loading } = useRestaurants();
9 |
10 | return (
11 |
12 |
13 | Restaurants
14 |
15 | {loading && Loading... }
16 |
17 |
18 |
19 | Name
20 | Status
21 | Phone
22 | Email
23 | Actions
24 |
25 |
26 | {data?.restaurants && (
27 |
28 | {data.restaurants.map((restaurant) => (
29 |
30 | {restaurant.name}
31 |
32 |
33 | {restaurant.is_open ? "Open" : "Closed"}
34 |
35 |
36 | {restaurant.phone}
37 | {restaurant.email}
38 |
39 |
40 |
41 |
42 | ))}
43 |
44 | )}
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export const config = defineRouteConfig({
52 | label: "Restaurants",
53 | icon: StoreIcon,
54 | });
55 |
56 | export default Restaurants;
57 |
--------------------------------------------------------------------------------
/backend/src/admin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "module": "Node16"
5 | },
6 | "include": ["."],
7 | "exclude": ["**/*.spec.js"]
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/api/middlewares.ts:
--------------------------------------------------------------------------------
1 | import { defineMiddlewares, authenticate } from "@medusajs/framework";
2 |
3 | const isAllowed = (req, res, next) => {
4 | const { restaurant_id, driver_id } = req.auth_context.app_metadata;
5 |
6 | if (restaurant_id || driver_id) {
7 | const user = {
8 | actor_type: restaurant_id ? "restaurant" : "driver",
9 | user_id: restaurant_id || driver_id,
10 | };
11 |
12 | req.user = user;
13 |
14 | next();
15 | } else {
16 | res.status(403).json({
17 | message:
18 | "Forbidden. Reason: No restaurant_id or driver_id in app_metadata",
19 | });
20 | }
21 | };
22 |
23 | export default defineMiddlewares({
24 | routes: [
25 | {
26 | method: ["GET"],
27 | matcher: "/store/users/me",
28 | middlewares: [
29 | authenticate(["driver", "restaurant"], "bearer"),
30 | isAllowed,
31 | ],
32 | },
33 | {
34 | method: ["POST"],
35 | matcher: "/store/users",
36 | middlewares: [
37 | authenticate(["driver", "restaurant"], "bearer", {
38 | allowUnregistered: true,
39 | }),
40 | ],
41 | },
42 | {
43 | method: ["POST", "DELETE"],
44 | matcher: "/store/restaurants/:id/**",
45 | middlewares: [authenticate(["restaurant", "admin"], "bearer")],
46 | },
47 | {
48 | matcher: "/store/restaurants/:id/admin/**",
49 | middlewares: [authenticate(["restaurant", "admin"], "bearer")],
50 | },
51 | ],
52 | });
53 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/[id]/accept/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { MedusaError } from "@medusajs/utils";
3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common";
4 | import { notifyRestaurantStepId } from "../../../../../workflows/delivery/steps";
5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows";
6 |
7 | const DEFAULT_PROCESSING_TIME = 30 * 60 * 1000; // 30 min
8 |
9 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
10 | const { id } = req.params;
11 |
12 | const eta = new Date(new Date().getTime() + DEFAULT_PROCESSING_TIME);
13 |
14 | const data = {
15 | id,
16 | delivery_status: DeliveryStatus.RESTAURANT_ACCEPTED,
17 | eta,
18 | };
19 |
20 | const updatedDelivery = await updateDeliveryWorkflow(req.scope)
21 | .run({
22 | input: {
23 | data,
24 | stepIdToSucceed: notifyRestaurantStepId,
25 | },
26 | })
27 | .catch((error) => {
28 | console.log(error);
29 | return MedusaError.Types.UNEXPECTED_STATE;
30 | });
31 |
32 | return res.status(200).json({ delivery: updatedDelivery });
33 | }
34 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/[id]/claim/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { MedusaError } from "@medusajs/utils";
3 | import zod from "zod";
4 | import { claimDeliveryWorkflow } from "../../../../../workflows/delivery/workflows";
5 |
6 | const schema = zod.object({
7 | driver_id: zod.string(),
8 | });
9 |
10 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
11 | const validatedBody = schema.parse(req.body);
12 | const deliveryId = req.params.id;
13 |
14 | if (!validatedBody.driver_id) {
15 | return MedusaError.Types.INVALID_DATA;
16 | }
17 |
18 | const claimedDelivery = await claimDeliveryWorkflow(req.scope).run({
19 | input: {
20 | driver_id: validatedBody.driver_id,
21 | delivery_id: deliveryId,
22 | },
23 | });
24 |
25 | return res.status(200).json({ delivery: claimedDelivery });
26 | }
27 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/[id]/complete/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { MedusaError } from "@medusajs/utils";
3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common";
4 | import { awaitDeliveryStepId } from "../../../../../workflows/delivery/steps";
5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows";
6 |
7 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
8 | const { id } = req.params;
9 |
10 | const data = {
11 | id,
12 | delivery_status: DeliveryStatus.DELIVERED,
13 | delivered_at: new Date(),
14 | };
15 |
16 | const updatedDelivery = await updateDeliveryWorkflow(req.scope)
17 | .run({
18 | input: {
19 | data,
20 | stepIdToSucceed: awaitDeliveryStepId,
21 | },
22 | })
23 | .catch((error) => {
24 | return MedusaError.Types.UNEXPECTED_STATE;
25 | });
26 |
27 | return res.status(200).json({ delivery: updatedDelivery });
28 | }
29 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/[id]/decline/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { MedusaError } from "@medusajs/utils";
3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common";
4 | import { notifyRestaurantStepId } from "../../../../../workflows/delivery/steps";
5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows";
6 |
7 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
8 | const { id } = req.params;
9 |
10 | const data = {
11 | id,
12 | delivery_status: DeliveryStatus.RESTAURANT_DECLINED,
13 | };
14 |
15 | const updatedDelivery = await updateDeliveryWorkflow(req.scope)
16 | .run({
17 | input: {
18 | data,
19 | stepIdToFail: notifyRestaurantStepId,
20 | },
21 | })
22 | .catch((error) => {
23 | return MedusaError.Types.UNEXPECTED_STATE;
24 | });
25 |
26 | return res.status(200).json({ delivery: updatedDelivery });
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/[id]/pass/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import zod from "zod";
3 | import { passDeliveryWorkflow } from "../../../../../workflows/delivery/workflows";
4 |
5 | const schema = zod.object({
6 | driver_id: zod.string(),
7 | });
8 |
9 | export async function DELETE(req: MedusaRequest, res: MedusaResponse) {
10 | const validatedBody = schema.parse(req.body);
11 |
12 | const deliveryId = req.params.id;
13 |
14 | await passDeliveryWorkflow(req.scope).run({
15 | input: {
16 | driver_id: validatedBody.driver_id,
17 | delivery_id: deliveryId,
18 | },
19 | });
20 |
21 | return res.status(200).json({ message: "Driver declined delivery" });
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/[id]/pick-up/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { MedusaError } from "@medusajs/utils";
3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common";
4 | import { awaitPickUpStepId } from "../../../../../workflows/delivery/steps";
5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows";
6 |
7 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
8 | const { id } = req.params;
9 |
10 | const data = {
11 | id,
12 | delivery_status: DeliveryStatus.IN_TRANSIT,
13 | };
14 |
15 | const updatedDelivery = await updateDeliveryWorkflow(req.scope)
16 | .run({
17 | input: {
18 | data,
19 | stepIdToSucceed: awaitPickUpStepId,
20 | },
21 | })
22 | .catch((error) => {
23 | console.log(error);
24 | return MedusaError.Types.UNEXPECTED_STATE;
25 | });
26 |
27 | return res.status(200).json({ delivery: updatedDelivery });
28 | }
29 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/[id]/prepare/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { MedusaError } from "@medusajs/utils";
3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common";
4 | import { awaitStartPreparationStepId } from "../../../../../workflows/delivery/steps";
5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows";
6 |
7 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
8 | const { id } = req.params;
9 |
10 | const data = {
11 | id,
12 | delivery_status: DeliveryStatus.RESTAURANT_PREPARING,
13 | };
14 |
15 | const updatedDelivery = await updateDeliveryWorkflow(req.scope)
16 | .run({
17 | input: {
18 | data,
19 | stepIdToSucceed: awaitStartPreparationStepId,
20 | },
21 | })
22 | .catch((error) => {
23 | return MedusaError.Types.UNEXPECTED_STATE;
24 | });
25 |
26 | return res.status(200).json({ delivery: updatedDelivery });
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/[id]/ready/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { MedusaError } from "@medusajs/utils";
3 | import { DeliveryStatus } from "../../../../../modules/delivery/types/common";
4 | import { awaitPreparationStepId } from "../../../../../workflows/delivery/steps";
5 | import { updateDeliveryWorkflow } from "../../../../../workflows/delivery/workflows";
6 |
7 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
8 | const { id } = req.params;
9 |
10 | const data = {
11 | id,
12 | delivery_status: DeliveryStatus.READY_FOR_PICKUP,
13 | };
14 |
15 | const updatedDelivery = await updateDeliveryWorkflow(req.scope)
16 | .run({
17 | input: {
18 | data,
19 | stepIdToSucceed: awaitPreparationStepId,
20 | },
21 | })
22 | .catch((error) => {
23 | return MedusaError.Types.UNEXPECTED_STATE;
24 | });
25 |
26 | return res.status(200).json({ delivery: updatedDelivery });
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { ContainerRegistrationKeys } from "@medusajs/utils";
3 | import zod from "zod";
4 | import { DeliveryStatus } from "../../../../modules/delivery/types/common";
5 | import { UpdateDeliveryDTO } from "../../../../modules/delivery/types/mutations";
6 | import { updateDeliveryWorkflow } from "../../../../workflows/delivery/workflows";
7 |
8 | const schema = zod.object({
9 | driver_id: zod.string().optional(),
10 | notified_driver_ids: zod.array(zod.string()).optional(),
11 | order_id: zod.string().optional(),
12 | delivery_status: zod.nativeEnum(DeliveryStatus).optional(),
13 | eta: zod.date().optional(),
14 | });
15 |
16 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
17 | const validatedBody = schema.parse(req.body);
18 |
19 | const deliveryId = req.params.id;
20 |
21 | const updateData: UpdateDeliveryDTO = {
22 | id: deliveryId,
23 | ...validatedBody,
24 | };
25 |
26 | if (validatedBody.delivery_status === "delivered") {
27 | updateData.delivered_at = new Date();
28 | }
29 |
30 | try {
31 | const delivery = await updateDeliveryWorkflow(req.scope).run({
32 | input: {
33 | data: updateData,
34 | },
35 | });
36 |
37 | return res.status(200).json({ delivery });
38 | } catch (error) {
39 | return res.status(500).json({ message: error.message });
40 | }
41 | }
42 |
43 | export async function GET(req: MedusaRequest, res: MedusaResponse) {
44 | const deliveryId = req.params.id;
45 |
46 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY);
47 |
48 | const deliveryQuery = {
49 | entity: "delivery",
50 | fields: [
51 | "*",
52 | "cart.*",
53 | "cart.items.*",
54 | "order.*",
55 | "order.items.*",
56 | "restaurant.*",
57 | ],
58 | filters: {
59 | id: deliveryId,
60 | },
61 | };
62 |
63 | const {
64 | data: [delivery],
65 | } = await query.graph(deliveryQuery);
66 |
67 | if (!delivery) {
68 | return res.status(404).json({ message: "Delivery not found" });
69 | }
70 |
71 | try {
72 | return res.status(200).json({ delivery });
73 | } catch (error) {
74 | return res.status(500).json({ message: error.message });
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/backend/src/api/store/deliveries/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { ContainerRegistrationKeys } from "@medusajs/utils";
3 | import zod from "zod";
4 | import {
5 | createDeliveryWorkflow,
6 | handleDeliveryWorkflow,
7 | } from "../../../workflows/delivery/workflows";
8 |
9 | const schema = zod.object({
10 | cart_id: zod.string().startsWith("cart_"),
11 | restaurant_id: zod.string().startsWith("res_"),
12 | });
13 |
14 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
15 | const validatedBody = schema.parse(req.body);
16 |
17 | const { result: delivery } = await createDeliveryWorkflow(req.scope).run({
18 | input: {
19 | cart_id: validatedBody.cart_id,
20 | restaurant_id: validatedBody.restaurant_id,
21 | },
22 | });
23 |
24 | const { transaction } = await handleDeliveryWorkflow(req.scope).run({
25 | input: {
26 | delivery_id: delivery.id,
27 | },
28 | });
29 |
30 | return res
31 | .status(200)
32 | .json({ message: "Delivery created", delivery, transaction });
33 | }
34 |
35 | export async function GET(req: MedusaRequest, res: MedusaResponse) {
36 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY);
37 |
38 | const filters = {} as Record;
39 | let take = parseInt(req.query.take as string) || null;
40 | let skip = parseInt(req.query.skip as string) || 0;
41 |
42 | for (const key in req.query) {
43 | if (["take", "skip"].includes(key)) continue;
44 |
45 | filters[key] = req.query[key];
46 | }
47 | const deliveryQuery = {
48 | entity: "delivery",
49 | fields: ["*", "cart.*", "cart.items.*", "order.*", "order.items.*"],
50 | filters,
51 | pagination: {
52 | take,
53 | skip,
54 | },
55 | };
56 |
57 | const { data: deliveries } = await query.graph(deliveryQuery);
58 |
59 | if (filters.hasOwnProperty("driver_id")) {
60 | const driverQuery = {
61 | entity: "delivery_driver",
62 | fields: ["driver_id", "delivery_id"],
63 | filters: {
64 | deleted_at: null,
65 | driver_id: filters["driver_id"],
66 | },
67 | };
68 |
69 | const { data: availableDeliveriesIds } = await query.graph(driverQuery);
70 |
71 | const availableDeliveriesQuery = {
72 | entity: "delivery",
73 | fields: ["*", "cart.*", "cart.items.*", "order.*", "order.items.*"],
74 | filters: {
75 | id: availableDeliveriesIds.map((d) => d.delivery_id),
76 | },
77 | };
78 |
79 | const { data: availableDeliveries } = await query.graph(
80 | availableDeliveriesQuery
81 | );
82 |
83 | deliveries.push(...availableDeliveries);
84 | }
85 |
86 | return res.status(200).json({ deliveries });
87 | }
88 |
--------------------------------------------------------------------------------
/backend/src/api/store/drivers/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils";
3 | import zod from "zod";
4 | import { DELIVERY_MODULE } from "../../../../modules/delivery";
5 |
6 | const schema = zod.object({
7 | first_name: zod.string().optional(),
8 | last_name: zod.string().optional(),
9 | email: zod.string().optional(),
10 | phone: zod.string().optional(),
11 | avatar_url: zod.string().optional(),
12 | });
13 |
14 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
15 | const validatedBody = schema.parse(req.body);
16 |
17 | if (!validatedBody) {
18 | return MedusaError.Types.INVALID_DATA;
19 | }
20 |
21 | const deliveryModuleService = req.scope.resolve(DELIVERY_MODULE);
22 |
23 | const driverId = req.params.id;
24 |
25 | const data = {
26 | id: driverId,
27 | ...validatedBody,
28 | };
29 |
30 | if (!driverId) {
31 | return MedusaError.Types.NOT_FOUND;
32 | }
33 |
34 | const driver = await deliveryModuleService.updateDrivers(data);
35 |
36 | return res.status(200).json({ driver });
37 | }
38 |
39 | export async function GET(req: MedusaRequest, res: MedusaResponse) {
40 | const driverId = req.params.id;
41 |
42 | if (!driverId) {
43 | return MedusaError.Types.INVALID_DATA;
44 | }
45 |
46 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY);
47 |
48 | const driverQuery = {
49 | entity: "driver",
50 | filters: {
51 | id: driverId,
52 | },
53 | fields: ["id", "first_name", "last_name", "email", "phone", "avatar_url"],
54 | };
55 |
56 | const {
57 | data: [driver],
58 | } = await query.graph(driverQuery);
59 |
60 | return res.status(200).json({ driver });
61 | }
62 |
63 | export async function DELETE(req: MedusaRequest, res: MedusaResponse) {
64 | const driverId = req.params.id;
65 |
66 | if (!driverId) {
67 | return MedusaError.Types.INVALID_DATA;
68 | }
69 |
70 | const deliveryModuleService = req.scope.resolve(DELIVERY_MODULE);
71 |
72 | await deliveryModuleService.deleteDrivers(driverId);
73 |
74 | return res.status(200).json({ message: "Driver deleted" });
75 | }
76 |
--------------------------------------------------------------------------------
/backend/src/api/store/drivers/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { ContainerRegistrationKeys } from "@medusajs/utils";
3 |
4 | export async function GET(req: MedusaRequest, res: MedusaResponse) {
5 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY);
6 |
7 | try {
8 | const driverQuery = {
9 | entity: "driver",
10 | fields: ["*"],
11 | pagination: {
12 | take: null,
13 | skip: 0,
14 | },
15 | };
16 |
17 | const { data: drivers } = await query.graph(driverQuery);
18 |
19 | return res.status(200).json({ drivers });
20 | } catch (error) {
21 | return res.status(500).json({ message: error.message });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/backend/src/api/store/restaurants/[id]/admins/route.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AuthenticatedMedusaRequest,
3 | MedusaRequest,
4 | MedusaResponse,
5 | } from "@medusajs/framework";
6 | import {
7 | ContainerRegistrationKeys,
8 | MedusaError,
9 | remoteQueryObjectFromString,
10 | } from "@medusajs/utils";
11 | import zod from "zod";
12 | import { createUserWorkflow } from "../../../../../workflows/user/workflows/create-user";
13 | import { RESTAURANT_MODULE } from "../../../../../modules/restaurant";
14 |
15 | const schema = zod
16 | .object({
17 | email: zod.string().email(),
18 | first_name: zod.string().optional(),
19 | last_name: zod.string().optional(),
20 | })
21 | .required({ email: true });
22 |
23 | export const POST = async (
24 | req: AuthenticatedMedusaRequest,
25 | res: MedusaResponse
26 | ) => {
27 | const authIdentityId = req.auth_context.auth_identity_id;
28 | const restaurantId = req.params.id;
29 |
30 | const validatedBody = schema.parse(req.body) as {
31 | email: string;
32 | first_name: string;
33 | last_name: string;
34 | };
35 |
36 | const { result, errors } = await createUserWorkflow(req.scope).run({
37 | input: {
38 | user: {
39 | ...validatedBody,
40 | actor_type: "restaurant",
41 | restaurant_id: restaurantId,
42 | },
43 | auth_identity_id: authIdentityId,
44 | },
45 | throwOnError: false,
46 | });
47 |
48 | if (Array.isArray(errors) && errors[0]) {
49 | throw errors[0].error;
50 | }
51 |
52 | res.status(201).json({ user: result });
53 | };
54 |
55 | export async function GET(req: MedusaRequest, res: MedusaResponse) {
56 | const restaurantId = req.params.id;
57 |
58 | if (!restaurantId) {
59 | return MedusaError.Types.NOT_FOUND, "Restaurant not found";
60 | }
61 |
62 | const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY);
63 |
64 | const restaurantAdminsQuery = remoteQueryObjectFromString({
65 | entryPoint: "restaurant_admin",
66 | fields: ["id", "email", "first_name", "last_name"],
67 | filters: {
68 | restaurant_id: restaurantId,
69 | },
70 | });
71 |
72 | const restaurantAdmins = await remoteQuery(restaurantAdminsQuery);
73 |
74 | return res.status(200).json({ restaurant_admins: restaurantAdmins });
75 | }
76 |
77 | export async function DELETE(req: MedusaRequest, res: MedusaResponse) {
78 | const restaurantId = req.params.id;
79 | const adminId = req.params.adminId;
80 |
81 | if (!restaurantId || !adminId) {
82 | return MedusaError.Types.INVALID_DATA;
83 | }
84 |
85 | const restaurantModuleService = req.scope.resolve(RESTAURANT_MODULE);
86 |
87 | await restaurantModuleService.deleteRestaurantAdmins(adminId);
88 |
89 | return res.status(200).json({ message: "Admin deleted" });
90 | }
91 |
--------------------------------------------------------------------------------
/backend/src/api/store/restaurants/[id]/products/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils";
3 | import { deleteProductsWorkflow } from "@medusajs/core-flows";
4 | import { createRestaurantProductsWorkflow } from "../../../../../workflows/restaurant/workflows";
5 | import { AdminCreateProduct } from "@medusajs/types";
6 | import { z } from "zod";
7 |
8 | const createSchema = z.object({
9 | products: z.array(z.custom()),
10 | });
11 |
12 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
13 | const validatedBody = createSchema.parse(req.body);
14 |
15 | const { result: restaurantProducts } = await createRestaurantProductsWorkflow(
16 | req.scope
17 | ).run({
18 | input: {
19 | products: validatedBody.products as any[],
20 | restaurant_id: req.params.id,
21 | },
22 | });
23 |
24 | // Return the product
25 | return res.status(200).json({ restaurant_products: restaurantProducts });
26 | }
27 |
28 | export async function GET(req: MedusaRequest, res: MedusaResponse) {
29 | const restaurantId = req.params.id;
30 |
31 | if (!restaurantId) {
32 | return MedusaError.Types.NOT_FOUND;
33 | }
34 |
35 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY);
36 |
37 | const restaurantProductsQuery = {
38 | entity: "product",
39 | filters: {
40 | restaurant: restaurantId,
41 | },
42 | fields: [
43 | "id",
44 | "title",
45 | "description",
46 | "thumbnail",
47 | "categories.*",
48 | "categories.id",
49 | "categories.name",
50 | "variants.*",
51 | "variants.id",
52 | "variants.price_set.*",
53 | "variants.price_set.id",
54 | "restaurant.*",
55 | ],
56 | };
57 |
58 | const { data: restaurantProducts } = await query.graph(
59 | restaurantProductsQuery
60 | );
61 |
62 | return res.status(200).json({ restaurant_products: restaurantProducts });
63 | }
64 |
65 | const deleteSchema = z.object({
66 | product_ids: z.string().array(),
67 | });
68 |
69 | export async function DELETE(req: MedusaRequest, res: MedusaResponse) {
70 | const validatedBody = deleteSchema.parse(req.body);
71 |
72 | await deleteProductsWorkflow(req.scope).run({
73 | input: {
74 | ids: validatedBody.product_ids,
75 | },
76 | });
77 |
78 | return res.status(200).json({ success: true });
79 | }
80 |
--------------------------------------------------------------------------------
/backend/src/api/store/restaurants/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { QueryContext } from "@medusajs/framework/utils";
3 | import { ContainerRegistrationKeys } from "@medusajs/utils";
4 | import { query } from "express";
5 |
6 | export async function GET(req: MedusaRequest, res: MedusaResponse) {
7 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY);
8 |
9 | const { currency_code = "eur", ...reqQuery } = req.query;
10 |
11 | const restaurantId = req.params.id;
12 |
13 | const restaurantQuery = {
14 | entity: "restaurant",
15 | fields: [
16 | "*",
17 | "products.*",
18 | "products.categories.*",
19 | "products.categories.*",
20 | "products.variants.*",
21 | "products.variants.calculated_price.*",
22 | "deliveries.*",
23 | "deliveries.cart.*",
24 | "deliveries.cart.items.*",
25 | "deliveries.order.*",
26 | "deliveries.order.items.*",
27 | ],
28 | filters: {
29 | id: restaurantId,
30 | },
31 | context: {
32 | products: {
33 | variants: {
34 | calculated_price: QueryContext({
35 | currency_code,
36 | }),
37 | },
38 | },
39 | },
40 | };
41 |
42 | const {
43 | data: [restaurant],
44 | } = await query.graph(restaurantQuery);
45 |
46 | return res.status(200).json({ restaurant });
47 | }
48 |
--------------------------------------------------------------------------------
/backend/src/api/store/restaurants/[id]/status/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import zod from "zod";
3 | import { RESTAURANT_MODULE } from "../../../../../modules/restaurant";
4 |
5 | const schema = zod.object({
6 | is_open: zod.boolean(),
7 | });
8 |
9 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
10 | const { id } = req.params;
11 | const { is_open } = schema.parse(req.body);
12 |
13 | const restaurantService = req.scope.resolve(RESTAURANT_MODULE);
14 |
15 | try {
16 | const restaurant = await restaurantService.updateRestaurants({
17 | id,
18 | is_open,
19 | });
20 | res.status(200).json({ restaurant });
21 | } catch (error) {
22 | res.status(400).json({ message: error.message });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/src/api/store/restaurants/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from "@medusajs/framework";
2 | import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils";
3 | import zod from "zod";
4 | import { CreateRestaurantDTO } from "../../../modules/restaurant/types/mutations";
5 | import { createRestaurantWorkflow } from "../../../workflows/restaurant/workflows";
6 | import { QueryContext } from "@medusajs/framework/utils";
7 |
8 | const schema = zod.object({
9 | name: zod.string(),
10 | handle: zod.string(),
11 | address: zod.string(),
12 | phone: zod.string(),
13 | email: zod.string(),
14 | image_url: zod.string().optional(),
15 | });
16 |
17 | export async function POST(req: MedusaRequest, res: MedusaResponse) {
18 | const validatedBody = schema.parse(req.body) as CreateRestaurantDTO;
19 |
20 | if (!validatedBody) {
21 | return MedusaError.Types.INVALID_DATA;
22 | }
23 |
24 | const { result: restaurant } = await createRestaurantWorkflow(req.scope).run({
25 | input: {
26 | restaurant: validatedBody,
27 | },
28 | });
29 |
30 | return res.status(200).json({ message: "Restaurant created", restaurant });
31 | }
32 |
33 | export async function GET(req: MedusaRequest, res: MedusaResponse) {
34 | const { currency_code = "eur", ...queryFilters } = req.query;
35 |
36 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY);
37 |
38 | const restaurantsQuery = {
39 | entity: "restaurant",
40 | fields: [
41 | "id",
42 | "handle",
43 | "name",
44 | "address",
45 | "phone",
46 | "email",
47 | "image_url",
48 | "is_open",
49 | "products.*",
50 | "products.categories.*",
51 | "products.variants.*",
52 | "products.variants.calculated_price.*",
53 | ],
54 | filters: queryFilters,
55 | context: {
56 | products: {
57 | variants: {
58 | calculated_price: QueryContext({
59 | currency_code,
60 | }),
61 | },
62 | },
63 | },
64 | };
65 |
66 | const { data: restaurants } = await query.graph(restaurantsQuery);
67 |
68 | return res.status(200).json({ restaurants });
69 | }
70 |
--------------------------------------------------------------------------------
/backend/src/api/store/users/me/route.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AuthenticatedMedusaRequest,
3 | MedusaResponse,
4 | } from "@medusajs/framework";
5 | import { RESTAURANT_MODULE } from "../../../../modules/restaurant";
6 | import { DELIVERY_MODULE } from "../../../../modules/delivery";
7 |
8 | export const GET = async (
9 | req: AuthenticatedMedusaRequest,
10 | res: MedusaResponse
11 | ) => {
12 | const { user_id, actor_type } = req.user as {
13 | user_id: string;
14 | actor_type: "restaurant" | "driver";
15 | };
16 |
17 | if (actor_type === "restaurant") {
18 | const service = req.scope.resolve(RESTAURANT_MODULE);
19 | const user = await service.retrieveRestaurantAdmin(user_id);
20 | return res.json({ user });
21 | }
22 |
23 | if (actor_type === "driver") {
24 | const service = req.scope.resolve(DELIVERY_MODULE);
25 | const user = await service.retrieveDriver(user_id);
26 | return res.json({ user });
27 | }
28 |
29 | return res.status(404).json({ message: "User not found" });
30 | };
31 |
--------------------------------------------------------------------------------
/backend/src/api/store/users/route.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AuthenticatedMedusaRequest,
3 | MedusaResponse,
4 | } from "@medusajs/framework";
5 | import zod from "zod";
6 |
7 | import {
8 | CreateDriverInput,
9 | createUserWorkflow,
10 | } from "../../../workflows/user/workflows/create-user";
11 |
12 | const schema = zod
13 | .object({
14 | email: zod.string().email(),
15 | first_name: zod.string(),
16 | last_name: zod.string(),
17 | phone: zod.string(),
18 | avatar_url: zod.string().optional(),
19 | restaurant_id: zod.string().optional(),
20 | actor_type: zod.ZodEnum.create(["restaurant", "driver"]),
21 | })
22 | .required({
23 | email: true,
24 | first_name: true,
25 | last_name: true,
26 | phone: true,
27 | actor_type: true,
28 | });
29 |
30 | export const POST = async (
31 | req: AuthenticatedMedusaRequest,
32 | res: MedusaResponse
33 | ) => {
34 | const { auth_identity_id } = req.auth_context;
35 |
36 | const validatedBody = schema.parse(req.body) as CreateDriverInput & {
37 | actor_type: "restaurant" | "driver";
38 | };
39 |
40 | const { result, errors } = await createUserWorkflow(req.scope).run({
41 | input: {
42 | user: validatedBody,
43 | auth_identity_id,
44 | },
45 | throwOnError: false,
46 | });
47 |
48 | if (Array.isArray(errors) && errors[0]) {
49 | throw errors[0].error;
50 | }
51 |
52 | res.status(201).json({ user: result });
53 | };
54 |
--------------------------------------------------------------------------------
/backend/src/api/types.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedMedusaRequest } from "@medusajs/framework";
2 |
3 | export interface AuthUserScopedMedusaRequest
4 | extends AuthenticatedMedusaRequest {
5 | auth_user_id: string;
6 | }
7 |
--------------------------------------------------------------------------------
/backend/src/jobs/README.md:
--------------------------------------------------------------------------------
1 | # Custom scheduled jobs
2 |
3 | You may define custom scheduled jobs (cron jobs) by creating files in the `/jobs` directory.
4 |
5 | ```ts
6 | import {
7 | ProductService,
8 | ScheduledJobArgs,
9 | ScheduledJobConfig,
10 | } from "@medusajs/medusa"
11 |
12 | export default async function myCustomJob({ container }: ScheduledJobArgs) {
13 | const productService: ProductService = container.resolve("productService")
14 |
15 | const products = await productService.listAndCount()
16 |
17 | // Do something with the products
18 | }
19 |
20 | export const config: ScheduledJobConfig = {
21 | name: "daily-product-report",
22 | schedule: "0 0 * * *", // Every day at midnight
23 | }
24 | ```
25 |
26 | A scheduled job is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when the job is scheduled. The `config` is an object which defines the name of the job, the schedule, and an optional data object.
27 |
28 | The `handler` is a function which takes one parameter, an `object` of type `ScheduledJobArgs` with the following properties:
29 |
30 | - `container` - a `MedusaContainer` instance which can be used to resolve services.
31 | - `data` - an `object` containing data passed to the job when it was scheduled. This object is passed in the `config` object.
32 | - `pluginOptions` - an `object` containing plugin options, if the job is defined in a plugin.
33 |
--------------------------------------------------------------------------------
/backend/src/links/delivery-cart.ts:
--------------------------------------------------------------------------------
1 | import DeliveryModule from "../modules/delivery";
2 | import CartModule from "@medusajs/cart";
3 | import { defineLink } from "@medusajs/utils";
4 |
5 | export default defineLink(
6 | DeliveryModule.linkable.delivery,
7 | CartModule.linkable.cart
8 | );
9 |
--------------------------------------------------------------------------------
/backend/src/links/delivery-order.ts:
--------------------------------------------------------------------------------
1 | import DeliveryModule from "../modules/delivery";
2 | import OrderModule from "@medusajs/order";
3 | import { defineLink } from "@medusajs/utils";
4 |
5 | export default defineLink(
6 | DeliveryModule.linkable.delivery,
7 | OrderModule.linkable.order
8 | );
9 |
--------------------------------------------------------------------------------
/backend/src/links/restaurant-deliveries.ts:
--------------------------------------------------------------------------------
1 | import RestaurantModule from "../modules/restaurant";
2 | import DeliveryModule from "../modules/delivery";
3 | import { defineLink } from "@medusajs/utils";
4 |
5 | export default defineLink(
6 | RestaurantModule.linkable.restaurant,
7 | {
8 | linkable: DeliveryModule.linkable.delivery,
9 | isList: true,
10 | }
11 | );
12 |
--------------------------------------------------------------------------------
/backend/src/links/restaurant-products.ts:
--------------------------------------------------------------------------------
1 | import RestaurantModule from "../modules/restaurant";
2 | import ProductModule from "@medusajs/product";
3 | import { defineLink } from "@medusajs/utils";
4 |
5 | export default defineLink(
6 | RestaurantModule.linkable.restaurant,
7 | {
8 | linkable: ProductModule.linkable.product,
9 | isList: true,
10 | }
11 | );
12 |
--------------------------------------------------------------------------------
/backend/src/loaders/README.md:
--------------------------------------------------------------------------------
1 | # Custom loader
2 |
3 | A loader is a function executed when the Medusa application starts.
4 |
5 | To create a loader in your Medusa application, create a TypeScript or JavaScript file under the `src/loaders` directory that default exports a function.
6 |
7 | ```ts
8 | export default function () {
9 | console.log(
10 | "[HELLO LOADER] Just started the Medusa application!"
11 | )
12 | }
13 | ```
14 |
15 | ## Loader Parameters
16 |
17 | A loader receives the Medusa container as a first parameter, and the Medusa configuration as a second parameter.
18 |
19 | ```ts
20 | import { MedusaContainer } from "@medusajs/medusa"
21 | import { ConfigModule } from "@medusajs/types"
22 |
23 | export default async function (
24 | container: MedusaContainer,
25 | config: ConfigModule
26 | ) {
27 | console.log(`You have ${
28 | Object.values(config.modules || {}).length
29 | } modules!`)
30 | }
31 | ```
32 |
--------------------------------------------------------------------------------
/backend/src/modules/delivery/index.ts:
--------------------------------------------------------------------------------
1 | import Service from "./service";
2 | import { Module } from "@medusajs/utils";
3 |
4 | export const DELIVERY_MODULE = "deliveryModuleService";
5 |
6 | export default Module(DELIVERY_MODULE, {
7 | service: Service,
8 | });
9 |
--------------------------------------------------------------------------------
/backend/src/modules/delivery/migrations/Migration20240822141125.ts:
--------------------------------------------------------------------------------
1 | import { Migration } from '@mikro-orm/migrations';
2 |
3 | export class Migration20240822141125 extends Migration {
4 |
5 | async up(): Promise {
6 | this.addSql('create table if not exists "delivery" ("id" text not null, "transaction_id" text null, "driver_id" text null, "delivery_status" text check ("delivery_status" in (\'pending\', \'restaurant_declined\', \'restaurant_accepted\', \'pickup_claimed\', \'restaurant_preparing\', \'ready_for_pickup\', \'in_transit\', \'delivered\')) not null default \'pending\', "eta" timestamptz null, "delivered_at" timestamptz null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "delivery_pkey" primary key ("id"));');
7 |
8 | this.addSql('create table if not exists "driver" ("id" text not null, "first_name" text not null, "last_name" text not null, "email" text not null, "phone" text not null, "avatar_url" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "driver_pkey" primary key ("id"));');
9 |
10 | this.addSql('create table if not exists "delivery_driver" ("id" text not null, "delivery_id" text not null, "driver_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "delivery_driver_pkey" primary key ("id"));');
11 | this.addSql('CREATE INDEX IF NOT EXISTS "IDX_delivery_driver_delivery_id" ON "delivery_driver" (delivery_id) WHERE deleted_at IS NULL;');
12 | this.addSql('CREATE INDEX IF NOT EXISTS "IDX_delivery_driver_driver_id" ON "delivery_driver" (driver_id) WHERE deleted_at IS NULL;');
13 |
14 | this.addSql('alter table if exists "delivery_driver" add constraint "delivery_driver_delivery_id_foreign" foreign key ("delivery_id") references "delivery" ("id") on update cascade;');
15 | this.addSql('alter table if exists "delivery_driver" add constraint "delivery_driver_driver_id_foreign" foreign key ("driver_id") references "driver" ("id") on update cascade;');
16 | }
17 |
18 | async down(): Promise {
19 | this.addSql('alter table if exists "delivery_driver" drop constraint if exists "delivery_driver_delivery_id_foreign";');
20 |
21 | this.addSql('alter table if exists "delivery_driver" drop constraint if exists "delivery_driver_driver_id_foreign";');
22 |
23 | this.addSql('drop table if exists "delivery" cascade;');
24 |
25 | this.addSql('drop table if exists "driver" cascade;');
26 |
27 | this.addSql('drop table if exists "delivery_driver" cascade;');
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/backend/src/modules/delivery/models/delivery-driver.ts:
--------------------------------------------------------------------------------
1 | import { model } from "@medusajs/utils";
2 | import { Delivery } from "./delivery";
3 | import { Driver } from "./driver";
4 |
5 | export const DeliveryDriver = model.define("delivery_driver", {
6 | id: model
7 | .id({
8 | prefix: "deldrv",
9 | })
10 | .primaryKey(),
11 | delivery: model.belongsTo(() => Delivery, {
12 | mappedBy: "deliveryDriver"
13 | }),
14 | driver: model.belongsTo(() => Driver, {
15 | mappedBy: "deliveryDriver"
16 | })
17 | });
18 |
--------------------------------------------------------------------------------
/backend/src/modules/delivery/models/delivery.ts:
--------------------------------------------------------------------------------
1 | import { model } from "@medusajs/utils";
2 | import { DeliveryStatus } from "../types/common";
3 | import { DeliveryDriver } from "./delivery-driver";
4 |
5 | export const Delivery = model.define("delivery", {
6 | id: model
7 | .id({
8 | prefix: "del",
9 | })
10 | .primaryKey(),
11 | transaction_id: model.text().nullable(),
12 | driver_id: model.text().nullable(),
13 | delivery_status: model.enum(DeliveryStatus).default(DeliveryStatus.PENDING),
14 | eta: model.dateTime().nullable(),
15 | delivered_at: model.dateTime().nullable(),
16 | deliveryDriver: model.hasMany(() => DeliveryDriver, {
17 | mappedBy: "delivery"
18 | })
19 | });
20 |
--------------------------------------------------------------------------------
/backend/src/modules/delivery/models/driver.ts:
--------------------------------------------------------------------------------
1 | import { model } from "@medusajs/utils";
2 | import { DeliveryDriver } from "./delivery-driver";
3 |
4 | export const Driver = model.define("driver", {
5 | id: model
6 | .id({
7 | prefix: "drv",
8 | })
9 | .primaryKey(),
10 | first_name: model.text(),
11 | last_name: model.text(),
12 | email: model.text(),
13 | phone: model.text(),
14 | avatar_url: model.text().nullable(),
15 | deliveryDriver: model.hasMany(() => DeliveryDriver, {
16 | mappedBy: "driver"
17 | })
18 | });
19 |
--------------------------------------------------------------------------------
/backend/src/modules/delivery/models/index.ts:
--------------------------------------------------------------------------------
1 | export { Delivery } from "./delivery";
2 | export { Driver } from "./driver";
3 | export { DeliveryDriver } from "./delivery-driver";
4 |
--------------------------------------------------------------------------------
/backend/src/modules/delivery/service.ts:
--------------------------------------------------------------------------------
1 | import { MedusaService } from "@medusajs/utils";
2 | import { Delivery, DeliveryDriver, Driver } from "./models";
3 |
4 | class DeliveryModuleService extends MedusaService({
5 | Delivery,
6 | Driver,
7 | DeliveryDriver,
8 | }) {}
9 |
10 | export default DeliveryModuleService;
11 |
--------------------------------------------------------------------------------
/backend/src/modules/delivery/types/common.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CartLineItemDTO,
3 | OrderLineItemDTO,
4 | CartDTO,
5 | OrderDTO,
6 | } from "@medusajs/types";
7 | import { RestaurantDTO } from "../../restaurant/types/common";
8 | import DeliveryModuleService from "../service";
9 |
10 | export enum DeliveryStatus {
11 | PENDING = "pending",
12 | RESTAURANT_DECLINED = "restaurant_declined",
13 | RESTAURANT_ACCEPTED = "restaurant_accepted",
14 | PICKUP_CLAIMED = "pickup_claimed",
15 | RESTAURANT_PREPARING = "restaurant_preparing",
16 | READY_FOR_PICKUP = "ready_for_pickup",
17 | IN_TRANSIT = "in_transit",
18 | DELIVERED = "delivered",
19 | }
20 |
21 | export interface DeliveryDTO {
22 | id: string;
23 | transaction_id: string;
24 | driver_id?: string;
25 | delivered_at?: Date;
26 | delivery_status: DeliveryStatus;
27 | created_at: Date;
28 | updated_at: Date;
29 | eta?: Date;
30 | items: DeliveryItemDTO[];
31 | cart?: CartDTO;
32 | order?: OrderDTO;
33 | restaurant: RestaurantDTO;
34 | }
35 |
36 | export type DeliveryItemDTO = (CartLineItemDTO | OrderLineItemDTO) & {
37 | quantity: number;
38 | };
39 |
40 | export interface DriverDTO {
41 | id: string;
42 | first_name: string;
43 | last_name: string;
44 | email: string;
45 | phone: string;
46 | avatar_url?: string;
47 | created_at: Date;
48 | updated_at: Date;
49 | }
50 |
51 | export interface DeliveryDriverDTO {
52 | id: string;
53 | delivery_id: string;
54 | driver_id: string;
55 | }
56 |
57 | declare module "@medusajs/types" {
58 | export interface ModuleImplementations {
59 | deliveryModuleService: DeliveryModuleService;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/backend/src/modules/delivery/types/mutations.ts:
--------------------------------------------------------------------------------
1 | import { DeliveryDTO, DriverDTO } from "./common";
2 |
3 | export interface CreateDeliveryDTO {
4 | cart_id: string;
5 | }
6 |
7 | export interface UpdateDeliveryDTO extends Partial {
8 | id: string;
9 | }
10 |
11 | export interface CreateDriverDTO {
12 | first_name: string;
13 | last_name: string;
14 | email: string;
15 | phone: string;
16 | avatar_url?: string;
17 | }
18 |
19 | export interface UpdateDriverDTO extends Partial {
20 | id: string;
21 | }
22 |
23 | export interface CreateDeliveryDriverDTO {
24 | delivery_id: string;
25 | driver_id: string;
26 | }
27 |
--------------------------------------------------------------------------------
/backend/src/modules/restaurant/index.ts:
--------------------------------------------------------------------------------
1 | import Service from "./service";
2 | import { Module } from "@medusajs/utils";
3 |
4 | export const RESTAURANT_MODULE = "restaurantModuleService";
5 |
6 | export default Module(RESTAURANT_MODULE, {
7 | service: Service,
8 | });
9 |
--------------------------------------------------------------------------------
/backend/src/modules/restaurant/migrations/Migration20240925143313.ts:
--------------------------------------------------------------------------------
1 | import { Migration } from '@mikro-orm/migrations';
2 |
3 | export class Migration20240925143313 extends Migration {
4 |
5 | async up(): Promise {
6 | this.addSql('create table if not exists "restaurant" ("id" text not null, "handle" text not null, "is_open" boolean not null default false, "name" text not null, "description" text null, "phone" text not null, "email" text not null, "address" text not null, "image_url" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "restaurant_pkey" primary key ("id"));');
7 |
8 | this.addSql('create table if not exists "restaurant_admin" ("id" text not null, "first_name" text not null, "last_name" text not null, "email" text not null, "avatar_url" text null, "restaurant_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "restaurant_admin_pkey" primary key ("id"));');
9 | this.addSql('CREATE INDEX IF NOT EXISTS "IDX_restaurant_admin_restaurant_id" ON "restaurant_admin" (restaurant_id) WHERE deleted_at IS NULL;');
10 |
11 | this.addSql('alter table if exists "restaurant_admin" add constraint "restaurant_admin_restaurant_id_foreign" foreign key ("restaurant_id") references "restaurant" ("id") on update cascade;');
12 | }
13 |
14 | async down(): Promise {
15 | this.addSql('alter table if exists "restaurant_admin" drop constraint if exists "restaurant_admin_restaurant_id_foreign";');
16 |
17 | this.addSql('drop table if exists "restaurant" cascade;');
18 |
19 | this.addSql('drop table if exists "restaurant_admin" cascade;');
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/backend/src/modules/restaurant/models/index.ts:
--------------------------------------------------------------------------------
1 | export { Restaurant } from "./restaurant";
2 | export { RestaurantAdmin } from "./restaurant-admin";
3 |
--------------------------------------------------------------------------------
/backend/src/modules/restaurant/models/restaurant-admin.ts:
--------------------------------------------------------------------------------
1 | import { model } from "@medusajs/utils";
2 | import { Restaurant } from "./restaurant";
3 |
4 | export const RestaurantAdmin = model.define("restaurant_admin", {
5 | id: model
6 | .id({
7 | prefix: "resadm",
8 | })
9 | .primaryKey(),
10 | first_name: model.text(),
11 | last_name: model.text(),
12 | email: model.text(),
13 | avatar_url: model.text().nullable(),
14 | restaurant: model.belongsTo(() => Restaurant, {
15 | mappedBy: "admins",
16 | }),
17 | });
18 |
--------------------------------------------------------------------------------
/backend/src/modules/restaurant/models/restaurant.ts:
--------------------------------------------------------------------------------
1 | import { model } from "@medusajs/utils";
2 | import { RestaurantAdmin } from "./restaurant-admin";
3 |
4 | export const Restaurant = model.define("restaurant", {
5 | id: model
6 | .id({
7 | prefix: "res",
8 | })
9 | .primaryKey(),
10 | handle: model.text(),
11 | is_open: model.boolean().default(false),
12 | name: model.text(),
13 | description: model.text().nullable(),
14 | phone: model.text(),
15 | email: model.text(),
16 | address: model.text(),
17 | image_url: model.text().nullable(),
18 | admins: model.hasMany(() => RestaurantAdmin),
19 | });
20 |
--------------------------------------------------------------------------------
/backend/src/modules/restaurant/service.ts:
--------------------------------------------------------------------------------
1 | import { MedusaService } from "@medusajs/utils";
2 | import { Restaurant, RestaurantAdmin } from "./models";
3 |
4 | class RestaurantModuleService extends MedusaService({
5 | Restaurant,
6 | RestaurantAdmin,
7 | }) {}
8 |
9 | export default RestaurantModuleService;
10 |
--------------------------------------------------------------------------------
/backend/src/modules/restaurant/types/common.ts:
--------------------------------------------------------------------------------
1 | import { ProductDTO } from "@medusajs/types";
2 | import RestaurantModuleService from "../service";
3 |
4 | export interface RestaurantDTO {
5 | id: string;
6 | handle: string;
7 | is_open: boolean;
8 | name: string;
9 | description?: string;
10 | address: string;
11 | phone: string;
12 | email: string;
13 | image_url?: string;
14 | created_at: Date;
15 | updated_at: Date;
16 | products?: ProductDTO[];
17 | }
18 |
19 | export interface RestaurantAdminDTO {
20 | id: string;
21 | restaurant: RestaurantDTO;
22 | first_name: string;
23 | last_name: string;
24 | email: string;
25 | created_at: Date;
26 | updated_at: Date;
27 | }
28 |
29 | export interface RestaurantProductDTO {
30 | restaurant_id: string;
31 | product_id: string;
32 | }
33 |
34 | declare module "@medusajs/types" {
35 | export interface ModuleImplementations {
36 | restaurantModuleService: RestaurantModuleService;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/backend/src/modules/restaurant/types/mutations.ts:
--------------------------------------------------------------------------------
1 | export interface CreateRestaurantDTO {
2 | name: string;
3 | handle: string;
4 | address: string;
5 | phone: string;
6 | email: string;
7 | image_url?: string;
8 | is_open?: boolean;
9 | }
10 |
11 | export interface UpdateRestaurantsDTO extends Partial {
12 | id: string;
13 | }
14 |
15 | export interface CreateRestaurantAdminDTO {
16 | email: string;
17 | first_name: string;
18 | last_name: string;
19 | restaurant_id: string;
20 | }
21 |
22 | export interface UpdateRestaurantAdminsDTO
23 | extends Partial {
24 | id: string;
25 | }
26 |
27 | export interface CreateAdminInviteDTO {
28 | resadm_id: string;
29 | role?: string | null;
30 | email?: string;
31 | }
32 |
--------------------------------------------------------------------------------
/backend/src/subscribers/README.md:
--------------------------------------------------------------------------------
1 | # Custom subscribers
2 |
3 | You may define custom eventhandlers, `subscribers` by creating files in the `/subscribers` directory.
4 |
5 | ```ts
6 | import MyCustomService from "../services/my-custom"
7 | import {
8 | OrderService,
9 | SubscriberArgs,
10 | SubscriberConfig,
11 | } from "@medusajs/medusa"
12 |
13 | type OrderPlacedEvent = {
14 | id: string
15 | no_notification: boolean
16 | }
17 |
18 | export default async function orderPlacedHandler({
19 | data,
20 | eventName,
21 | container,
22 | }: SubscriberArgs) {
23 | const orderService: OrderService = container.resolve(OrderService)
24 |
25 | const order = await orderService.retrieve(data.id, {
26 | relations: ["items", "items.variant", "items.variant.product"],
27 | })
28 |
29 | // Do something with the order
30 | }
31 |
32 | export const config: SubscriberConfig = {
33 | event: OrderService.Events.PLACED,
34 | }
35 | ```
36 |
37 | A subscriber is defined in two parts a `handler` and a `config`. The `handler` is a function which is invoked when an event is emitted. The `config` is an object which defines which event(s) the subscriber should subscribe to.
38 |
39 | The `handler` is a function which takes one parameter, an `object` of type `SubscriberArgs` with the following properties:
40 |
41 | - `data` - an `object` of type `T` containing information about the event.
42 | - `eventName` - a `string` containing the name of the event.
43 | - `container` - a `MedusaContainer` instance which can be used to resolve services.
44 | - `pluginOptions` - an `object` containing plugin options, if the subscriber is defined in a plugin.
45 |
--------------------------------------------------------------------------------
/backend/src/utils/create-variant-price-set.ts:
--------------------------------------------------------------------------------
1 | import { MedusaContainer } from "@medusajs/framework";
2 | import {
3 | CreatePriceSetDTO,
4 | IPricingModuleService,
5 | PriceSetDTO,
6 | } from "@medusajs/types";
7 | import { Modules, remoteQueryObjectFromString } from "@medusajs/utils";
8 |
9 | export const createVariantPriceSet = async ({
10 | container,
11 | variantId,
12 | prices,
13 | }: {
14 | container: MedusaContainer;
15 | variantId: string;
16 | prices: CreatePriceSetDTO["prices"];
17 | }): Promise => {
18 | const remoteLink = container.resolve("remoteLink");
19 | const remoteQuery = container.resolve("remoteQuery");
20 | const pricingModuleService: IPricingModuleService = container.resolve(
21 | "pricingModuleService"
22 | );
23 |
24 | const priceSet = await pricingModuleService.createPriceSets({
25 | prices,
26 | });
27 |
28 | await remoteLink.create({
29 | [Modules.PRODUCT]: { variant_id: variantId },
30 | [Modules.PRICING]: { price_set_id: priceSet.id },
31 | });
32 |
33 | const pricingQuery = remoteQueryObjectFromString({
34 | entryPoint: "price",
35 | fields: ["*"],
36 | filters: {
37 | id: priceSet.id,
38 | },
39 | });
40 |
41 | return await remoteQuery(pricingQuery);
42 | };
43 |
--------------------------------------------------------------------------------
/backend/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./create-variant-price-set"
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/await-delivery.ts:
--------------------------------------------------------------------------------
1 | import { createStep } from "@medusajs/workflows-sdk";
2 |
3 | export const awaitDeliveryStepId = "await-delivery-step";
4 | export const awaitDeliveryStep = createStep(
5 | { name: awaitDeliveryStepId, async: true, timeout: 60 * 15 },
6 | async function (_, { container }) {
7 | const logger = container.resolve("logger");
8 | logger.info("Awaiting delivery by driver...");
9 | }
10 | );
11 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/await-pick-up.ts:
--------------------------------------------------------------------------------
1 | import { createStep } from "@medusajs/workflows-sdk";
2 |
3 | export const awaitPickUpStepId = "await-pick-up-step";
4 | export const awaitPickUpStep = createStep(
5 | { name: awaitPickUpStepId, async: true, timeout: 60 * 15 },
6 | async function (_, { container }) {
7 | const logger = container.resolve("logger");
8 | logger.info("Awaiting pick up by driver...");
9 | }
10 | );
11 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/await-preparation.ts:
--------------------------------------------------------------------------------
1 | import { createStep } from "@medusajs/workflows-sdk";
2 |
3 | export const awaitPreparationStepId = "await-preparation-step";
4 | export const awaitPreparationStep = createStep(
5 | { name: awaitPreparationStepId, async: true, timeout: 60 * 15 },
6 | async function (_, { container }) {
7 | const logger = container.resolve("logger");
8 | logger.info("Awaiting preparation...");
9 | }
10 | );
11 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/await-start-preparation.ts:
--------------------------------------------------------------------------------
1 | import { createStep } from "@medusajs/workflows-sdk";
2 |
3 | export const awaitStartPreparationStepId = "await-start-preparation-step";
4 | export const awaitStartPreparationStep = createStep(
5 | { name: awaitStartPreparationStepId, async: true, timeout: 60 * 15 },
6 | async function (_, { container }) {
7 | const logger = container.resolve("logger");
8 | logger.info("Awaiting start of preparation...");
9 | }
10 | );
11 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/create-delivery.ts:
--------------------------------------------------------------------------------
1 | import { StepResponse, createStep } from "@medusajs/workflows-sdk";
2 | import { DeliveryDTO } from "../../../modules/delivery/types/common";
3 | import { DELIVERY_MODULE } from "../../../modules/delivery";
4 |
5 | export const createDeliveryStepId = "create-delivery-step";
6 | export const createDeliveryStep = createStep(
7 | createDeliveryStepId,
8 | async function ({}, { container }) {
9 | const service = container.resolve(DELIVERY_MODULE);
10 |
11 | const delivery = await service.createDeliveries({}) as DeliveryDTO
12 |
13 | return new StepResponse(delivery, {
14 | delivery_id: delivery.id,
15 | });
16 | },
17 | async function (
18 | {
19 | delivery_id,
20 | },
21 | { container }
22 | ) {
23 | const service = container.resolve(DELIVERY_MODULE);
24 |
25 | service.softDeleteDeliveries(delivery_id);
26 | }
27 | );
28 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/create-fulfillment.ts:
--------------------------------------------------------------------------------
1 | import { OrderDTO } from "@medusajs/types";
2 | import { ModuleRegistrationName } from "@medusajs/utils";
3 | import { StepResponse, createStep } from "@medusajs/workflows-sdk";
4 |
5 | export const createFulfillmentStepId = "create-fulfillment-step";
6 | export const createFulfillmentStep = createStep(
7 | createFulfillmentStepId,
8 | async function (order: OrderDTO, { container }) {
9 | const fulfillmentModuleService = container.resolve(
10 | ModuleRegistrationName.FULFILLMENT
11 | );
12 |
13 | const items = order.items?.map((lineItem) => {
14 | return {
15 | title: lineItem.title,
16 | sku: lineItem.variant_sku || "",
17 | quantity: lineItem.quantity,
18 | barcode: lineItem.variant_barcode || "",
19 | line_item_id: lineItem.id,
20 | };
21 | });
22 |
23 | const fulfillment = await fulfillmentModuleService.createFulfillment({
24 | provider_id: "manual_manual",
25 | location_id: "1",
26 | delivery_address: order.shipping_address!,
27 | items: items || [],
28 | labels: [],
29 | order,
30 | });
31 |
32 | return new StepResponse(fulfillment, fulfillment.id);
33 | },
34 | function (input: string, { container }) {
35 | const fulfillmentModuleService = container.resolve(
36 | ModuleRegistrationName.FULFILLMENT
37 | );
38 |
39 | return fulfillmentModuleService.softDeleteFulfillmentSets([input]);
40 | }
41 | );
42 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/create-order.ts:
--------------------------------------------------------------------------------
1 | import { CreateOrderShippingMethodDTO } from "@medusajs/types";
2 | import {
3 | ModuleRegistrationName,
4 | Modules,
5 | ContainerRegistrationKeys,
6 | } from "@medusajs/utils";
7 | import { StepResponse, createStep } from "@medusajs/workflows-sdk";
8 |
9 | export const createOrderStepId = "create-order-step";
10 | export const createOrderStep = createStep(
11 | createOrderStepId,
12 | async function (deliveryId: string, { container }) {
13 | const query = container.resolve(ContainerRegistrationKeys.QUERY);
14 |
15 | const deliveryQuery = {
16 | entity: "delivery",
17 | filters: {
18 | id: deliveryId,
19 | },
20 | fields: ["id", "cart.id", "delivery_status", "driver_id"],
21 | };
22 |
23 | const {
24 | data: [delivery],
25 | } = await query.graph(deliveryQuery);
26 |
27 | const cartQuery = {
28 | entity: "cart",
29 | fields: ["*", "items.*"],
30 | filters: {
31 | id: delivery.cart.id,
32 | },
33 | };
34 |
35 | const {
36 | data: [cart],
37 | } = await query.graph(cartQuery);
38 |
39 | const orderModuleService = container.resolve(ModuleRegistrationName.ORDER);
40 |
41 | const order = await orderModuleService.createOrders({
42 | currency_code: cart.currency_code,
43 | email: cart.email,
44 | shipping_address: cart.shipping_address,
45 | billing_address: cart.billing_address,
46 | items: cart.items,
47 | region_id: cart.region_id,
48 | customer_id: cart.customer_id,
49 | sales_channel_id: cart.sales_channel_id,
50 | shipping_methods:
51 | cart.shipping_methods as unknown as CreateOrderShippingMethodDTO[],
52 | });
53 |
54 | const remoteLink = container.resolve("remoteLink");
55 |
56 | await remoteLink.create({
57 | deliveryModuleService: {
58 | delivery_id: delivery.id,
59 | },
60 | [Modules.ORDER]: {
61 | order_id: order.id,
62 | },
63 | });
64 |
65 | return new StepResponse(order, {
66 | orderId: order.id,
67 | deliveryId,
68 | });
69 | },
70 | async (
71 | {
72 | orderId,
73 | deliveryId,
74 | }: {
75 | orderId: string;
76 | deliveryId: string;
77 | },
78 | { container }
79 | ) => {
80 | const remoteLink = container.resolve("remoteLink");
81 |
82 | await remoteLink.dismiss({
83 | deliveryModuleService: {
84 | delivery_id: deliveryId,
85 | },
86 | [Modules.ORDER]: {
87 | order_id: orderId,
88 | },
89 | });
90 |
91 | const orderService = container.resolve(ModuleRegistrationName.ORDER);
92 |
93 | await orderService.softDeleteOrders([orderId]);
94 | }
95 | );
96 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/delete-delivery-drivers.ts:
--------------------------------------------------------------------------------
1 | import { remoteQueryObjectFromString } from "@medusajs/utils";
2 | import { StepResponse, createStep } from "@medusajs/workflows-sdk";
3 | import { DriverDTO } from "../../../modules/delivery/types/common";
4 | import { DELIVERY_MODULE } from "../../../modules/delivery";
5 |
6 | export type DeleteDeliveryDriversStepInput = {
7 | delivery_id: string;
8 | driver_id?: string;
9 | };
10 |
11 | export const deleteDeliveryDriversStepId = "delete-delivery-drivers-step";
12 | export const deleteDeliveryDriversStep = createStep(
13 | deleteDeliveryDriversStepId,
14 | async function (input: DeleteDeliveryDriversStepInput, { container }) {
15 | const remoteQuery = container.resolve("remoteQuery");
16 |
17 | const driverQuery = remoteQueryObjectFromString({
18 | entryPoint: "delivery_driver",
19 | filters: {
20 | id: input.driver_id,
21 | },
22 | fields: ["id"],
23 | });
24 |
25 | const drivers = await remoteQuery(driverQuery)
26 | .then((res) => res.map((d: DriverDTO) => d.id))
27 | .catch(() => []);
28 |
29 | const deliveryModuleService = container.resolve(DELIVERY_MODULE);
30 |
31 | await deliveryModuleService.softDeleteDeliveryDrivers(drivers);
32 |
33 | return new StepResponse(drivers, drivers);
34 | },
35 | (deletedDrivers: string[], { container }) => {}
36 | );
37 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/find-driver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ModuleRegistrationName,
3 | remoteQueryObjectFromString,
4 | } from "@medusajs/utils";
5 | import { createStep } from "@medusajs/workflows-sdk";
6 | import { DriverDTO } from "../../../modules/delivery/types/common";
7 | import { DELIVERY_MODULE } from "../../../modules/delivery";
8 |
9 | export const findDriverStepId = "await-driver-response-step";
10 | export const findDriverStep = createStep(
11 | { name: findDriverStepId, async: true, timeout: 60 * 15, maxRetries: 2 },
12 | async function (deliveryId: string, { container }) {
13 | const remoteQuery = container.resolve("remoteQuery");
14 |
15 | const driversQuery = remoteQueryObjectFromString({
16 | entryPoint: "drivers",
17 | fields: ["id"],
18 | pagination: {
19 | skip: 0,
20 | take: null,
21 | },
22 | });
23 |
24 | const { rows: drivers } = await remoteQuery(driversQuery).catch((e) => []);
25 |
26 | const idsToNotify = drivers.map((d: DriverDTO) => d.id);
27 |
28 | const createData = idsToNotify.map((driverId: string) => ({
29 | delivery_id: deliveryId,
30 | driver_id: driverId,
31 | }));
32 |
33 | const deliveryService = container.resolve(DELIVERY_MODULE);
34 |
35 | await deliveryService.createDeliveryDrivers(createData);
36 |
37 | const eventBus = container.resolve(ModuleRegistrationName.EVENT_BUS);
38 |
39 | await eventBus.emit({
40 | name: "notify.drivers",
41 | data: {
42 | drivers: idsToNotify,
43 | delivery_id: deliveryId,
44 | },
45 | });
46 | },
47 | (input, { container }) => {
48 | const deliveryService = container.resolve(DELIVERY_MODULE);
49 |
50 | return deliveryService.softDeleteDeliveryDrivers(input);
51 | }
52 | );
53 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./await-delivery";
2 | export * from "./await-pick-up";
3 | export * from "./await-preparation";
4 | export * from "./await-start-preparation";
5 | export * from "./create-delivery";
6 | export * from "./create-fulfillment";
7 | export * from "./create-order";
8 | export {
9 | DeleteDeliveryDriversStepInput as CreateDeliveryStepInput,
10 | deleteDeliveryDriversStep,
11 | deleteDeliveryDriversStepId,
12 | } from "./delete-delivery-drivers";
13 | export * from "./find-driver";
14 | export * from "./notify-restaurant";
15 | export * from "./set-transaction-id";
16 | export * from "./update-delivery";
17 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/notify-restaurant.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ModuleRegistrationName,
3 | remoteQueryObjectFromString,
4 | } from "@medusajs/utils";
5 | import { createStep } from "@medusajs/workflows-sdk";
6 |
7 | export const notifyRestaurantStepId = "notify-restaurant-step";
8 | export const notifyRestaurantStep = createStep(
9 | {
10 | name: notifyRestaurantStepId,
11 | async: true,
12 | timeout: 60 * 15,
13 | maxRetries: 2,
14 | },
15 | async function (deliveryId: string, { container }) {
16 | const remoteQuery = container.resolve("remoteQuery");
17 |
18 | const deliveryQuery = remoteQueryObjectFromString({
19 | entryPoint: "deliveries",
20 | filters: {
21 | id: deliveryId,
22 | },
23 | fields: ["id", "restaurant.id"],
24 | });
25 |
26 | const delivery = await remoteQuery(deliveryQuery).then((res) => res[0]);
27 |
28 | const eventBus = container.resolve(ModuleRegistrationName.EVENT_BUS);
29 |
30 | await eventBus.emit({
31 | name: "notify.restaurant",
32 | data: {
33 | restaurant_id: delivery.restaurant.id,
34 | delivery_id: delivery.id,
35 | },
36 | });
37 | },
38 | function (input: string, { container }) {
39 | const logger = container.resolve("logger");
40 |
41 | logger.error("Failed to notify restaurant", { input });
42 | }
43 | );
44 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/set-transaction-id.ts:
--------------------------------------------------------------------------------
1 | import { StepResponse, createStep } from "@medusajs/workflows-sdk";
2 | import { DELIVERY_MODULE } from "../../../modules/delivery";
3 |
4 | export type SetTransactionIdStepInput = {
5 | delivery_id: string;
6 | };
7 |
8 | export const setTransactionIdStepId = "create-delivery-step";
9 | export const setTransactionIdStep = createStep(
10 | setTransactionIdStepId,
11 | async function (deliveryId: string, { container, context }) {
12 | const service = container.resolve(DELIVERY_MODULE);
13 |
14 | const delivery = await service.updateDeliveries({
15 | id: deliveryId,
16 | transaction_id: context.transactionId,
17 | });
18 |
19 | return new StepResponse(delivery, delivery.id);
20 | },
21 | async function (delivery_id: string, { container }) {
22 | const service = container.resolve(DELIVERY_MODULE);
23 |
24 | const delivery = await service.updateDeliveries({
25 | id: delivery_id,
26 | transaction_id: null,
27 | });
28 | }
29 | );
30 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/steps/update-delivery.ts:
--------------------------------------------------------------------------------
1 | import { createStep, StepResponse } from "@medusajs/workflows-sdk";
2 | import { UpdateDeliveryDTO } from "../../../modules/delivery/types/mutations";
3 | import { DELIVERY_MODULE } from "../../../modules/delivery";
4 |
5 | type UpdateDeliveryStepInput = {
6 | data: UpdateDeliveryDTO;
7 | };
8 |
9 | export const updateDeliveryStepId = "update-delivery-step";
10 | export const updateDeliveryStep = createStep(
11 | updateDeliveryStepId,
12 | async function (input: UpdateDeliveryStepInput, { container }) {
13 | const deliveryService = container.resolve(DELIVERY_MODULE);
14 |
15 | const delivery = await deliveryService
16 | .updateDeliveries([input.data])
17 | .then((res) => res[0]);
18 |
19 | return new StepResponse(delivery, delivery.id);
20 | }
21 | );
22 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/workflows/claim-delivery.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createWorkflow,
3 | WorkflowData,
4 | WorkflowResponse,
5 | } from "@medusajs/workflows-sdk";
6 | import { DeliveryStatus } from "../../../modules/delivery/types/common";
7 | import { setStepSuccessStep } from "../../util/steps";
8 | import { deleteDeliveryDriversStep, updateDeliveryStep } from "../steps";
9 | import { findDriverStepId } from "../steps/find-driver";
10 |
11 | export type ClaimWorkflowInput = {
12 | driver_id: string;
13 | delivery_id: string;
14 | };
15 |
16 | export const claimDeliveryWorkflow = createWorkflow(
17 | "claim-delivery-workflow",
18 | function (input: WorkflowData) {
19 | // Update the delivery with the provided data
20 | const claimedDelivery = updateDeliveryStep({
21 | data: {
22 | id: input.delivery_id,
23 | driver_id: input.driver_id,
24 | delivery_status: DeliveryStatus.PICKUP_CLAIMED,
25 | },
26 | });
27 |
28 | // Delete the delivery drivers as they are no longer needed
29 | deleteDeliveryDriversStep({ delivery_id: claimedDelivery.id });
30 |
31 | // Set the step success for the find driver step
32 | setStepSuccessStep({
33 | stepId: findDriverStepId,
34 | updatedDelivery: claimedDelivery,
35 | });
36 |
37 | // Return the updated delivery
38 | return new WorkflowResponse(claimedDelivery);
39 | }
40 | );
41 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/workflows/create-delivery.ts:
--------------------------------------------------------------------------------
1 | import {
2 | WorkflowData,
3 | WorkflowResponse,
4 | createWorkflow,
5 | transform,
6 | } from "@medusajs/workflows-sdk";
7 | import { Modules } from "@medusajs/utils"
8 | import { createRemoteLinkStep } from "@medusajs/core-flows"
9 | import { DeliveryDTO } from "../../../modules/delivery/types/common";
10 | import { createDeliveryStep } from "../../delivery/steps";
11 | import { DELIVERY_MODULE } from "../../../modules/delivery";
12 | import { RESTAURANT_MODULE } from "../../../modules/restaurant";
13 |
14 | type WorkflowInput = {
15 | cart_id: string;
16 | restaurant_id: string;
17 | };
18 |
19 | export const createDeliveryWorkflowId = "create-delivery-workflow";
20 | export const createDeliveryWorkflow = createWorkflow(
21 | createDeliveryWorkflowId,
22 | function (input: WorkflowData): WorkflowResponse {
23 | const delivery = createDeliveryStep();
24 |
25 | const links = transform({
26 | input,
27 | delivery
28 | }, (data) => ([
29 | {
30 | [DELIVERY_MODULE]: {
31 | delivery_id: data.delivery.id
32 | },
33 | [Modules.CART]: {
34 | cart_id: data.input.cart_id
35 | }
36 | },
37 | {
38 | [RESTAURANT_MODULE]: {
39 | restaurant_id: data.input.restaurant_id
40 | },
41 | [DELIVERY_MODULE]: {
42 | delivery_id: data.delivery.id
43 | }
44 | }
45 | ]))
46 |
47 | createRemoteLinkStep(links)
48 |
49 | return new WorkflowResponse(delivery);
50 | }
51 | );
52 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/workflows/handle-delivery.ts:
--------------------------------------------------------------------------------
1 | import {
2 | WorkflowData,
3 | WorkflowResponse,
4 | createWorkflow,
5 | } from "@medusajs/workflows-sdk";
6 | import {
7 | awaitDeliveryStep,
8 | awaitPickUpStep,
9 | awaitPreparationStep,
10 | awaitStartPreparationStep,
11 | createFulfillmentStep,
12 | createOrderStep,
13 | findDriverStep,
14 | notifyRestaurantStep,
15 | setTransactionIdStep,
16 | } from "../../delivery/steps";
17 |
18 | type WorkflowInput = {
19 | delivery_id: string;
20 | };
21 |
22 | const TWO_HOURS = 60 * 60 * 2;
23 | export const handleDeliveryWorkflowId = "handle-delivery-workflow";
24 | export const handleDeliveryWorkflow = createWorkflow(
25 | {
26 | name: handleDeliveryWorkflowId,
27 | store: true,
28 | retentionTime: TWO_HOURS,
29 | },
30 | function (input: WorkflowData): WorkflowResponse {
31 | setTransactionIdStep(input.delivery_id);
32 |
33 | notifyRestaurantStep(input.delivery_id);
34 |
35 | findDriverStep(input.delivery_id);
36 |
37 | const order = createOrderStep(input.delivery_id);
38 |
39 | awaitStartPreparationStep();
40 |
41 | awaitPreparationStep();
42 |
43 | createFulfillmentStep(order);
44 |
45 | awaitPickUpStep();
46 |
47 | awaitDeliveryStep();
48 |
49 | return new WorkflowResponse("Delivery completed");
50 | }
51 | );
52 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/workflows/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./claim-delivery";
2 | export * from "./create-delivery";
3 | export * from "./handle-delivery";
4 | export * from "./pass-delivery";
5 | export { WorkflowInput, updateDeliveryWorkflow } from "./update-delivery";
6 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/workflows/pass-delivery.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createWorkflow,
3 | WorkflowData,
4 | WorkflowResponse,
5 | } from "@medusajs/workflows-sdk";
6 | import { DeliveryDTO } from "../../../modules/delivery/types/common";
7 | import { deleteDeliveryDriversStep } from "../../delivery/steps";
8 |
9 | export type WorkflowInput = {
10 | driver_id: string;
11 | delivery_id: string;
12 | };
13 |
14 | export const passDeliveryWorkflow = createWorkflow(
15 | "pass-delivery-workflow",
16 | function (input: WorkflowData) {
17 | // Delete the delivery drivers as they are no longer needed
18 | deleteDeliveryDriversStep({
19 | delivery_id: input.delivery_id,
20 | driver_id: input.driver_id,
21 | });
22 |
23 | return new WorkflowResponse({} as DeliveryDTO);
24 | }
25 | );
26 |
--------------------------------------------------------------------------------
/backend/src/workflows/delivery/workflows/update-delivery.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createWorkflow,
3 | WorkflowData,
4 | WorkflowResponse,
5 | } from "@medusajs/workflows-sdk";
6 | import { UpdateDeliveryDTO } from "../../../modules/delivery/types/mutations";
7 | import { updateDeliveryStep } from "../../delivery/steps";
8 | import { setStepFailedStep, setStepSuccessStep } from "../../util/steps";
9 |
10 | export type WorkflowInput = {
11 | data: UpdateDeliveryDTO;
12 | stepIdToSucceed?: string;
13 | stepIdToFail?: string;
14 | };
15 |
16 | export const updateDeliveryWorkflow = createWorkflow(
17 | "update-delivery-workflow",
18 | function (input: WorkflowData) {
19 | // Update the delivery with the provided data
20 | const updatedDelivery = updateDeliveryStep({
21 | data: input.data,
22 | });
23 |
24 | // If a stepIdToSucceed is provided, we will set that step as successful
25 | setStepSuccessStep({
26 | stepId: input.stepIdToSucceed,
27 | updatedDelivery,
28 | });
29 |
30 | // If a stepIdToFail is provided, we will set that step as failed
31 | setStepFailedStep({
32 | stepId: input.stepIdToFail,
33 | updatedDelivery,
34 | });
35 |
36 | // Return the updated delivery
37 | return new WorkflowResponse(updatedDelivery);
38 | }
39 | );
40 |
--------------------------------------------------------------------------------
/backend/src/workflows/restaurant/steps/create-restaurant.ts:
--------------------------------------------------------------------------------
1 | import { StepResponse, createStep } from "@medusajs/workflows-sdk";
2 | import { CreateRestaurantDTO } from "../../../modules/restaurant/types/mutations";
3 | import { RESTAURANT_MODULE } from "../../../modules/restaurant";
4 |
5 | export const createRestaurantStepId = "create-restaurant-step";
6 | export const createRestaurantStep = createStep(
7 | createRestaurantStepId,
8 | async function (data: CreateRestaurantDTO, { container }) {
9 | const restaurantModuleService = container.resolve(
10 | RESTAURANT_MODULE
11 | );
12 |
13 | const restaurant = await restaurantModuleService.createRestaurants(data);
14 |
15 | return new StepResponse(restaurant, restaurant.id);
16 | },
17 | function (input: string, { container }) {
18 | const restaurantModuleService = container.resolve(
19 | RESTAURANT_MODULE
20 | );
21 |
22 | return restaurantModuleService.deleteRestaurants([input]);
23 | }
24 | );
25 |
--------------------------------------------------------------------------------
/backend/src/workflows/restaurant/steps/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./create-restaurant";
--------------------------------------------------------------------------------
/backend/src/workflows/restaurant/workflows/create-restaurant-products.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createProductsWorkflow,
3 | createRemoteLinkStep
4 | } from "@medusajs/core-flows";
5 | import { CreateProductDTO } from "@medusajs/types";
6 | import { Modules } from "@medusajs/utils"
7 | import {
8 | WorkflowData,
9 | WorkflowResponse,
10 | createWorkflow,
11 | transform,
12 | } from "@medusajs/workflows-sdk";
13 | import { RESTAURANT_MODULE } from "../../../modules/restaurant";
14 |
15 | type WorkflowInput = {
16 | products: CreateProductDTO[];
17 | restaurant_id: string;
18 | };
19 |
20 | export const createRestaurantProductsWorkflow = createWorkflow(
21 | "create-restaurant-products-workflow",
22 | function (input: WorkflowData) {
23 | const products = createProductsWorkflow.runAsStep({
24 | input: {
25 | products: input.products,
26 | },
27 | });
28 |
29 | const links = transform({
30 | products,
31 | input
32 | }, (data) => data.products.map((product) => ({
33 | [RESTAURANT_MODULE]: {
34 | restaurant_id: data.input.restaurant_id
35 | },
36 | [Modules.PRODUCT]: {
37 | product_id: product.id
38 | }
39 | })))
40 |
41 | createRemoteLinkStep(links)
42 |
43 | return new WorkflowResponse(links);
44 | }
45 | );
46 |
--------------------------------------------------------------------------------
/backend/src/workflows/restaurant/workflows/create-restaurant.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createWorkflow,
3 | WorkflowData,
4 | WorkflowResponse,
5 | } from "@medusajs/workflows-sdk";
6 | import { CreateRestaurantDTO } from "../../../modules/restaurant/types/mutations";
7 | import { createRestaurantStep } from "../steps";
8 |
9 | type WorkflowInput = {
10 | restaurant: CreateRestaurantDTO;
11 | };
12 |
13 | export const createRestaurantWorkflow = createWorkflow(
14 | "create-restaurant-workflow",
15 | function (input: WorkflowData) {
16 | const restaurant = createRestaurantStep(input.restaurant);
17 |
18 | return new WorkflowResponse(restaurant);
19 | }
20 | );
21 |
--------------------------------------------------------------------------------
/backend/src/workflows/restaurant/workflows/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./create-restaurant";
2 | export * from "./create-restaurant-products";
--------------------------------------------------------------------------------
/backend/src/workflows/user/steps/create-user.ts:
--------------------------------------------------------------------------------
1 | import { MedusaError } from "@medusajs/utils";
2 | import { createStep, StepResponse } from "@medusajs/workflows-sdk";
3 | import { DriverDTO } from "../../../modules/delivery/types/common";
4 | import { RestaurantAdminDTO } from "../../../modules/restaurant/types/common";
5 | import {
6 | CreateDriverInput,
7 | CreateRestaurantAdminInput,
8 | } from "../workflows/create-user";
9 | import { RESTAURANT_MODULE } from "../../../modules/restaurant";
10 | import { DELIVERY_MODULE } from "../../../modules/delivery";
11 |
12 | type CreateUserStepInput = (CreateRestaurantAdminInput | CreateDriverInput) & {
13 | actor_type: "restaurant" | "driver";
14 | };
15 |
16 | type CompensationStepInput = {
17 | id: string;
18 | actor_type: string;
19 | };
20 |
21 | export const createUserStepId = "create-user-step";
22 | export const createUserStep = createStep(
23 | createUserStepId,
24 | async (
25 | input: CreateUserStepInput,
26 | { container }
27 | ): Promise<
28 | StepResponse
29 | > => {
30 | if (input.actor_type === "restaurant") {
31 | const service = container.resolve(RESTAURANT_MODULE);
32 |
33 | const restaurantAdmin = await service.createRestaurantAdmins(
34 | input as CreateRestaurantAdminInput
35 | );
36 |
37 | const compensationData = {
38 | id: restaurantAdmin.id,
39 | actor_type: "restaurant",
40 | };
41 |
42 | return new StepResponse(restaurantAdmin, compensationData);
43 | }
44 |
45 | if (input.actor_type === "driver") {
46 | const service = container.resolve(DELIVERY_MODULE);
47 |
48 | const driver = await service.createDrivers(input as CreateDriverInput);
49 |
50 | const driverWithAvatar = await service.updateDrivers({
51 | id: driver.id,
52 | avatar_url: `https://robohash.org/${driver.id}?size=40x40&set=set1&bgset=bg1`,
53 | });
54 |
55 | const compensationData = {
56 | id: driverWithAvatar.id,
57 | actor_type: "driver",
58 | };
59 |
60 | return new StepResponse(driverWithAvatar, compensationData);
61 | }
62 |
63 | throw MedusaError.Types.INVALID_DATA;
64 | },
65 | function ({ id, actor_type }: CompensationStepInput, { container }) {
66 | if (actor_type === "restaurant") {
67 | const service = container.resolve(RESTAURANT_MODULE);
68 |
69 | return service.deleteRestaurantAdmins(id);
70 | }
71 |
72 | if (actor_type === "driver") {
73 | const service = container.resolve(DELIVERY_MODULE);
74 |
75 | return service.deleteDrivers(id);
76 | }
77 | }
78 | );
79 |
--------------------------------------------------------------------------------
/backend/src/workflows/user/steps/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./create-user";
2 |
--------------------------------------------------------------------------------
/backend/src/workflows/user/steps/update-user.ts:
--------------------------------------------------------------------------------
1 | import { MedusaError } from "@medusajs/utils";
2 | import { createStep, StepResponse } from "@medusajs/workflows-sdk";
3 | import { DriverDTO } from "../../../modules/delivery/types/common";
4 | import { RestaurantAdminDTO } from "../../../modules/restaurant/types/common";
5 | import {
6 | UpdateRestaurantsDTO,
7 | UpdateRestaurantAdminsDTO,
8 | } from "../../../modules/restaurant/types/mutations";
9 | import { RESTAURANT_MODULE } from "../../../modules/restaurant";
10 | import { DELIVERY_MODULE } from "../../../modules/delivery";
11 |
12 | type UpdateUserStepInput = (
13 | | UpdateRestaurantsDTO
14 | | UpdateRestaurantAdminsDTO
15 | ) & {
16 | actor_type: "restaurant" | "driver";
17 | };
18 |
19 | export const updateUserStepId = "update-user-step";
20 | export const updateUserStep = createStep(
21 | updateUserStepId,
22 | async (
23 | input: UpdateUserStepInput,
24 | { container }
25 | ): Promise<
26 | StepResponse
27 | > => {
28 | const { actor_type, ...data } = input;
29 |
30 | if (actor_type === "restaurant") {
31 | const service = container.resolve(RESTAURANT_MODULE);
32 |
33 | const compensationData = {
34 | ...(await service.retrieveRestaurantAdmin(data.id)),
35 | actor_type: "restaurant" as "restaurant",
36 | };
37 |
38 | const restaurantAdmin = await service.updateRestaurantAdmins(data);
39 |
40 | return new StepResponse(restaurantAdmin, compensationData);
41 | }
42 |
43 | if (actor_type === "driver") {
44 | const service = container.resolve(DELIVERY_MODULE);
45 |
46 | const compensationData = {
47 | ...(await service.retrieveDriver(data.id)),
48 | actor_type: "driver" as "driver",
49 | };
50 |
51 | const driver = await service.updateDrivers(data);
52 |
53 | return new StepResponse(driver, compensationData);
54 | }
55 |
56 | throw MedusaError.Types.INVALID_DATA;
57 | },
58 | function ({ actor_type, ...data }: UpdateUserStepInput, { container }) {
59 | if (actor_type === "restaurant") {
60 | const service = container.resolve(RESTAURANT_MODULE);
61 |
62 | return service.updateRestaurantAdmins(data);
63 | }
64 |
65 | if (actor_type === "driver") {
66 | const service = container.resolve(DELIVERY_MODULE);
67 |
68 | return service.updateDrivers(data);
69 | }
70 | }
71 | );
72 |
--------------------------------------------------------------------------------
/backend/src/workflows/user/workflows/create-user.ts:
--------------------------------------------------------------------------------
1 | import { setAuthAppMetadataStep } from "@medusajs/core-flows";
2 | import {
3 | createWorkflow,
4 | transform,
5 | WorkflowData,
6 | WorkflowResponse,
7 | } from "@medusajs/workflows-sdk";
8 | import { createUserStep } from "../../user/steps";
9 |
10 | export type CreateRestaurantAdminInput = {
11 | restaurant_id: string;
12 | email: string;
13 | first_name: string;
14 | last_name: string;
15 | };
16 |
17 | export type CreateDriverInput = {
18 | email: string;
19 | first_name: string;
20 | last_name: string;
21 | phone: string;
22 | avatar_url?: string;
23 | };
24 |
25 | type WorkflowInput = {
26 | user: (CreateRestaurantAdminInput | CreateDriverInput) & {
27 | actor_type: "restaurant" | "driver";
28 | };
29 | auth_identity_id: string;
30 | };
31 |
32 | export const createUserWorkflow = createWorkflow(
33 | "create-user-workflow",
34 | function (input: WorkflowData) {
35 | let user = createUserStep(input.user);
36 |
37 | const authUserInput = transform({ input, user }, ({ input, user }) => {
38 | const data = {
39 | authIdentityId: input.auth_identity_id,
40 | actorType: input.user.actor_type,
41 | key:
42 | input.user.actor_type === "restaurant"
43 | ? "restaurant_id"
44 | : "driver_id",
45 | value: user.id,
46 | };
47 |
48 | return data;
49 | });
50 |
51 | setAuthAppMetadataStep(authUserInput);
52 |
53 | return new WorkflowResponse(user);
54 | }
55 | );
56 |
--------------------------------------------------------------------------------
/backend/src/workflows/user/workflows/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./create-user";
2 |
--------------------------------------------------------------------------------
/backend/src/workflows/util/steps/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./set-step-failed";
2 | export * from "./set-step-success";
3 |
--------------------------------------------------------------------------------
/backend/src/workflows/util/steps/set-step-failed.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ModuleRegistrationName,
3 | TransactionHandlerType,
4 | } from "@medusajs/utils";
5 | import { StepResponse, createStep } from "@medusajs/workflows-sdk";
6 | import { DeliveryDTO } from "../../../modules/delivery/types/common";
7 | import { handleDeliveryWorkflowId } from "../../delivery/workflows/handle-delivery";
8 |
9 | type SetStepFailedtepInput = {
10 | stepId?: string;
11 | updatedDelivery: DeliveryDTO;
12 | };
13 |
14 | export const setStepFailedStepId = "set-step-failed-step";
15 | export const setStepFailedStep = createStep(
16 | setStepFailedStepId,
17 | async function (
18 | { stepId, updatedDelivery }: SetStepFailedtepInput,
19 | { container }
20 | ) {
21 | if (!stepId) {
22 | return;
23 | }
24 |
25 | const engineService = container.resolve(
26 | ModuleRegistrationName.WORKFLOW_ENGINE
27 | );
28 |
29 | await engineService.setStepFailure({
30 | idempotencyKey: {
31 | action: TransactionHandlerType.INVOKE,
32 | transactionId: updatedDelivery.transaction_id,
33 | stepId,
34 | workflowId: handleDeliveryWorkflowId,
35 | },
36 | stepResponse: new StepResponse(updatedDelivery, updatedDelivery.id),
37 | options: {
38 | container,
39 | },
40 | });
41 | }
42 | );
43 |
--------------------------------------------------------------------------------
/backend/src/workflows/util/steps/set-step-success.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ModuleRegistrationName,
3 | TransactionHandlerType,
4 | } from "@medusajs/utils";
5 | import { StepResponse, createStep } from "@medusajs/workflows-sdk";
6 | import { DeliveryDTO } from "../../../modules/delivery/types/common";
7 | import { handleDeliveryWorkflowId } from "../../delivery/workflows/handle-delivery";
8 |
9 | type SetStepSuccessStepInput = {
10 | stepId?: string;
11 | updatedDelivery: DeliveryDTO;
12 | };
13 |
14 | export const setStepSuccessStepId = "set-step-success-step";
15 | export const setStepSuccessStep = createStep(
16 | setStepSuccessStepId,
17 | async function (
18 | { stepId, updatedDelivery }: SetStepSuccessStepInput,
19 | { container }
20 | ) {
21 | if (!stepId) {
22 | return;
23 | }
24 |
25 | const engineService = container.resolve(
26 | ModuleRegistrationName.WORKFLOW_ENGINE
27 | );
28 |
29 | await engineService.setStepSuccess({
30 | idempotencyKey: {
31 | action: TransactionHandlerType.INVOKE,
32 | transactionId: updatedDelivery.transaction_id,
33 | stepId,
34 | workflowId: handleDeliveryWorkflowId,
35 | },
36 | stepResponse: new StepResponse(updatedDelivery, updatedDelivery.id),
37 | options: {
38 | container,
39 | },
40 | });
41 | }
42 | );
43 |
--------------------------------------------------------------------------------
/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2021",
4 | "esModuleInterop": true,
5 | "module": "Node16",
6 | "moduleResolution": "Node16",
7 | "emitDecoratorMetadata": true,
8 | "experimentalDecorators": true,
9 | "skipLibCheck": true,
10 | "skipDefaultLibCheck": true,
11 | "declaration": false,
12 | "sourceMap": false,
13 | "inlineSourceMap": true,
14 | "outDir": "./.medusa/server",
15 | "rootDir": "./",
16 | "baseUrl": ".",
17 | "jsx": "react-jsx",
18 | "forceConsistentCasingInFileNames": true,
19 | "resolveJsonModule": true,
20 | "checkJs": false
21 | },
22 | "ts-node": {
23 | "swc": true
24 | },
25 | "include": ["**/*", ".medusa/types/*"],
26 | "exclude": ["node_modules", ".medusa/server", ".medusa/admin", ".cache"]
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/.env.template:
--------------------------------------------------------------------------------
1 | JWT_SECRET=supersecret
2 | BACKEND_URL=http://localhost:9000
--------------------------------------------------------------------------------
/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | Frontend Overview
2 |
3 | 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).
4 |
5 | ## Getting Started
6 |
7 | First, run the development server:
8 |
9 | ```bash
10 | npm run dev
11 | # or
12 | yarn dev
13 | # or
14 | pnpm dev
15 | # or
16 | bun dev
17 | ```
18 |
19 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
20 |
21 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
22 |
23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
24 |
25 | ## Learn More
26 |
27 | To learn more about Next.js, take a look at the following resources:
28 |
29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
31 |
32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
33 |
34 | ## Deploy on Vercel
35 |
36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
37 |
38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
39 |
--------------------------------------------------------------------------------
/frontend/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: "http",
7 | hostname: "localhost",
8 | port: "3000",
9 | },
10 | {
11 | protocol: "https",
12 | hostname: "robohash.org",
13 | },
14 | {
15 | protocol: "https",
16 | hostname: "medusa-eats.vercel.app",
17 | },
18 | ],
19 | },
20 | };
21 |
22 | export default nextConfig;
23 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "medusa-eats",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@dotlottie/react-player": "^1.6.19",
13 | "@medusajs/icons": "^1.2.2-preview-20240925090259",
14 | "@medusajs/js-sdk": "^0.0.2-preview-20241008090600",
15 | "@medusajs/ui": "preview",
16 | "@next/third-parties": "^14.2.5",
17 | "eventsource": "^2.0.2",
18 | "jose": "^5.2.4",
19 | "lucide-react": "^0.427.0",
20 | "next": "14.1.4",
21 | "next-view-transitions": "^0.1.1",
22 | "prop-types": "^15.8.1",
23 | "react": "^18",
24 | "react-dom": "^18",
25 | "react-lottie": "^1.2.4",
26 | "sharp": "^0.33.4",
27 | "tailwind-scrollbar-hide": "^1.1.7"
28 | },
29 | "devDependencies": {
30 | "@medusajs/types": "preview",
31 | "@medusajs/ui-preset": "preview",
32 | "@types/eventsource": "^1.1.15",
33 | "@types/jsonwebtoken": "^9.0.6",
34 | "@types/node": "^20",
35 | "@types/react": "^18",
36 | "@types/react-dom": "^18",
37 | "@types/tailwindcss": "^3.1.0",
38 | "autoprefixer": "^10.0.1",
39 | "eslint": "^8",
40 | "eslint-config-next": "14.1.4",
41 | "postcss": "^8",
42 | "tailwindcss": "^3.3.0",
43 | "typescript": "^5.5.4"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/frontend/public/_1ed747de-53f1-4915-aaba-92c046039beb.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_1ed747de-53f1-4915-aaba-92c046039beb.jpeg
--------------------------------------------------------------------------------
/frontend/public/_35841975-1e89-4c07-adb8-208cc8c59e17.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_35841975-1e89-4c07-adb8-208cc8c59e17.jpeg
--------------------------------------------------------------------------------
/frontend/public/_411c260a-d6e5-4989-9a00-1cd8b80265fb.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_411c260a-d6e5-4989-9a00-1cd8b80265fb.jpeg
--------------------------------------------------------------------------------
/frontend/public/_4476ce30-b852-402d-b65f-e3db54165e4c.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_4476ce30-b852-402d-b65f-e3db54165e4c.jpeg
--------------------------------------------------------------------------------
/frontend/public/_7a9bad3a-6f04-4030-a584-a622a1bb8c68.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_7a9bad3a-6f04-4030-a584-a622a1bb8c68.jpeg
--------------------------------------------------------------------------------
/frontend/public/_ca16b6a2-9c4c-4a6a-98fa-620f12f6123f.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_ca16b6a2-9c4c-4a6a-98fa-620f12f6123f.jpeg
--------------------------------------------------------------------------------
/frontend/public/_de4759b3-096f-47f6-936a-ea291df53f9f.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/_de4759b3-096f-47f6-936a-ea291df53f9f.jpeg
--------------------------------------------------------------------------------
/frontend/public/medusa-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/public/notification.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/notification.mp3
--------------------------------------------------------------------------------
/frontend/public/pizza-loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/public/pizza-loading.gif
--------------------------------------------------------------------------------
/frontend/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/app/(checkout)/checkout/page.tsx:
--------------------------------------------------------------------------------
1 | import CheckoutForm from "@frontend/components/store/checkout/checkout-form";
2 | import { OrderSummary } from "@frontend/components/store/checkout/order-summary";
3 | import { retrieveCart } from "@frontend/lib/data";
4 | import { HttpTypes } from "@medusajs/types";
5 | import { cookies } from "next/headers";
6 |
7 | export default async function CheckoutPage() {
8 | const cartId = cookies().get("_medusa_cart_id")?.value;
9 |
10 | if (!cartId) {
11 | return null;
12 | }
13 |
14 | const cart = (await retrieveCart(cartId)) as HttpTypes.StoreCart;
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/src/app/(checkout)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from "@frontend/components/common/footer";
2 | import { ProfileBadge } from "@frontend/components/common/profile-badge";
3 | import { retrieveUser } from "@frontend/lib/data";
4 | import { FlyingBox } from "@medusajs/icons";
5 | import { Text } from "@medusajs/ui";
6 | import type { Metadata } from "next";
7 | import { Link } from "next-view-transitions";
8 | import Image from "next/image";
9 |
10 | export const metadata: Metadata = {
11 | title: "Medusa Eats",
12 | description: "Order food from your favorite restaurants",
13 | };
14 |
15 | export default async function RootLayout({
16 | children,
17 | }: Readonly<{
18 | children: React.ReactNode;
19 | }>) {
20 | const user = await retrieveUser();
21 |
22 | return (
23 | <>
24 |
25 |
29 | Medusa Eats
30 |
31 |
35 |
42 |
43 |
46 |
47 |
48 | {children}
49 |
50 |
51 | >
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/app/(store)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from "@frontend/components/common/footer";
2 | import { ProfileBadge } from "@frontend/components/common/profile-badge";
3 | import NavCart from "@frontend/components/store/cart/nav-cart";
4 | import { retrieveUser } from "@frontend/lib/data";
5 | import { FlyingBox, ShoppingBag } from "@medusajs/icons";
6 | import { IconButton, Text } from "@medusajs/ui";
7 | import type { Metadata } from "next";
8 | import { Link } from "next-view-transitions";
9 | import Image from "next/image";
10 | import { Suspense } from "react";
11 |
12 | export const metadata: Metadata = {
13 | title: "Medusa Eats",
14 | description: "Order food from your favorite restaurants",
15 | };
16 |
17 | export default async function RootLayout({
18 | children,
19 | }: Readonly<{
20 | children: React.ReactNode;
21 | }>) {
22 | const user = await retrieveUser();
23 |
24 | return (
25 | <>
26 |
27 |
31 | Medusa Eats
32 |
33 |
37 |
44 |
45 |
46 |
49 |
50 |
51 | }
52 | >
53 |
54 |
55 |
56 |
57 |
58 |
59 | {children}
60 |
61 |
62 | >
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/frontend/src/app/(store)/login/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 |
3 | export const metadata: Metadata = {
4 | title: "Login | Medusa Eats",
5 | description: "Order food from your favorite restaurants",
6 | };
7 |
8 | export default function RootLayout({
9 | children,
10 | }: Readonly<{
11 | children: React.ReactNode;
12 | }>) {
13 | return {children}
;
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/app/(store)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { LoginForm } from "@frontend/components/dashboard/login-form";
2 | import { Container, Heading, Text } from "@medusajs/ui";
3 |
4 | export default function LoginPage() {
5 | return (
6 |
7 |
8 |
9 | {process.env.NEXT_PUBLIC_DEMO_MODE === "true"
10 | ? "Log in as a demo admin"
11 | : "Log in to your Medusa Eats account"}
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/app/(store)/page.tsx:
--------------------------------------------------------------------------------
1 | import DemoModal from "@frontend/components/common/demo-modal";
2 | import RestaurantCategory from "@frontend/components/store/restaurant/restaurant-category";
3 | import { listRestaurants } from "@frontend/lib/data/restaurants";
4 | import { Heading } from "@medusajs/ui";
5 |
6 | export default async function Home() {
7 | const restaurants = await listRestaurants();
8 |
9 | if (!restaurants) {
10 | return No restaurants open near you ;
11 | }
12 |
13 | return (
14 |
15 | {process.env.NEXT_PUBLIC_DEMO_MODE === "true" && }
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/app/(store)/restaurant/[handle]/page.tsx:
--------------------------------------------------------------------------------
1 | import RestaurantCategories from "@frontend/components/store/restaurant/restaurant-categories";
2 | import { retrieveRestaurantByHandle } from "@frontend/lib/data";
3 | import { Heading, Text } from "@medusajs/ui";
4 | import { notFound } from "next/navigation";
5 |
6 | export default async function RestaurantPage({
7 | params,
8 | }: {
9 | params: { handle: string };
10 | }) {
11 | const restaurant = await retrieveRestaurantByHandle(params.handle);
12 |
13 | if (!restaurant) return notFound();
14 |
15 | const categoryProductMap = new Map();
16 |
17 | restaurant.products?.forEach((product) => {
18 | if (product.categories) {
19 | product.categories.forEach((category) => {
20 | if (categoryProductMap.has(category.id)) {
21 | categoryProductMap.get(category.id).products.push(product);
22 | } else {
23 | categoryProductMap.set(category.id, {
24 | category_name: category.name,
25 | products: [product],
26 | });
27 | }
28 | });
29 | }
30 | });
31 |
32 | return (
33 |
34 |
35 |
36 |
37 | {restaurant.name} | Order food online
38 |
39 | {restaurant.description}
40 |
41 |
42 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/src/app/(store)/signup/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 |
3 | export const metadata: Metadata = {
4 | title: "Login | Medusa Eats",
5 | description: "Order food from your favorite restaurants",
6 | };
7 |
8 | export default function RootLayout({
9 | children,
10 | }: Readonly<{
11 | children: React.ReactNode;
12 | }>) {
13 | return {children}
;
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/app/(store)/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignupForm } from "@frontend/components/dashboard/signup-form";
2 | import { Container, Heading } from "@medusajs/ui";
3 | import { listRestaurants } from "@frontend/lib/data";
4 |
5 | export default async function SignupPage() {
6 | const restaurants = await listRestaurants();
7 |
8 | return (
9 |
10 |
11 | Create your Medusa Eats account
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/src/app/api/subscribe/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import EventSource from "eventsource";
3 |
4 | export const runtime = "nodejs";
5 | export const dynamic = "force-dynamic";
6 | export const maxDuration = 300;
7 |
8 | const BACKEND_URL =
9 | process.env.BACKEND_URL ||
10 | process.env.NEXT_PUBLIC_BACKEND_URL ||
11 | "http://localhost:9000";
12 |
13 | export async function GET(req: NextRequest, res: NextResponse) {
14 | let responseStream = new TransformStream();
15 | const writer = responseStream.writable.getWriter();
16 | const encoder = new TextEncoder();
17 |
18 | const restaurantId = req.nextUrl.searchParams.get("restaurant_id");
19 | const driverId = req.nextUrl.searchParams.get("driver_id");
20 | const deliveryId = req.nextUrl.searchParams.get("delivery_id");
21 |
22 | let serverUrl = BACKEND_URL + "/store/deliveries/subscribe";
23 |
24 | if (restaurantId) {
25 | serverUrl += `?restaurant_id=${restaurantId}`;
26 | writer.write(
27 | encoder.encode(
28 | "data: " +
29 | JSON.stringify({
30 | message: "Subscribing to restaurant " + restaurantId,
31 | }) +
32 | "\n\n"
33 | )
34 | );
35 | }
36 |
37 | if (driverId) {
38 | serverUrl += `?driver_id=${driverId}`;
39 | writer.write(
40 | encoder.encode(
41 | "data: " +
42 | JSON.stringify({ message: "Subscribing to driver " + driverId }) +
43 | "\n\n"
44 | )
45 | );
46 | }
47 |
48 | if (deliveryId) {
49 | serverUrl += `?delivery_id=${deliveryId}`;
50 | writer.write(
51 | encoder.encode(
52 | "data: " +
53 | JSON.stringify({ message: "Subscribing to delivery " + deliveryId }) +
54 | "\n\n"
55 | )
56 | );
57 | }
58 |
59 | const source = new EventSource(serverUrl, {
60 | headers: {
61 | "x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
62 | },
63 | });
64 |
65 | source.onmessage = (message: Record) => {
66 | writer.write(encoder.encode("data: " + message.data + "\n\n"));
67 | };
68 |
69 | source.onerror = (error) => {
70 | writer.write(
71 | encoder.encode(`event: error\ndata: ${JSON.stringify(error)}\n\n`)
72 | );
73 | source.close();
74 | writer.close();
75 | };
76 |
77 | req.signal.onabort = () => {
78 | source.close();
79 | writer.close();
80 | };
81 |
82 | return new Response(responseStream.readable, {
83 | headers: {
84 | "Content-Type": "text/event-stream",
85 | Connection: "keep-alive",
86 | "Cache-Control": "no-cache, no-transform",
87 | },
88 | });
89 | }
90 |
--------------------------------------------------------------------------------
/frontend/src/app/dashboard/driver/page.tsx:
--------------------------------------------------------------------------------
1 | import AccountBadge from "@frontend/components/dashboard/account-badge";
2 | import DeliveryColumn from "@frontend/components/dashboard/delivery-column";
3 | import RealtimeClient from "@frontend/components/dashboard/realtime-client";
4 | import {
5 | listDeliveries,
6 | retrieveDriver,
7 | retrieveUser,
8 | } from "@frontend/lib/data";
9 | import { DeliveryStatus } from "@frontend/lib/types";
10 | import { Container, Heading, Text } from "@medusajs/ui";
11 | import { redirect } from "next/navigation";
12 |
13 | export default async function DriverDashboardPage() {
14 | const user = await retrieveUser();
15 |
16 | if (!user || !user.id.includes("drv_")) {
17 | redirect("/login");
18 | }
19 |
20 | const driver = await retrieveDriver(user.id);
21 | const deliveries = await listDeliveries({
22 | driver_id: driver.id,
23 | });
24 |
25 | return (
26 |
27 |
28 |
29 |
30 | {driver.first_name} {driver.last_name} | Driver Dashboard
31 |
32 | View and manage your Medusa Eats deliveries.
33 |
34 |
35 |
36 |
37 |
38 |
39 |
46 |
56 |
63 |
70 |
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/frontend/src/app/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from "@frontend/components/common/footer";
2 | import { ProfileBadge } from "@frontend/components/common/profile-badge";
3 | import { retrieveUser } from "@frontend/lib/data";
4 | import { FlyingBox } from "@medusajs/icons";
5 | import { Text } from "@medusajs/ui";
6 | import type { Metadata } from "next";
7 | import { Link } from "next-view-transitions";
8 | import Image from "next/image";
9 |
10 | export const metadata: Metadata = {
11 | title: "Medusa Eats",
12 | description: "Order food from your favorite restaurants",
13 | };
14 |
15 | export default async function RootLayout({
16 | children,
17 | }: Readonly<{
18 | children: React.ReactNode;
19 | }>) {
20 | const user = await retrieveUser();
21 |
22 | return (
23 | <>
24 |
25 |
29 | Medusa Eats
30 |
31 |
35 |
42 |
43 |
46 |
47 |
48 | {children}
49 |
50 |
51 | >
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/src/app/favicon.ico
--------------------------------------------------------------------------------
/frontend/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | color: rgb(var(--fg-base));
7 | background: rgb(var(--bg-subtle));
8 | }
9 |
10 | @layer utilities {
11 | .text-balance {
12 | text-wrap: balance;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { ViewTransitions } from "next-view-transitions";
5 |
6 | export const dynamic = "force-dynamic";
7 |
8 | const inter = Inter({ subsets: ["latin"] });
9 |
10 | export const metadata: Metadata = {
11 | title: "Medusa Eats",
12 | description: "Order food from your favorite restaurants",
13 | };
14 |
15 | export default function RootLayout({
16 | children,
17 | }: Readonly<{
18 | children: React.ReactNode;
19 | }>) {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 | {children}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/src/components/common/demo-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Github, XMarkMini } from "@medusajs/icons";
4 | import { Button, Container, Heading, IconButton, Text } from "@medusajs/ui";
5 | import Link from "next/link";
6 | import { useState } from "react";
7 |
8 | export default function DemoModal() {
9 | const [modalClosed, setModalClosed] = useState(false);
10 |
11 | if (modalClosed) {
12 | return null;
13 | }
14 |
15 | return (
16 |
17 |
18 |
19 | Medusa Eats Demo Mode
20 | setModalClosed(true)}
22 | size="xsmall"
23 | variant="transparent"
24 | >
25 |
26 |
27 |
28 |
29 | This is a demo of Medusa Eats, a food delivery platform template built
30 | with Medusa 2.0 and Next.js.
31 |
32 |
33 | To go through the entire delivery process, complete the following
34 | steps:
35 |
36 |
37 |
38 | Select a restaurant, add some food to your cart and complete the
39 | checkout form.
40 |
41 | In a new tab, log in as a restaurant and accept the order.
42 |
43 | In a different incognito tab, log in as a driver and claim the
44 | delivery.
45 |
46 |
47 | You can now go back and forth between the driver and restaurant tabs
48 | to complete the delivery.
49 |
50 |
51 |
55 |
56 |
57 | View project on GitHub
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/frontend/src/components/common/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Text } from "@medusajs/ui";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 | import { GoogleAnalytics } from "@next/third-parties/google";
5 |
6 | const GA_ID = process.env.NEXT_PUBLIC_GA_ID;
7 |
8 | export default function Footer() {
9 | return (
10 |
11 | {GA_ID && }
12 |
13 | © {new Date().getFullYear()}
14 |
18 |
25 | Medusa
26 |
27 | |
28 |
32 | GitHub repository
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/components/common/icons.tsx:
--------------------------------------------------------------------------------
1 | import { Bike, Pizza, Store } from "lucide-react";
2 | import React, {
3 | ForwardRefExoticComponent,
4 | PropsWithChildren,
5 | RefAttributes,
6 | } from "react";
7 |
8 | export const PizzaIcon: ForwardRefExoticComponent<
9 | RefAttributes
10 | > = React.forwardRef((props: PropsWithChildren, ref) => (
11 |
12 | ));
13 | PizzaIcon.displayName = "PizzaIcon";
14 |
15 | export const BikeIcon: ForwardRefExoticComponent> =
16 | React.forwardRef((props: PropsWithChildren, ref) => (
17 |
18 | ));
19 | BikeIcon.displayName = "BikeIcon";
20 |
21 | export const StoreIcon: ForwardRefExoticComponent<
22 | RefAttributes
23 | > = React.forwardRef((props: PropsWithChildren, ref) => (
24 |
25 | ));
26 | StoreIcon.displayName = "StoreIcon";
27 |
--------------------------------------------------------------------------------
/frontend/src/components/common/profile-badge.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DriverDTO, RestaurantAdminDTO } from "@frontend/lib/types";
4 | import { logout } from "@frontend/lib/actions";
5 | import { Avatar, Button, Text } from "@medusajs/ui";
6 | import { Link } from "next-view-transitions";
7 |
8 | type ProfileBadgeProps = {
9 | user: RestaurantAdminDTO | DriverDTO | null;
10 | };
11 |
12 | export function ProfileBadge({ user }: ProfileBadgeProps) {
13 | const dashboardPath = user
14 | ? user.hasOwnProperty("restaurant_id")
15 | ? "/dashboard/restaurant"
16 | : "/dashboard/driver"
17 | : "/login";
18 |
19 | return (
20 |
21 |
22 |
27 | {user ? (
28 | <>
29 |
30 | {`${user.first_name} ${user.last_name}`}
31 |
32 |
37 | >
38 | ) : (
39 | <>
40 |
41 | Login
42 |
43 |
44 |
45 |
46 |
47 | >
48 | )}
49 |
50 |
51 | {user && (
52 |
logout()}
57 | >
58 |
59 | Logout
60 |
61 |
62 | )}
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/account-badge.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DeliveryDTO,
3 | DeliveryStatus,
4 | DriverDTO,
5 | RestaurantDTO,
6 | } from "@frontend/lib/types";
7 | import { Badge, Text } from "@medusajs/ui";
8 | import Image from "next/image";
9 |
10 | const BACKEND_URL =
11 | process.env.BACKEND_URL ||
12 | process.env.NEXT_PUBLIC_BACKEND_URL ||
13 | "http://localhost:9000";
14 |
15 | async function getDeliveries(query: string) {
16 | const { deliveries } = await fetch(
17 | `${BACKEND_URL}/deliveries?${query}&delivery_status=${DeliveryStatus.DELIVERED}`,
18 | {
19 | next: {
20 | tags: ["deliveries"],
21 | },
22 | }
23 | ).then((res) => res.json());
24 | return deliveries;
25 | }
26 |
27 | export default async function AccountBadge({
28 | data,
29 | type,
30 | }: {
31 | data: DriverDTO | RestaurantDTO;
32 | type: "driver" | "restaurant";
33 | }) {
34 | let name = "";
35 |
36 | if (type === "driver") {
37 | const driver = data as DriverDTO;
38 | name = driver.first_name + " " + driver.last_name;
39 | }
40 |
41 | if (type === "restaurant") {
42 | const restaurant = data as RestaurantDTO;
43 | name = restaurant.name;
44 | }
45 |
46 | return (
47 |
48 |
49 |
50 | {name}
51 | {data.email}
52 | {data.phone}
53 |
54 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/completed-grid.tsx:
--------------------------------------------------------------------------------
1 | import { DeliveryDTO, DeliveryStatus, DriverDTO } from "@frontend/lib/types";
2 | import { Heading } from "@medusajs/ui";
3 | import DeliveryCard from "./delivery-card";
4 |
5 | export default async function CompletedGrid({
6 | title,
7 | deliveries,
8 | statusFilters,
9 | driver,
10 | type,
11 | }: {
12 | title: string;
13 | deliveries: DeliveryDTO[];
14 | statusFilters?: DeliveryStatus[];
15 | driver?: DriverDTO;
16 | type: "restaurant" | "driver";
17 | }) {
18 | return (
19 |
20 | {title}
21 | {deliveries
22 | .filter((d) => statusFilters?.includes(d.delivery_status))
23 | .sort(
24 | (a, b) =>
25 | new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
26 | )
27 | .map((delivery) => (
28 |
34 | ))}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/delivery-card.tsx:
--------------------------------------------------------------------------------
1 | import { DeliveryDTO, DriverDTO } from "@frontend/lib/types";
2 | import { Container, Heading, Table } from "@medusajs/ui";
3 | import DriverDeliveryButtons from "./driver/delivery-buttons";
4 | import { DriverDeliveryStatusBadge } from "./driver/delivery-status-badge";
5 | import RestaurantDeliveryButtons from "./restaurant/delivery-buttons";
6 | import { RestaurantDeliveryStatusBadge } from "./restaurant/delivery-status-badge";
7 |
8 | export default async function DeliveryCard({
9 | delivery,
10 | driver,
11 | type,
12 | }: {
13 | delivery: DeliveryDTO;
14 | driver?: DriverDTO;
15 | type: "restaurant" | "driver";
16 | }) {
17 | if (!delivery || delivery === null) return null;
18 |
19 | const items = delivery.order?.items || delivery.cart?.items;
20 |
21 | return (
22 |
23 |
24 |
25 | Order {delivery?.id?.slice(-4)}
26 |
27 | {type === "driver" && }
28 | {type === "restaurant" && (
29 |
30 | )}
31 |
32 |
33 |
34 |
35 |
36 | Items
37 |
38 |
39 |
40 |
41 | {items?.map((item) => (
42 |
43 | {item.title}
44 |
45 | {item.quantity as number}
46 |
47 |
48 | ))}
49 |
50 |
51 |
52 |
53 | {type === "driver" && driver && (
54 |
55 | )}
56 | {type === "restaurant" && (
57 |
58 | )}
59 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/delivery-column.tsx:
--------------------------------------------------------------------------------
1 | import { DeliveryDTO, DeliveryStatus, DriverDTO } from "@frontend/lib/types";
2 | import { Container, Heading, Text } from "@medusajs/ui";
3 | import DeliveryCard from "./delivery-card";
4 |
5 | export default async function DeliveryColumn({
6 | title,
7 | deliveries,
8 | statusFilters,
9 | driver,
10 | type,
11 | }: {
12 | title: string;
13 | deliveries: DeliveryDTO[];
14 | statusFilters?: DeliveryStatus[];
15 | driver?: DriverDTO;
16 | type: "restaurant" | "driver";
17 | }) {
18 | const columnDeliveries = deliveries?.filter(
19 | (d) => d && statusFilters?.includes(d.delivery_status)
20 | );
21 |
22 | return (
23 |
24 |
25 | {title}
26 | {columnDeliveries?.length > 0 ? (
27 | columnDeliveries.map((delivery) => (
28 |
34 | ))
35 | ) : (
36 |
37 | {title} will show up here
38 |
39 | )}
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/driver/create-category-drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { RestaurantDTO } from "@frontend/lib/types";
4 | import { Plus } from "@medusajs/icons";
5 | import { Button, Drawer, Text } from "@medusajs/ui";
6 |
7 | export function CreateCategoryDrawer({
8 | restaurant,
9 | }: {
10 | restaurant: RestaurantDTO;
11 | }) {
12 | return (
13 |
14 |
15 |
16 |
17 | Create Category
18 |
19 |
20 |
21 |
22 | Creat Menu Category
23 |
24 |
25 |
26 | This is where you create a new category for your restaurant's
27 | menu (not implemented in this demo)
28 |
29 |
30 |
31 |
32 | Cancel
33 |
34 | Save
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/driver/delivery-buttons.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { passDelivery, proceedDelivery } from "@frontend/lib/actions";
4 | import { DeliveryDTO, DeliveryStatus, DriverDTO } from "@frontend/lib/types";
5 | import { Button } from "@medusajs/ui";
6 | import { useState } from "react";
7 |
8 | export default function DriverDeliveryButtons({
9 | delivery,
10 | driver,
11 | }: {
12 | delivery: DeliveryDTO;
13 | driver: DriverDTO;
14 | }) {
15 | const [proceedIsLoading, setProceedIsLoading] = useState(false);
16 | const [declineIsLoading, setDeclineIsLoading] = useState(false);
17 |
18 | const handleProceedDelivery = async () => {
19 | setProceedIsLoading(true);
20 | await proceedDelivery(delivery, driver.id);
21 | };
22 |
23 | const handleDeclineDelivery = async () => {
24 | setDeclineIsLoading(true);
25 | await passDelivery(delivery.id, driver.id);
26 | };
27 |
28 | return (
29 | <>
30 | {delivery.delivery_status === DeliveryStatus.RESTAURANT_ACCEPTED && (
31 |
36 | Pass
37 |
38 | )}
39 | {[
40 | DeliveryStatus.RESTAURANT_ACCEPTED,
41 | DeliveryStatus.READY_FOR_PICKUP,
42 | DeliveryStatus.IN_TRANSIT,
43 | ].includes(delivery.delivery_status) && (
44 |
49 | {delivery.delivery_status === DeliveryStatus.RESTAURANT_ACCEPTED &&
50 | "Claim delivery"}
51 | {delivery.delivery_status === DeliveryStatus.READY_FOR_PICKUP &&
52 | "Set order picked up"}
53 | {delivery.delivery_status === DeliveryStatus.IN_TRANSIT &&
54 | "Set order delivered"}
55 |
56 | )}
57 | >
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/driver/delivery-status-badge.tsx:
--------------------------------------------------------------------------------
1 | import { DeliveryDTO, DeliveryStatus } from "@frontend/lib/types";
2 | import { CircleQuarterSolid } from "@medusajs/icons";
3 | import { Badge } from "@medusajs/ui";
4 |
5 | export async function DriverDeliveryStatusBadge({
6 | delivery,
7 | }: {
8 | delivery: DeliveryDTO;
9 | }) {
10 | switch (delivery.delivery_status) {
11 | case DeliveryStatus.RESTAURANT_ACCEPTED:
12 | return Available ;
13 | case DeliveryStatus.PICKUP_CLAIMED:
14 | return (
15 |
16 |
17 | Queued
18 |
19 | );
20 | case DeliveryStatus.RESTAURANT_PREPARING:
21 | return (
22 |
23 |
24 | Preparing
25 |
26 | );
27 | case DeliveryStatus.READY_FOR_PICKUP:
28 | return (
29 |
30 | Waiting for pickup
31 |
32 | );
33 | case DeliveryStatus.IN_TRANSIT:
34 | return (
35 |
36 |
37 | Out for delivery
38 |
39 | );
40 | case DeliveryStatus.DELIVERED:
41 | return Delivered ;
42 | default:
43 | return {delivery.delivery_status} ;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/menu/create-category-drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { RestaurantDTO } from "@frontend/lib/types";
4 | import { Plus } from "@medusajs/icons";
5 | import { Button, Drawer, Text } from "@medusajs/ui";
6 |
7 | export function CreateCategoryDrawer({
8 | restaurant,
9 | }: {
10 | restaurant: RestaurantDTO;
11 | }) {
12 | return (
13 |
14 |
15 |
16 |
17 | Create Category
18 |
19 |
20 |
21 |
22 | Creat Menu Category
23 |
24 |
25 |
26 | This is where you create a new category for your restaurant's
27 | menu (not implemented in this demo)
28 |
29 |
30 |
31 |
32 | Cancel
33 |
34 | Save
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/menu/create-category-form.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/medusajs/medusa-eats/14270e1ed5913811965ddfca85fc63c9cf0bdd8d/frontend/src/components/dashboard/menu/create-category-form.tsx
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/menu/create-product-drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { RestaurantDTO } from "@frontend/lib/types";
4 | import { Plus } from "@medusajs/icons";
5 | import { HttpTypes } from "@medusajs/types";
6 | import { Button, Drawer, Text } from "@medusajs/ui";
7 | import { CreateProductForm } from "./create-product-form";
8 |
9 | export function CreateProductDrawer({
10 | restaurant,
11 | categories,
12 | }: {
13 | restaurant: RestaurantDTO;
14 | categories: HttpTypes.StoreProductCategory[];
15 | }) {
16 | return (
17 |
18 |
19 |
20 |
21 | Create Menu Item
22 |
23 |
24 |
25 |
26 | Create Menu Item
27 |
28 |
29 |
30 | This is where you create a new item for your restaurant's menu
31 |
32 |
33 |
34 |
35 |
36 | Cancel
37 |
38 |
39 | Save
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/menu/create-product-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { createProduct } from "@frontend/lib/actions";
4 | import { RestaurantDTO } from "@frontend/lib/types";
5 | import { HttpTypes } from "@medusajs/types";
6 | import { Input, Label, Select, Textarea } from "@medusajs/ui";
7 | import { useFormState } from "react-dom";
8 |
9 | export function CreateProductForm({
10 | restaurant,
11 | categories,
12 | }: {
13 | restaurant: RestaurantDTO;
14 | categories: HttpTypes.StoreProductCategory[];
15 | }) {
16 | const [state, formAction] = useFormState(createProduct, null);
17 |
18 | return (
19 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/menu/menu-actions.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { RestaurantDTO } from "@frontend/lib/types";
4 | import { HttpTypes, ProductCategoryDTO } from "@medusajs/types";
5 | import { CreateCategoryDrawer } from "./create-category-drawer";
6 | import { CreateProductDrawer } from "./create-product-drawer";
7 |
8 | export function MenuActions({
9 | restaurant,
10 | categories,
11 | }: {
12 | restaurant: RestaurantDTO;
13 | categories: HttpTypes.StoreProductCategory[];
14 | }) {
15 | return (
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/menu/menu-product-actions.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { deleteProduct } from "@frontend/lib/actions";
4 | import { RestaurantDTO } from "@frontend/lib/types";
5 | import {
6 | EllipsisHorizontal,
7 | PencilSquare,
8 | Spinner,
9 | Trash,
10 | } from "@medusajs/icons";
11 | import { ProductDTO } from "@medusajs/types";
12 | import { DropdownMenu, IconButton } from "@medusajs/ui";
13 | import { useState } from "react";
14 |
15 | export function MenuProductActions({
16 | product,
17 | restaurant,
18 | }: {
19 | product: ProductDTO;
20 | restaurant: RestaurantDTO;
21 | }) {
22 | const [isDeleting, setIsDeleting] = useState(false);
23 |
24 | const handleDelete = async () => {
25 | setIsDeleting(true);
26 | await deleteProduct(product.id, restaurant.id);
27 | setIsDeleting(false);
28 | };
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Edit
41 |
42 |
43 |
44 | {isDeleting ? (
45 |
46 | ) : (
47 |
48 | )}
49 | Delete
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/realtime-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { StatusBadge } from "@medusajs/ui";
4 | import { useRouter } from "next/navigation";
5 | import { useEffect, useTransition } from "react";
6 |
7 | export default function RealtimeClient({
8 | restaurantId,
9 | driverId,
10 | deliveryId,
11 | }: {
12 | restaurantId?: string;
13 | driverId?: string;
14 | deliveryId?: string;
15 | }) {
16 | const [isPending, startTransition] = useTransition();
17 | const router = useRouter();
18 |
19 | let serverUrl = "/api/subscribe";
20 |
21 | if (restaurantId) {
22 | serverUrl += `?restaurant_id=${restaurantId}`;
23 | }
24 |
25 | if (driverId) {
26 | serverUrl += `?driver_id=${driverId}`;
27 | }
28 |
29 | if (deliveryId) {
30 | serverUrl += `?delivery_id=${deliveryId}`;
31 | }
32 |
33 | useEffect(() => {
34 | const source = new EventSource(serverUrl);
35 | const audio = new Audio("/notification.mp3");
36 |
37 | source.onmessage = (message: Record) => {
38 | const data = JSON.parse(message.data);
39 |
40 | data.new && audio.play();
41 |
42 | startTransition(() => {
43 | router.refresh();
44 | });
45 | };
46 |
47 | return () => {
48 | source.close();
49 | };
50 | }, []);
51 |
52 | if (isPending) {
53 | return (
54 |
55 | {deliveryId ? "Syncing order status" : "Syncing deliveries"}
56 |
57 |
58 | );
59 | }
60 |
61 | return (
62 |
63 |
64 | Up to date
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/restaurant/delivery-buttons.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { declineDelivery, proceedDelivery } from "@frontend/lib/actions";
4 | import { DeliveryDTO, DeliveryStatus } from "@frontend/lib/types";
5 | import { Button } from "@medusajs/ui";
6 | import { useState } from "react";
7 |
8 | export default function RestaurantDeliveryButtons({
9 | delivery,
10 | }: {
11 | delivery: DeliveryDTO;
12 | }) {
13 | const [proceedIsLoading, setProceedIsLoading] = useState(false);
14 | const [declineIsLoading, setDeclineIsLoading] = useState(false);
15 |
16 | const handleProceedDelivery = async () => {
17 | setProceedIsLoading(true);
18 | await proceedDelivery(delivery);
19 | };
20 |
21 | const handleDeclineDelivery = async () => {
22 | setDeclineIsLoading(true);
23 | await declineDelivery(delivery.id);
24 | };
25 |
26 | return (
27 | <>
28 | {delivery.delivery_status === DeliveryStatus.PENDING && (
29 |
34 | Decline
35 |
36 | )}
37 | {[
38 | DeliveryStatus.PENDING,
39 | DeliveryStatus.PICKUP_CLAIMED,
40 | DeliveryStatus.RESTAURANT_PREPARING,
41 | ].includes(delivery.delivery_status) && (
42 |
47 | {delivery.delivery_status === DeliveryStatus.PENDING &&
48 | "Accept order"}
49 | {delivery.delivery_status === DeliveryStatus.PICKUP_CLAIMED &&
50 | "Start preparing"}
51 | {delivery.delivery_status === DeliveryStatus.RESTAURANT_PREPARING &&
52 | "Set ready for pickup"}
53 |
54 | )}
55 | >
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/restaurant/delivery-status-badge.tsx:
--------------------------------------------------------------------------------
1 | import { DeliveryDTO, DeliveryStatus } from "@frontend/lib/types";
2 | import { CircleQuarterSolid } from "@medusajs/icons";
3 | import { Badge } from "@medusajs/ui";
4 |
5 | export async function RestaurantDeliveryStatusBadge({
6 | delivery,
7 | }: {
8 | delivery: DeliveryDTO;
9 | }) {
10 | switch (delivery.delivery_status) {
11 | case DeliveryStatus.PENDING:
12 | return New order ;
13 | case DeliveryStatus.RESTAURANT_ACCEPTED:
14 | return (
15 |
16 |
17 | Looking for driver
18 |
19 | );
20 | case DeliveryStatus.PICKUP_CLAIMED:
21 | return (
22 |
23 | Driver found
24 |
25 | );
26 | case DeliveryStatus.RESTAURANT_PREPARING:
27 | return (
28 |
29 |
30 | Preparing
31 |
32 | );
33 | case DeliveryStatus.READY_FOR_PICKUP:
34 | return (
35 |
36 |
37 | Waiting for pickup
38 |
39 | );
40 | case DeliveryStatus.IN_TRANSIT:
41 | return (
42 |
43 |
44 | Out for delivery
45 |
46 | );
47 | case DeliveryStatus.DELIVERED:
48 | return Delivered ;
49 | case DeliveryStatus.RESTAURANT_DECLINED:
50 | return Declined by restaurant ;
51 | default:
52 | return {delivery.delivery_status} ;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/frontend/src/components/dashboard/restaurant/restaurant-status.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { setRestaurantStatus } from "@frontend/lib/actions";
4 | import { RestaurantDTO } from "@frontend/lib/types";
5 | import { Switch } from "@medusajs/ui";
6 | import { useState } from "react";
7 |
8 | export default function RestaurantStatus({
9 | restaurant,
10 | }: {
11 | restaurant: RestaurantDTO;
12 | }) {
13 | const [isOpen, setIsOpen] = useState(restaurant.is_open);
14 |
15 | const handleStatusChange = async () => {
16 | setIsOpen(!isOpen);
17 | await setRestaurantStatus(restaurant.id, !isOpen);
18 | };
19 |
20 | return (
21 |
22 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/components/store/cart/cart-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ShoppingBag } from "@medusajs/icons";
4 | import { DropdownMenu, IconButton } from "@medusajs/ui";
5 | import { CartCounter } from "./cart-counter";
6 | import CartModal from "./cart-modal";
7 | import { useRouter } from "next/navigation";
8 | import { useEffect, useState } from "react";
9 |
10 | export default function CartButton({ cart }: { cart: any }) {
11 | const [open, setOpen] = useState(false);
12 | const router = useRouter();
13 |
14 | useEffect(() => {
15 | const mainElement = window.document.getElementsByTagName("main")[0];
16 |
17 | mainElement.style.filter = "none";
18 | if (open) {
19 | mainElement.style.filter = "blur(3px)";
20 | } else {
21 | mainElement.style.filter = "none";
22 | }
23 |
24 | return () => {
25 | mainElement.style.filter = "none";
26 | };
27 | }, [open]);
28 |
29 | const handleCheckoutClick = () => {
30 | const mainElement = window.document.getElementsByTagName("main")[0];
31 |
32 | mainElement.style.filter = "none";
33 |
34 | setOpen(false);
35 |
36 | router.push("/checkout");
37 | };
38 |
39 | return (
40 |
41 | {
43 | setOpen(isOpen);
44 | }}
45 | open={open}
46 | >
47 |
51 |
52 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/src/components/store/cart/cart-counter.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CartLineItemDTO } from "@medusajs/types";
4 | import { Text, clx } from "@medusajs/ui";
5 |
6 | export function CartCounter({ cart }: { cart: any }) {
7 | const numberOfItems =
8 | cart?.items?.reduce(
9 | (acc: number, item: CartLineItemDTO) => acc + (item.quantity as number),
10 | 0
11 | ) || 0;
12 |
13 | return (
14 | 0,
20 | }
21 | )}
22 | >
23 | {numberOfItems}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/src/components/store/cart/cart-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { removeItemFromCart } from "@frontend/lib/actions";
4 | import { XMark } from "@medusajs/icons";
5 | import { Button, Container, Heading, IconButton, Text } from "@medusajs/ui";
6 | import Image from "next/image";
7 | import { useState } from "react";
8 |
9 | export default function CartModal({
10 | cart,
11 | handleCheckoutClick,
12 | }: {
13 | cart: Record;
14 | handleCheckoutClick: () => void;
15 | }) {
16 | return (
17 |
18 |
19 | Your order
20 |
21 |
22 | {cart && cart.items.length ? (
23 | cart?.items?.map((item: any) => (
24 |
25 | ))
26 | ) : (
27 | No items added yet.
28 | )}
29 |
36 | Go to checkout
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | function CartItem({ item }: { item: Record }) {
44 | const [deleting, setDeleting] = useState(false);
45 |
46 | const deleteItem = async (itemId: string) => {
47 | setDeleting(true);
48 | await removeItemFromCart(itemId);
49 | setDeleting(false);
50 | };
51 |
52 | const thumbnail = item.thumbnail;
53 |
54 | return (
55 |
56 |
63 |
64 |
65 |
66 | {item.title}
67 |
68 |
69 | {item.quantity} x €{item.unit_price}
70 |
71 |
72 |
deleteItem(item.id)}
74 | variant="transparent"
75 | isLoading={deleting}
76 | >
77 |
78 |
79 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/frontend/src/components/store/cart/nav-cart.tsx:
--------------------------------------------------------------------------------
1 | import { retrieveCart } from "@frontend/lib/data";
2 | import { cookies } from "next/headers";
3 | import CartButton from "./cart-button";
4 |
5 | export default async function NavCart() {
6 | const cartId = cookies().get("_medusa_cart_id")?.value;
7 | let cart;
8 |
9 | if (cartId) {
10 | cart = await retrieveCart(cartId);
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/src/components/store/checkout/checkout-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { placeOrder } from "@frontend/lib/actions";
4 | import { HttpTypes } from "@medusajs/types";
5 | import { Badge, Button, Heading, Input, Label, Textarea } from "@medusajs/ui";
6 | import { useFormState, useFormStatus } from "react-dom";
7 |
8 | function Submit() {
9 | const status = useFormStatus();
10 |
11 | return (
12 |
18 | Place Order
19 |
20 | );
21 | }
22 |
23 | export default function CheckoutForm({ cart }: { cart: HttpTypes.StoreCart }) {
24 | const [state, action] = useFormState(placeOrder, { message: "" });
25 |
26 | const restaurantId = cart.metadata?.restaurant_id as string;
27 |
28 | return (
29 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/src/components/store/checkout/order-summary.tsx:
--------------------------------------------------------------------------------
1 | import { retrieveRestaurant } from "@frontend/lib/data";
2 | import { HttpTypes } from "@medusajs/types";
3 | import { Container, Heading, Text } from "@medusajs/ui";
4 | import Image from "next/image";
5 |
6 | export async function OrderSummary({ cart }: { cart: HttpTypes.StoreCart }) {
7 | const restaurant = await retrieveRestaurant(
8 | cart?.metadata?.restaurant_id as string
9 | );
10 |
11 | return (
12 |
13 |
17 | Your order from {restaurant.name}
18 |
19 |
20 | {cart?.items?.map((item: any) => {
21 | const image = item.thumbnail;
22 | return (
23 |
24 |
31 |
32 |
33 | {item.title}
34 |
35 |
36 | {item.quantity} x €{item.unit_price}
37 |
38 |
39 |
40 | );
41 | })}
42 |
43 |
44 | Order total
45 |
46 | €{cart.total as number}
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/frontend/src/components/store/order/lottie-player.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DotLottiePlayer } from "@dotlottie/react-player";
4 |
5 | export default function LottiePlayer({ src }: { src: string }) {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/components/store/order/order-status-content-tab.tsx:
--------------------------------------------------------------------------------
1 | import { DotLottiePlayer } from "@dotlottie/react-player";
2 | import { ProgressTabs } from "@medusajs/ui";
3 |
4 | export function OrderStatusContentTab({ value }: { value: string }) {
5 | return (
6 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/components/store/restaurant/dish-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ProductDTO } from "@medusajs/types";
4 | import { Heading, Text } from "@medusajs/ui";
5 | import { IconButton } from "@medusajs/ui";
6 | import { Plus } from "@medusajs/icons";
7 | import Image from "next/image";
8 | import { addToCart } from "@frontend/lib/actions";
9 | import { useState } from "react";
10 |
11 | export default function DishCard({
12 | product,
13 | restaurantId,
14 | }: {
15 | product: ProductDTO;
16 | restaurantId: string;
17 | }) {
18 | const [isAdding, setIsAdding] = useState(false);
19 |
20 | const handleAdd = async () => {
21 | setIsAdding(true);
22 | await addToCart(product.variants[0].id, restaurantId);
23 | setIsAdding(false);
24 | };
25 |
26 | const thumbnail =
27 | process.env.NEXT_PUBLIC_DEMO_MODE === "true"
28 | ? product.thumbnail?.replace(
29 | "http://localhost:3000",
30 | "https://medusa-eats.vercel.app"
31 | )
32 | : product.thumbnail;
33 |
34 | return (
35 |
36 |
37 | {product.title}
38 | {product.description}
39 |
40 | €
41 | {
42 | //@ts-ignore
43 | product.variants[0].calculated_price.calculated_amount
44 | }
45 |
46 |
47 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/frontend/src/components/store/restaurant/restaurant-categories.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { RestaurantDTO } from "@frontend/lib/types";
4 | import { ProductDTO } from "@medusajs/types";
5 | import { Button, Heading } from "@medusajs/ui";
6 | import { RefObject, createRef, useEffect, useState } from "react";
7 | import DishCard from "./dish-card";
8 |
9 | export default function RestaurantCategories({
10 | categoryProductMap,
11 | restaurant,
12 | }: {
13 | categoryProductMap: Map;
14 | restaurant: RestaurantDTO;
15 | }) {
16 | const [categoryRefs, setCategoryRefs] = useState[]>(
17 | []
18 | );
19 |
20 | useEffect(() => {
21 | setCategoryRefs((prevRefs) =>
22 | Array.from(categoryProductMap).map(
23 | ([_, category], idx) => prevRefs[idx] || createRef()
24 | )
25 | );
26 | }, [categoryProductMap]);
27 |
28 | const scrollIntoView = (idx: number) =>
29 | categoryRefs.length > 0 &&
30 | categoryRefs[idx]?.current?.scrollIntoView({
31 | behavior: "smooth",
32 | block: "center",
33 | });
34 |
35 | return (
36 |
37 |
38 |
39 | {Array.from(categoryProductMap).map(([_, category], idx) => (
40 | scrollIntoView(idx)}
44 | >
45 | {category.category_name}
46 |
47 | ))}
48 |
49 |
50 |
51 | {Array.from(categoryProductMap).map(([categoryId, category], idx) => (
52 |
58 |
59 | {category.category_name}
60 |
61 |
62 | {category.products?.map((product: ProductDTO) => (
63 |
68 | ))}
69 |
70 |
71 | ))}
72 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/frontend/src/components/store/restaurant/restaurant-category.tsx:
--------------------------------------------------------------------------------
1 | import { RestaurantDTO } from "@frontend/lib/types";
2 | import { ClockSolidMini } from "@medusajs/icons";
3 | import { Badge } from "@medusajs/ui";
4 | import { Link } from "next-view-transitions";
5 | import Image from "next/image";
6 |
7 | export default function RestaurantCategory({
8 | restaurants,
9 | categoryName,
10 | }: {
11 | restaurants: RestaurantDTO[];
12 | categoryName?: string;
13 | }) {
14 | return (
15 |
16 |
17 | {categoryName}Popular restaurants near you
18 |
19 |
20 | {restaurants.map((restaurant) => {
21 | const image =
22 | process.env.NEXT_PUBLIC_DEMO_MODE === "true"
23 | ? restaurant.image_url?.replace(
24 | "http://localhost:3000",
25 | "https://medusa-eats.vercel.app"
26 | )
27 | : restaurant.image_url;
28 | return (
29 |
34 | {image && (
35 |
42 | )}
43 |
44 |
45 |
{restaurant.name}
46 |
47 |
48 | 10-20 min
49 |
50 |
51 |
52 | {restaurant.description}
53 |
54 |
55 |
56 | );
57 | })}
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/frontend/src/lib/actions/carts.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { retrieveUser } from "@frontend/lib/data";
4 | import { CartDTO, HttpTypes } from "@medusajs/types";
5 | import { revalidateTag } from "next/cache";
6 | import { cookies } from "next/headers";
7 | import { sdk } from "../config";
8 | import { getAuthHeaders, getCacheTag } from "../data/cookies";
9 |
10 | export async function createCart(
11 | data: HttpTypes.StoreCreateCart,
12 | restaurant_id: string
13 | ): Promise {
14 | const user = await retrieveUser();
15 |
16 | const { regions } = await sdk.store.region.list();
17 |
18 | const region = regions[0];
19 |
20 | const body = {
21 | email: user?.email,
22 | region_id: region.id,
23 | metadata: {
24 | restaurant_id,
25 | },
26 | ...data,
27 | } as HttpTypes.StoreCreateCart & { items: HttpTypes.StoreAddCartLineItem[] };
28 |
29 | try {
30 | const { cart } = await sdk.store.cart.create(
31 | body,
32 | {},
33 | {
34 | ...getAuthHeaders(),
35 | }
36 | );
37 |
38 | cookies().set("_medusa_cart_id", cart.id);
39 |
40 | revalidateTag(getCacheTag("carts"));
41 |
42 | return cart as CartDTO;
43 | } catch (e) {
44 | throw e;
45 | }
46 | }
47 |
48 | export async function addToCart(
49 | variantId: string,
50 | restaurantId: string
51 | ): Promise {
52 | let cartId = cookies().get("_medusa_cart_id")?.value;
53 | let cart;
54 |
55 | if (!cartId) {
56 | cart = await createCart(
57 | {
58 | currency_code: "eur",
59 | },
60 | restaurantId
61 | );
62 |
63 | cartId = cart?.id;
64 | }
65 |
66 | if (!cartId) {
67 | return { message: "Error creating cart" };
68 | }
69 |
70 | try {
71 | const { cart } = await sdk.store.cart.createLineItem(cartId, {
72 | variant_id: variantId,
73 | quantity: 1,
74 | });
75 |
76 | revalidateTag(getCacheTag("carts"));
77 |
78 | return cart as HttpTypes.StoreCart;
79 | } catch (error) {
80 | return { message: "Error adding item to cart" };
81 | }
82 | }
83 |
84 | export async function removeItemFromCart(
85 | lineItemId: string
86 | ): Promise {
87 | try {
88 | const cartId = cookies().get("_medusa_cart_id")?.value;
89 |
90 | if (!cartId) {
91 | return { message: "Error removing item from cart" };
92 | }
93 |
94 | await sdk.store.cart.deleteLineItem(cartId, lineItemId);
95 |
96 | revalidateTag(getCacheTag("carts"));
97 | } catch (error) {
98 | return { message: "Error removing item from cart" };
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/frontend/src/lib/actions/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./carts";
2 | export * from "./checkout";
3 | export * from "./deliveries";
4 | export * from "./restaurants";
5 | export * from "./users";
6 |
--------------------------------------------------------------------------------
/frontend/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | import Medusa from "@medusajs/js-sdk";
2 |
3 | // Defaults to standard port for Medusa server
4 | let MEDUSA_BACKEND_URL = "http://localhost:9000";
5 |
6 | if (process.env.NEXT_PUBLIC_BACKEND_URL) {
7 | MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL;
8 | }
9 |
10 | export const sdk = new Medusa({
11 | baseUrl: MEDUSA_BACKEND_URL,
12 | debug: process.env.NODE_ENV === "development",
13 | publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
14 | });
15 |
--------------------------------------------------------------------------------
/frontend/src/lib/data/carts.ts:
--------------------------------------------------------------------------------
1 | import { sdk } from "../config";
2 | import { getAuthHeaders, getCacheHeaders } from "./cookies";
3 |
4 | export async function retrieveCart(cartId: string) {
5 | const { cart } = await sdk.store.cart.retrieve(
6 | cartId,
7 | {
8 | fields:
9 | "+metadata, +items.*, +items.thumbnail, +items.title, +items.quantity, +items.total, +items.variant",
10 | },
11 | {
12 | ...getAuthHeaders(),
13 | ...getCacheHeaders("carts"),
14 | }
15 | );
16 |
17 | return cart;
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/src/lib/data/categories.ts:
--------------------------------------------------------------------------------
1 | import { HttpTypes } from "@medusajs/types";
2 | import { sdk } from "../config";
3 | import { getAuthHeaders, getCacheHeaders } from "./cookies";
4 |
5 | export async function listCategories(): Promise<
6 | HttpTypes.StoreProductCategory[]
7 | > {
8 | const { product_categories } = await sdk.store.category.list(
9 | {},
10 | {
11 | ...getAuthHeaders(),
12 | ...getCacheHeaders("categories"),
13 | }
14 | );
15 |
16 | return product_categories as HttpTypes.StoreProductCategory[];
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/lib/data/cookies.ts:
--------------------------------------------------------------------------------
1 | import "server-only";
2 | import { cookies, headers } from "next/headers";
3 |
4 | export const getAuthHeaders = (): { authorization: string } | {} => {
5 | const token = cookies().get("_medusa_jwt")?.value;
6 |
7 | if (token) {
8 | return { authorization: `Bearer ${token}` };
9 | }
10 |
11 | return {};
12 | };
13 |
14 | export const getCacheTag = (tag: string): string => {
15 | const cacheId = headers().get("_medusa_cache_id");
16 |
17 | if (cacheId) {
18 | return `${tag}-${cacheId}`;
19 | }
20 |
21 | return "";
22 | };
23 |
24 | export const getCacheHeaders = (
25 | tag: string
26 | ): { next: { tags: string[] } } | {} => {
27 | const cacheTag = getCacheTag(tag);
28 |
29 | if (cacheTag) {
30 | return { next: { tags: [`${cacheTag}`] } };
31 | }
32 |
33 | return {};
34 | };
35 |
--------------------------------------------------------------------------------
/frontend/src/lib/data/deliveries.ts:
--------------------------------------------------------------------------------
1 | import { sdk } from "../config";
2 | import { DeliveryDTO } from "../types";
3 | import { getAuthHeaders, getCacheHeaders } from "./cookies";
4 |
5 | export async function listDeliveries(
6 | filter?: Record
7 | ): Promise {
8 | const { deliveries }: { deliveries: DeliveryDTO[] } = await sdk.client.fetch(
9 | "/store/deliveries",
10 | {
11 | method: "GET",
12 | headers: {
13 | "Content-Type": "application/json",
14 | ...getAuthHeaders(),
15 | ...getCacheHeaders("deliveries"),
16 | },
17 | }
18 | );
19 |
20 | return deliveries;
21 | }
22 |
23 | export async function retrieveDelivery(
24 | deliveryId: string
25 | ): Promise {
26 | const { delivery }: { delivery: DeliveryDTO } = await sdk.client.fetch(
27 | `/store/deliveries/${deliveryId}`,
28 | {
29 | method: "GET",
30 | headers: {
31 | "Content-Type": "application/json",
32 | ...getAuthHeaders(),
33 | ...getCacheHeaders("deliveries"),
34 | },
35 | }
36 | );
37 | return delivery;
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/lib/data/drivers.ts:
--------------------------------------------------------------------------------
1 | import { sdk } from "../config";
2 |
3 | import { DriverDTO } from "@frontend/lib/types";
4 | import { getAuthHeaders, getCacheHeaders } from "./cookies";
5 |
6 | export async function retrieveDriver(driverId: string): Promise {
7 | const {
8 | driver,
9 | }: {
10 | driver: DriverDTO;
11 | } = await sdk.client.fetch(`/store/drivers/${driverId}`, {
12 | method: "GET",
13 | headers: {
14 | "Content-Type": "application/json",
15 | ...getAuthHeaders(),
16 | ...getCacheHeaders("drivers"),
17 | },
18 | });
19 |
20 | return driver;
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/lib/data/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./carts";
2 | export * from "./categories";
3 | export * from "./deliveries";
4 | export * from "./drivers";
5 | export * from "./restaurants";
6 | export * from "./sessions";
7 | export * from "./users";
8 |
--------------------------------------------------------------------------------
/frontend/src/lib/data/restaurants.ts:
--------------------------------------------------------------------------------
1 | import { sdk } from "../config";
2 | import { RestaurantDTO } from "../types";
3 | import { getCacheHeaders } from "./cookies";
4 |
5 | export async function retrieveRestaurant(
6 | restaurantId: string
7 | ): Promise {
8 | const { restaurant }: { restaurant: RestaurantDTO } = await sdk.client.fetch(
9 | `/store/restaurants/${restaurantId}`,
10 | {
11 | method: "GET",
12 | headers: {
13 | ...getCacheHeaders("restaurants"),
14 | },
15 | }
16 | );
17 |
18 | return restaurant;
19 | }
20 |
21 | export async function listRestaurants(
22 | filter?: Record
23 | ): Promise {
24 | const query = new URLSearchParams(filter).toString();
25 |
26 | const { restaurants }: { restaurants: RestaurantDTO[] } =
27 | await sdk.client.fetch(`/store/restaurants?${query}`, {
28 | method: "GET",
29 | headers: {
30 | ...getCacheHeaders("restaurants"),
31 | },
32 | });
33 |
34 | return restaurants;
35 | }
36 |
37 | export async function retrieveRestaurantByHandle(
38 | handle: string
39 | ): Promise {
40 | const { restaurants }: { restaurants: RestaurantDTO[] } =
41 | await sdk.client.fetch(`/store/restaurants?handle=${handle}`, {
42 | method: "GET",
43 | headers: {
44 | ...getCacheHeaders("restaurants"),
45 | },
46 | });
47 |
48 | return restaurants[0];
49 | }
50 |
--------------------------------------------------------------------------------
/frontend/src/lib/data/sessions.ts:
--------------------------------------------------------------------------------
1 | import { jwtVerify } from "jose";
2 | import { revalidateTag } from "next/cache";
3 | import { cookies } from "next/headers";
4 | import "server-only";
5 |
6 | const jwtSecret = process.env.JWT_SECRET || "supersecret";
7 |
8 | export function createSession(token: string) {
9 | if (!token) {
10 | return;
11 | }
12 |
13 | const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
14 |
15 | cookies().set("_medusa_jwt", token, {
16 | httpOnly: true,
17 | secure: process.env.VERCEL_ENV === "production",
18 | expires: expiresAt,
19 | sameSite: "strict",
20 | path: "/",
21 | });
22 | }
23 |
24 | export function retrieveSession() {
25 | const token = cookies().get("_medusa_jwt")?.value;
26 |
27 | if (!token) {
28 | return null;
29 | }
30 |
31 | return token;
32 | }
33 |
34 | export function destroySession() {
35 | cookies().delete("_medusa_jwt");
36 | revalidateTag("user");
37 | }
38 |
39 | export async function decrypt(
40 | session: string | undefined = ""
41 | ): Promise {
42 | try {
43 | // Convert the secret to a CryptoKey
44 | const encoder = new TextEncoder();
45 | const keyData = encoder.encode(jwtSecret);
46 | const cryptoKey = await crypto.subtle.importKey(
47 | "raw",
48 | keyData,
49 | { name: "HMAC", hash: "SHA-256" },
50 | false,
51 | ["verify"]
52 | );
53 |
54 | const { payload } = await jwtVerify(session, cryptoKey, {
55 | algorithms: ["HS256"],
56 | });
57 |
58 | return payload;
59 | } catch (error) {
60 | console.error(error);
61 | return { message: "Error decrypting session" };
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/frontend/src/lib/data/users.ts:
--------------------------------------------------------------------------------
1 | import { UserDTO } from "@medusajs/types";
2 | import { sdk } from "../config";
3 | import { getAuthHeaders, getCacheHeaders } from "./cookies";
4 | import { DriverDTO, RestaurantAdminDTO } from "../types";
5 |
6 | export async function retrieveUser() {
7 | try {
8 | const { user } = await sdk.client.fetch<{
9 | user: RestaurantAdminDTO | DriverDTO | null;
10 | }>("/store/users/me", {
11 | headers: {
12 | ...getAuthHeaders(),
13 | ...getCacheHeaders("users"),
14 | },
15 | });
16 |
17 | return user;
18 | } catch (error) {
19 | console.error(error);
20 | return null;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CartDTO,
3 | CartLineItemDTO,
4 | OrderDTO,
5 | OrderLineItemDTO,
6 | ProductDTO,
7 | } from "@medusajs/types";
8 |
9 | export enum DeliveryStatus {
10 | PENDING = "pending",
11 | RESTAURANT_DECLINED = "restaurant_declined",
12 | RESTAURANT_ACCEPTED = "restaurant_accepted",
13 | PICKUP_CLAIMED = "pickup_claimed",
14 | RESTAURANT_PREPARING = "restaurant_preparing",
15 | READY_FOR_PICKUP = "ready_for_pickup",
16 | IN_TRANSIT = "in_transit",
17 | DELIVERED = "delivered",
18 | }
19 |
20 | export interface RestaurantDTO {
21 | id: string;
22 | handle: string;
23 | is_open: boolean;
24 | name: string;
25 | description?: string;
26 | address: string;
27 | phone: string;
28 | email: string;
29 | image_url?: string;
30 | created_at: Date;
31 | updated_at: Date;
32 | products?: ProductDTO[];
33 | deliveries: DeliveryDTO[];
34 | }
35 |
36 | export interface RestaurantAdminDTO {
37 | id: string;
38 | restaurant_id: string;
39 | first_name: string;
40 | last_name: string;
41 | email: string;
42 | created_at: Date;
43 | updated_at: Date;
44 | }
45 |
46 | export interface RestaurantProductDTO {
47 | restaurant_id: string;
48 | product_id: string;
49 | }
50 |
51 | export interface CreateRestaurantDTO {
52 | name: string;
53 | handle: string;
54 | address: string;
55 | phone: string;
56 | email: string;
57 | image_url?: string;
58 | is_open?: boolean;
59 | }
60 |
61 | export type UpdateRestaurantDTO = Partial;
62 |
63 | export interface CreateRestaurantAdminDTO {
64 | email: string;
65 | first_name: string;
66 | last_name: string;
67 | restaurant_id: string;
68 | }
69 |
70 | export interface CreateAdminInviteDTO {
71 | resadm_id: string;
72 | role?: string | null;
73 | email?: string;
74 | }
75 |
76 | export interface DeliveryDTO {
77 | id: string;
78 | transaction_id: string;
79 | driver_id?: string;
80 | cart: CartDTO;
81 | order?: OrderDTO;
82 | restaurant: RestaurantDTO;
83 | delivered_at?: Date;
84 | delivery_status: DeliveryStatus;
85 | created_at: Date;
86 | updated_at: Date;
87 | eta?: Date;
88 | items: DeliveryItemDTO[];
89 | }
90 |
91 | export type DeliveryItemDTO = (CartLineItemDTO | OrderLineItemDTO) & {
92 | quantity: number;
93 | };
94 |
95 | export interface DriverDTO {
96 | id: string;
97 | first_name: string;
98 | last_name: string;
99 | email: string;
100 | phone: string;
101 | avatar_url?: string;
102 | created_at: Date;
103 | updated_at: Date;
104 | }
105 |
106 | export interface DeliveryDriverDTO {
107 | id: string;
108 | delivery_id: string;
109 | driver_id: string;
110 | }
111 |
112 | export interface CreateDeliveryDTO {
113 | restaurant_id: string;
114 | cart_id: string;
115 | }
116 |
117 | export interface UpdateDeliveryDTO extends Partial {
118 | id: string;
119 | }
120 |
121 | export interface CreateDriverDTO {
122 | first_name: string;
123 | last_name: string;
124 | email: string;
125 | phone: string;
126 | avatar_url?: string;
127 | }
128 |
129 | export interface UpdateDriverDTO extends Partial {
130 | id: string;
131 | }
132 |
133 | export interface CreateDeliveryDriverDTO {
134 | delivery_id: string;
135 | driver_id: string;
136 | }
137 |
--------------------------------------------------------------------------------
/frontend/src/lib/util/constants.ts:
--------------------------------------------------------------------------------
1 | export const lottieMap = {
2 | 0: "https://lottie.host/67c627a5-4927-49cf-a10a-2ee2cda10240/yJXnlXpKOB.json",
3 | 1: "https://lottie.host/4a5a3197-f57e-483e-807f-5d29a127fbb1/18xz3Pezjf.json",
4 | 2: "https://lottie.host/f5a7812b-f7f8-493f-bf6a-fb9dc726c7ea/6sV39AMWTK.json",
5 | 3: "https://lottie.host/f5a7812b-f7f8-493f-bf6a-fb9dc726c7ea/6sV39AMWTK.json",
6 | 4: "https://lottie.host/60d3f375-2b70-4562-9d1f-db2eb166c969/Ewh5jyRxll.json",
7 | 5: "https://lottie.host/6f82efae-7ec1-4873-8c9b-6eb099bd1f74/IRjGqIZnhm.json",
8 | 6: "https://lottie.host/420a29ff-d1b5-43db-b901-77cc294ebd07/DezvWQOIoi.json",
9 | };
10 |
--------------------------------------------------------------------------------
/frontend/src/lib/util/get-numeric-status.ts:
--------------------------------------------------------------------------------
1 | import { DeliveryStatus } from "@frontend/lib/types";
2 |
3 | export const getNumericStatus = (status: DeliveryStatus) => {
4 | switch (status) {
5 | case DeliveryStatus.PENDING:
6 | return 0;
7 | case DeliveryStatus.RESTAURANT_ACCEPTED:
8 | return 1;
9 | case DeliveryStatus.PICKUP_CLAIMED:
10 | return 2;
11 | case DeliveryStatus.RESTAURANT_PREPARING:
12 | return 3;
13 | case DeliveryStatus.READY_FOR_PICKUP:
14 | return 4;
15 | case DeliveryStatus.IN_TRANSIT:
16 | return 5;
17 | case DeliveryStatus.DELIVERED:
18 | return 6;
19 | default:
20 | return 0;
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/frontend/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 |
3 | async function setCacheId(request: NextRequest, response: NextResponse) {
4 | const cacheId = request.cookies.get("_medusa_cache_id")?.value;
5 |
6 | if (cacheId) {
7 | return cacheId;
8 | }
9 |
10 | const newCacheId = crypto.randomUUID();
11 | response.cookies.set("_medusa_cache_id", newCacheId, {
12 | maxAge: 60 * 60 * 24,
13 | });
14 |
15 | return newCacheId;
16 | }
17 |
18 | export async function middleware(request: NextRequest) {
19 | const response = NextResponse.next();
20 | const cacheId = await setCacheId(request, response);
21 |
22 | response.headers.set("x-medusa-cache-id", cacheId);
23 |
24 | return response;
25 | }
26 |
27 | export const config = {
28 | // Match all paths except for the ones that are static assets
29 | matcher: "/((?!api|_next/static|_next/image|favicon.ico).*)",
30 | };
31 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}",
9 | ],
10 | theme: {
11 | extend: {
12 | backgroundImage: {
13 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
14 | "gradient-conic":
15 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
16 | },
17 | },
18 | },
19 | plugins: [require("tailwind-scrollbar-hide")],
20 | presets: [require("@medusajs/ui-preset")],
21 | darkMode: "class",
22 | };
23 | export default config;
24 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@frontend/*": ["./src/*"],
22 | "@backend/*": ["../backend/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------