Initial commit
commit
d805cefeae
@ -0,0 +1,12 @@
|
|||||||
|
let ignore = [`**/dist`]
|
||||||
|
|
||||||
|
// Jest needs to compile this code, but generally we don't want this copied
|
||||||
|
// to output folders
|
||||||
|
if (process.env.NODE_ENV !== `test`) {
|
||||||
|
ignore.push(`**/__tests__`)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
presets: [["babel-preset-medusa-package"], ["@babel/preset-typescript"]],
|
||||||
|
ignore,
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
JWT_SECRET=something
|
||||||
|
COOKIE_SECRET=something
|
||||||
|
|
||||||
|
DATABASE_TYPE="postgres"
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
allow:
|
||||||
|
- dependency-type: production
|
||||||
|
groups:
|
||||||
|
medusa:
|
||||||
|
patterns:
|
||||||
|
- "@medusajs*"
|
||||||
|
- "medusa*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "@medusajs*"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
- dependency-name: "medusa*"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
/dist
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
/uploads
|
||||||
|
/node_modules
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
coverage
|
||||||
|
|
||||||
|
!src/**
|
||||||
|
|
||||||
|
./tsconfig.tsbuildinfo
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
medusa-db.sql
|
||||||
|
build
|
||||||
|
.cache
|
||||||
|
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
@ -0,0 +1 @@
|
|||||||
|
nodeLinker: node-modules
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
"store": {
|
||||||
|
"currencies": [
|
||||||
|
"eur",
|
||||||
|
"usd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"users": [],
|
||||||
|
"regions": [
|
||||||
|
{
|
||||||
|
"id": "test-region-eu",
|
||||||
|
"name": "EU",
|
||||||
|
"currency_code": "eur",
|
||||||
|
"tax_rate": 0,
|
||||||
|
"payment_providers": [
|
||||||
|
"manual"
|
||||||
|
],
|
||||||
|
"fulfillment_providers": [
|
||||||
|
"manual"
|
||||||
|
],
|
||||||
|
"countries": [
|
||||||
|
"gb",
|
||||||
|
"de",
|
||||||
|
"dk",
|
||||||
|
"se",
|
||||||
|
"fr",
|
||||||
|
"es",
|
||||||
|
"it"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test-region-na",
|
||||||
|
"name": "NA",
|
||||||
|
"currency_code": "usd",
|
||||||
|
"tax_rate": 0,
|
||||||
|
"payment_providers": [
|
||||||
|
"manual"
|
||||||
|
],
|
||||||
|
"fulfillment_providers": [
|
||||||
|
"manual"
|
||||||
|
],
|
||||||
|
"countries": [
|
||||||
|
"us",
|
||||||
|
"ca"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shipping_options": [
|
||||||
|
{
|
||||||
|
"name": "PostFake Standard",
|
||||||
|
"region_id": "test-region-eu",
|
||||||
|
"provider_id": "manual",
|
||||||
|
"data": {
|
||||||
|
"id": "manual-fulfillment"
|
||||||
|
},
|
||||||
|
"price_type": "flat_rate",
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PostFake Express",
|
||||||
|
"region_id": "test-region-eu",
|
||||||
|
"provider_id": "manual",
|
||||||
|
"data": {
|
||||||
|
"id": "manual-fulfillment"
|
||||||
|
},
|
||||||
|
"price_type": "flat_rate",
|
||||||
|
"amount": 1500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PostFake Return",
|
||||||
|
"region_id": "test-region-eu",
|
||||||
|
"provider_id": "manual",
|
||||||
|
"data": {
|
||||||
|
"id": "manual-fulfillment"
|
||||||
|
},
|
||||||
|
"price_type": "flat_rate",
|
||||||
|
"is_return": true,
|
||||||
|
"amount": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "I want to return it myself",
|
||||||
|
"region_id": "test-region-eu",
|
||||||
|
"provider_id": "manual",
|
||||||
|
"data": {
|
||||||
|
"id": "manual-fulfillment"
|
||||||
|
},
|
||||||
|
"price_type": "flat_rate",
|
||||||
|
"is_return": true,
|
||||||
|
"amount": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FakeEx Standard",
|
||||||
|
"region_id": "test-region-na",
|
||||||
|
"provider_id": "manual",
|
||||||
|
"data": {
|
||||||
|
"id": "manual-fulfillment"
|
||||||
|
},
|
||||||
|
"price_type": "flat_rate",
|
||||||
|
"amount": 800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FakeEx Express",
|
||||||
|
"region_id": "test-region-na",
|
||||||
|
"provider_id": "manual",
|
||||||
|
"data": {
|
||||||
|
"id": "manual-fulfillment"
|
||||||
|
},
|
||||||
|
"price_type": "flat_rate",
|
||||||
|
"amount": 1200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FakeEx Return",
|
||||||
|
"region_id": "test-region-na",
|
||||||
|
"provider_id": "manual",
|
||||||
|
"data": {
|
||||||
|
"id": "manual-fulfillment"
|
||||||
|
},
|
||||||
|
"price_type": "flat_rate",
|
||||||
|
"is_return": true,
|
||||||
|
"amount": 800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "I want to return it myself",
|
||||||
|
"region_id": "test-region-na",
|
||||||
|
"provider_id": "manual",
|
||||||
|
"data": {
|
||||||
|
"id": "manual-fulfillment"
|
||||||
|
},
|
||||||
|
"price_type": "flat_rate",
|
||||||
|
"is_return": true,
|
||||||
|
"amount": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"products": [],
|
||||||
|
"categories": [],
|
||||||
|
"publishable_api_keys": [
|
||||||
|
{
|
||||||
|
"title": "Development"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,50 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const { GracefulShutdownServer } = require("medusa-core-utils")
|
||||||
|
|
||||||
|
const loaders = require("@medusajs/medusa/dist/loaders/index").default
|
||||||
|
|
||||||
|
;(async() => {
|
||||||
|
async function start() {
|
||||||
|
const app = express()
|
||||||
|
const directory = process.cwd()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { container } = await loaders({
|
||||||
|
directory,
|
||||||
|
expressApp: app
|
||||||
|
})
|
||||||
|
const configModule = container.resolve("configModule")
|
||||||
|
const port = process.env.PORT ?? configModule.projectConfig.port ?? 9000
|
||||||
|
|
||||||
|
const server = GracefulShutdownServer.create(
|
||||||
|
app.listen(port, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(`Server is ready on port: ${port}`)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
const gracefulShutDown = () => {
|
||||||
|
server
|
||||||
|
.shutdown()
|
||||||
|
.then(() => {
|
||||||
|
console.info("Gracefully stopping the server.")
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Error received when shutting down the server.", e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
process.on("SIGTERM", gracefulShutDown)
|
||||||
|
process.on("SIGINT", gracefulShutDown)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error starting server", err)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await start()
|
||||||
|
})()
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
const dotenv = require("dotenv");
|
||||||
|
|
||||||
|
let ENV_FILE_NAME = "";
|
||||||
|
switch (process.env.NODE_ENV) {
|
||||||
|
case "production":
|
||||||
|
ENV_FILE_NAME = ".env.production";
|
||||||
|
break;
|
||||||
|
case "staging":
|
||||||
|
ENV_FILE_NAME = ".env.staging";
|
||||||
|
break;
|
||||||
|
case "test":
|
||||||
|
ENV_FILE_NAME = ".env.test";
|
||||||
|
break;
|
||||||
|
case "development":
|
||||||
|
default:
|
||||||
|
ENV_FILE_NAME = ".env";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dotenv.config({ path: process.cwd() + "/" + ENV_FILE_NAME });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// CORS when consuming Medusa from admin
|
||||||
|
const ADMIN_CORS =
|
||||||
|
process.env.ADMIN_CORS || "http://localhost:7000,http://localhost:7001";
|
||||||
|
|
||||||
|
// CORS to avoid issues when consuming Medusa from a client
|
||||||
|
const STORE_CORS = process.env.STORE_CORS || "http://localhost:8000";
|
||||||
|
|
||||||
|
const DATABASE_URL =
|
||||||
|
process.env.DATABASE_URL || "postgres://localhost/medusa-starter-default";
|
||||||
|
|
||||||
|
const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
`medusa-fulfillment-manual`,
|
||||||
|
`medusa-payment-manual`,
|
||||||
|
{
|
||||||
|
resolve: `@medusajs/file-local`,
|
||||||
|
options: {
|
||||||
|
upload_dir: "uploads",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolve: "@medusajs/admin",
|
||||||
|
/** @type {import('@medusajs/admin').PluginOptions} */
|
||||||
|
options: {
|
||||||
|
autoRebuild: true,
|
||||||
|
develop: {
|
||||||
|
open: process.env.OPEN_BROWSER !== "false",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const modules = {
|
||||||
|
/*eventBus: {
|
||||||
|
resolve: "@medusajs/event-bus-redis",
|
||||||
|
options: {
|
||||||
|
redisUrl: REDIS_URL
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cacheService: {
|
||||||
|
resolve: "@medusajs/cache-redis",
|
||||||
|
options: {
|
||||||
|
redisUrl: REDIS_URL
|
||||||
|
}
|
||||||
|
},*/
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {import('@medusajs/medusa').ConfigModule["projectConfig"]} */
|
||||||
|
const projectConfig = {
|
||||||
|
jwtSecret: process.env.JWT_SECRET,
|
||||||
|
cookieSecret: process.env.COOKIE_SECRET,
|
||||||
|
store_cors: STORE_CORS,
|
||||||
|
database_url: DATABASE_URL,
|
||||||
|
admin_cors: ADMIN_CORS,
|
||||||
|
// Uncomment the following lines to enable REDIS
|
||||||
|
// redis_url: REDIS_URL
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {import('@medusajs/medusa').ConfigModule} */
|
||||||
|
module.exports = {
|
||||||
|
projectConfig,
|
||||||
|
plugins,
|
||||||
|
modules,
|
||||||
|
};
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"name": "medusa-starter-default",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "A starter for Medusa projects.",
|
||||||
|
"author": "Medusa (https://medusajs.com)",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"sqlite",
|
||||||
|
"postgres",
|
||||||
|
"typescript",
|
||||||
|
"ecommerce",
|
||||||
|
"headless",
|
||||||
|
"medusa"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"clean": "cross-env ./node_modules/.bin/rimraf dist",
|
||||||
|
"build": "cross-env npm run clean && npm run build:server && npm run build:admin",
|
||||||
|
"build:server": "cross-env npm run clean && tsc -p tsconfig.server.json",
|
||||||
|
"build:admin": "cross-env medusa-admin build",
|
||||||
|
"watch": "cross-env tsc --watch",
|
||||||
|
"test": "cross-env jest",
|
||||||
|
"seed": "cross-env medusa seed -f ./data/seed.json",
|
||||||
|
"start": "cross-env npm run build && medusa start",
|
||||||
|
"start:custom": "cross-env npm run build && node --preserve-symlinks --trace-warnings index.js",
|
||||||
|
"dev": "cross-env npm run build:server && medusa develop"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@medusajs/admin": "7.1.8",
|
||||||
|
"@medusajs/cache-inmemory": "^1.8.9",
|
||||||
|
"@medusajs/cache-redis": "^1.8.9",
|
||||||
|
"@medusajs/event-bus-local": "^1.9.7",
|
||||||
|
"@medusajs/event-bus-redis": "^1.8.10",
|
||||||
|
"@medusajs/file-local": "^1.0.2",
|
||||||
|
"@medusajs/medusa": "1.18.1",
|
||||||
|
"@tanstack/react-query": "4.22.0",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "16.3.1",
|
||||||
|
"express": "^4.17.2",
|
||||||
|
"medusa-fulfillment-manual": "^1.1.38",
|
||||||
|
"medusa-interfaces": "^1.3.7",
|
||||||
|
"medusa-payment-manual": "^1.0.24",
|
||||||
|
"medusa-payment-stripe": "^6.0.5",
|
||||||
|
"prism-react-renderer": "^2.0.4",
|
||||||
|
"typeorm": "^0.3.16"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.14.3",
|
||||||
|
"@babel/core": "^7.14.3",
|
||||||
|
"@babel/preset-typescript": "^7.21.4",
|
||||||
|
"@medusajs/medusa-cli": "^1.3.21",
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/jest": "^27.4.0",
|
||||||
|
"@types/node": "^17.0.8",
|
||||||
|
"babel-preset-medusa-package": "^1.1.19",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"jest": "^27.3.1",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"ts-jest": "^27.0.7",
|
||||||
|
"ts-loader": "^9.2.6",
|
||||||
|
"typescript": "^4.5.2"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"globals": {
|
||||||
|
"ts-jest": {
|
||||||
|
"tsconfig": "tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"testPathIgnorePatterns": [
|
||||||
|
"/node_modules/",
|
||||||
|
"<rootDir>/node_modules/"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$",
|
||||||
|
"transform": {
|
||||||
|
".ts": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "./coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
ComputerDesktopSolid,
|
||||||
|
CurrencyDollarSolid,
|
||||||
|
NextJs
|
||||||
|
} from "@medusajs/icons";
|
||||||
|
import { IconBadge, Heading, Text } from "@medusajs/ui";
|
||||||
|
|
||||||
|
const OrderDetailDefault = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text size="small" className="mb-6">
|
||||||
|
You finished the setup guide 🎉 You now have your first order. Feel free
|
||||||
|
to play around with the order management functionalities, such as
|
||||||
|
capturing payment, creating fulfillments, and more.
|
||||||
|
</Text>
|
||||||
|
<Heading
|
||||||
|
level="h2"
|
||||||
|
className="text-ui-fg-base pt-6 border-t border-ui-border-base border-solid mb-2"
|
||||||
|
>
|
||||||
|
Start developing with Medusa
|
||||||
|
</Heading>
|
||||||
|
<Text size="small">
|
||||||
|
Medusa is a completely customizable commerce solution. We've curated
|
||||||
|
some essential guides to kickstart your development with Medusa.
|
||||||
|
</Text>
|
||||||
|
<div className="grid grid-cols-3 gap-4 mt-6 pb-6 mb-6 border-b border-ui-border-base border-solid auto-rows-fr">
|
||||||
|
<a
|
||||||
|
href={`https://docs.medusajs.com/modules/overview?ref=onboarding`}
|
||||||
|
target="_blank"
|
||||||
|
className="flex"
|
||||||
|
>
|
||||||
|
<div className="p-3 rounded-rounded items-start flex bg-ui-bg-subtle shadow-elevation-card-rest hover:shadow-elevation-card-hover">
|
||||||
|
<div className="mr-4">
|
||||||
|
<div className="bg-ui-bg-base rounded-lg border border-ui-border-strong p-1 flex justify-center items-center">
|
||||||
|
<IconBadge>
|
||||||
|
<CurrencyDollarSolid />
|
||||||
|
</IconBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
size="xsmall"
|
||||||
|
weight="plus"
|
||||||
|
className="mb-1 text-ui-fg-base"
|
||||||
|
>
|
||||||
|
Add Commerce Features
|
||||||
|
</Text>
|
||||||
|
<Text size="small">
|
||||||
|
Learn about all available commerce features and how to
|
||||||
|
add them in your storefront
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://docs.medusajs.com/recipes/?ref=onboarding"
|
||||||
|
target="_blank"
|
||||||
|
className="flex"
|
||||||
|
>
|
||||||
|
<div className="p-3 rounded-rounded items-start flex bg-ui-bg-subtle shadow-elevation-card-rest hover:shadow-elevation-card-hover">
|
||||||
|
<div className="mr-4">
|
||||||
|
<div className="bg-ui-bg-base rounded-lg border border-ui-border-strong p-1 flex justify-center items-center">
|
||||||
|
<IconBadge>
|
||||||
|
<ComputerDesktopSolid />
|
||||||
|
</IconBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
size="xsmall"
|
||||||
|
weight="plus"
|
||||||
|
className="mb-1 text-ui-fg-base"
|
||||||
|
>
|
||||||
|
Build Custom Use Cases
|
||||||
|
</Text>
|
||||||
|
<Text size="small">
|
||||||
|
Build a marketplace, subscription-based purchases,
|
||||||
|
or your custom use-cases.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`https://docs.medusajs.com/starters/nextjs-medusa-starter?ref=onboarding`}
|
||||||
|
target="_blank"
|
||||||
|
className="flex"
|
||||||
|
>
|
||||||
|
<div className="p-3 rounded-rounded items-start flex bg-ui-bg-subtle shadow-elevation-card-rest hover:shadow-elevation-card-hover">
|
||||||
|
<div className="mr-4">
|
||||||
|
<div className="bg-ui-bg-base rounded-lg border border-ui-border-strong p-1 flex justify-center items-center">
|
||||||
|
<IconBadge>
|
||||||
|
<NextJs />
|
||||||
|
</IconBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text
|
||||||
|
size="xsmall"
|
||||||
|
weight="plus"
|
||||||
|
className="mb-1 text-ui-fg-base"
|
||||||
|
>
|
||||||
|
Install Next.js Quickstart
|
||||||
|
</Text>
|
||||||
|
<Text size="small">
|
||||||
|
Install and use the Next.js storefront with
|
||||||
|
your commerce store.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
You can find more useful guides in{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.medusajs.com/?ref=onboarding"
|
||||||
|
target="_blank"
|
||||||
|
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
|
||||||
|
>
|
||||||
|
our documentation
|
||||||
|
</a>
|
||||||
|
. If you like Medusa, please{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/medusajs/medusa"
|
||||||
|
target="_blank"
|
||||||
|
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
|
||||||
|
>
|
||||||
|
star us on GitHub
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderDetailDefault;
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import React, { useEffect, useMemo } from "react"
|
||||||
|
import {
|
||||||
|
useAdminPublishableApiKeys,
|
||||||
|
useAdminCreatePublishableApiKey
|
||||||
|
} from "medusa-react"
|
||||||
|
import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow"
|
||||||
|
import { Button, CodeBlock, Text } from "@medusajs/ui"
|
||||||
|
|
||||||
|
const ProductDetailDefault = ({ onNext, isComplete, data }: StepContentProps) => {
|
||||||
|
const { publishable_api_keys: keys, isLoading, refetch } = useAdminPublishableApiKeys({
|
||||||
|
offset: 0,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
const createPublishableApiKey = useAdminCreatePublishableApiKey()
|
||||||
|
|
||||||
|
const api_key = useMemo(() => keys?.[0]?.id || "", [keys])
|
||||||
|
const backendUrl = process.env.MEDUSA_BACKEND_URL === "/" || process.env.MEDUSA_ADMIN_BACKEND_URL === "/" ?
|
||||||
|
location.origin :
|
||||||
|
process.env.MEDUSA_BACKEND_URL || process.env.MEDUSA_ADMIN_BACKEND_URL || "http://location:9000"
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !keys?.length) {
|
||||||
|
createPublishableApiKey.mutate({
|
||||||
|
"title": "Development"
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [isLoading, keys])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Text>On this page, you can view your product's details and edit them.</Text>
|
||||||
|
<Text>
|
||||||
|
You can preview your product using Medusa's Store APIs. You can copy any
|
||||||
|
of the following code snippets to try it out.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{!isLoading && (
|
||||||
|
<CodeBlock snippets={[
|
||||||
|
{
|
||||||
|
label: "cURL",
|
||||||
|
language: "bash",
|
||||||
|
code: `curl "${backendUrl}/store/products/${data?.product_id}"${api_key ? ` -H "x-publishable-key: ${api_key}"` : ``}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Medusa JS Client",
|
||||||
|
language: "js",
|
||||||
|
code: `// Install the JS Client in your storefront project: @medusajs/medusa-js\n\nimport Medusa from "@medusajs/medusa-js"\n\nconst medusa = new Medusa(${api_key ? `{ publishableApiKey: "${api_key}"}` : ``})\nconst product = await medusa.products.retrieve("${data?.product_id}")\nconsole.log(product.id)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Medusa React",
|
||||||
|
language: "tsx",
|
||||||
|
code: `// Install the React SDK and required dependencies in your storefront project:\n// medusa-react @tanstack/react-query @medusajs/medusa\n\nimport { useProduct } from "medusa-react"\n\nconst { product } = useProduct("${data?.product_id}")\nconsole.log(product.id)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "@medusajs/product",
|
||||||
|
language: "tsx",
|
||||||
|
code: `// Install the Product module in a serverless project, such as a Next.js storefront: @medusajs/product\n\nimport {\ninitialize as initializeProductModule,\n} from "@medusajs/product"\n\n// in an async function, or you can use promises\nasync () => {\n // ...\n const productService = await initializeProductModule()\n const products = await productService.list({\n id: "${data?.product_id}",\n })\n\n console.log(products[0])\n}`,
|
||||||
|
},
|
||||||
|
]} className="my-6">
|
||||||
|
<CodeBlock.Header />
|
||||||
|
<CodeBlock.Body />
|
||||||
|
</CodeBlock>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<a
|
||||||
|
href={`${backendUrl}/store/products/${data?.product_id}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Button variant="secondary" size="base">
|
||||||
|
Open preview in browser
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
{!isComplete && (
|
||||||
|
<Button variant="primary" size="base" onClick={() => onNext()}>
|
||||||
|
Next step
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductDetailDefault;
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
useAdminCreateProduct,
|
||||||
|
useAdminCreateCollection,
|
||||||
|
useMedusa
|
||||||
|
} from "medusa-react";
|
||||||
|
import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow";
|
||||||
|
import { Button, Text } from "@medusajs/ui";
|
||||||
|
import getSampleProducts from "../../../../utils/sample-products";
|
||||||
|
import prepareRegions from "../../../../utils/prepare-region";
|
||||||
|
|
||||||
|
const ProductsListDefault = ({ onNext, isComplete }: StepContentProps) => {
|
||||||
|
const { mutateAsync: createCollection, isLoading: collectionLoading } =
|
||||||
|
useAdminCreateCollection();
|
||||||
|
const { mutateAsync: createProduct, isLoading: productLoading } =
|
||||||
|
useAdminCreateProduct();
|
||||||
|
const { client } = useMedusa()
|
||||||
|
|
||||||
|
const isLoading = useMemo(() =>
|
||||||
|
collectionLoading || productLoading,
|
||||||
|
[collectionLoading, productLoading]
|
||||||
|
);
|
||||||
|
|
||||||
|
const createSample = async () => {
|
||||||
|
try {
|
||||||
|
const { collection } = await createCollection({
|
||||||
|
title: "Merch",
|
||||||
|
handle: "merch",
|
||||||
|
});
|
||||||
|
|
||||||
|
const regions = await prepareRegions(client)
|
||||||
|
|
||||||
|
const sampleProducts = getSampleProducts({
|
||||||
|
regions,
|
||||||
|
collection_id: collection.id
|
||||||
|
})
|
||||||
|
const { product } = await createProduct(sampleProducts[0]);
|
||||||
|
onNext(product);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text className="mb-2">
|
||||||
|
Create a product and set its general details such as title and
|
||||||
|
description, its price, options, variants, images, and more. You'll then
|
||||||
|
use the product to create a sample order.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
You can create a product by clicking the "New Product" button below.
|
||||||
|
Alternatively, if you're not ready to create your own product, we can
|
||||||
|
create a sample one for you.
|
||||||
|
</Text>
|
||||||
|
{!isComplete && (
|
||||||
|
<div className="flex gap-2 mt-6">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="base"
|
||||||
|
onClick={() => createSample()}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create sample product
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductsListDefault;
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { useAdminProduct } from "medusa-react";
|
||||||
|
import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow";
|
||||||
|
import { Button, Text } from "@medusajs/ui";
|
||||||
|
|
||||||
|
const ProductDetailNextjs = ({ onNext, isComplete, data }: StepContentProps) => {
|
||||||
|
const { product, isLoading: productIsLoading } = useAdminProduct(data?.product_id)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Text>
|
||||||
|
We have now created a few sample products in your Medusa store. You can scroll down to see what the Product Detail view looks like in the Admin dashboard.
|
||||||
|
This is also the view you use to edit existing products.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
To view the products in your store, you can visit the Next.js Storefront that was installed with <code>create-medusa-app</code>.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
The Next.js Storefront Starter is a template that helps you start building an ecommerce store with Medusa.
|
||||||
|
You control the code for the storefront and you can customize it further to fit your specific needs.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Click the button below to view the products in your Next.js Storefront.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Having trouble? Click{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.medusajs.com/starters/nextjs-medusa-starter#troubleshooting-nextjs-storefront-not-working"
|
||||||
|
target="_blank"
|
||||||
|
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-6">
|
||||||
|
<a
|
||||||
|
href={`http://localhost:8000/products/${product?.handle}?onboarding=true`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Button variant="primary" size="base" isLoading={productIsLoading}>
|
||||||
|
Open Next.js Storefront
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
{!isComplete && (
|
||||||
|
<Button variant="secondary" size="base" onClick={() => onNext()}>
|
||||||
|
Next step
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductDetailNextjs
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
useAdminCreateProduct,
|
||||||
|
useAdminCreateCollection,
|
||||||
|
useMedusa
|
||||||
|
} from "medusa-react";
|
||||||
|
import { StepContentProps } from "../../../../widgets/onboarding-flow/onboarding-flow";
|
||||||
|
import { Button, Text } from "@medusajs/ui";
|
||||||
|
import { AdminPostProductsReq, Product } from "@medusajs/medusa";
|
||||||
|
import getSampleProducts from "../../../../utils/sample-products";
|
||||||
|
import prepareRegions from "../../../../utils/prepare-region";
|
||||||
|
|
||||||
|
const ProductsListNextjs = ({ onNext, isComplete }: StepContentProps) => {
|
||||||
|
const { mutateAsync: createCollection, isLoading: collectionLoading } =
|
||||||
|
useAdminCreateCollection();
|
||||||
|
const { mutateAsync: createProduct, isLoading: productLoading } =
|
||||||
|
useAdminCreateProduct();
|
||||||
|
const { client } = useMedusa()
|
||||||
|
|
||||||
|
const isLoading = collectionLoading || productLoading;
|
||||||
|
|
||||||
|
const createSample = async () => {
|
||||||
|
try {
|
||||||
|
const { collection } = await createCollection({
|
||||||
|
title: "Merch",
|
||||||
|
handle: "merch",
|
||||||
|
});
|
||||||
|
|
||||||
|
const regions = await prepareRegions(client)
|
||||||
|
|
||||||
|
const tryCreateProduct = async (sampleProduct: AdminPostProductsReq): Promise<Product | null> => {
|
||||||
|
try {
|
||||||
|
return (await createProduct(sampleProduct)).product
|
||||||
|
} catch {
|
||||||
|
// ignore if product already exists
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let product: Product
|
||||||
|
const sampleProducts = getSampleProducts({
|
||||||
|
regions,
|
||||||
|
collection_id: collection.id
|
||||||
|
})
|
||||||
|
await Promise.all(
|
||||||
|
sampleProducts.map(async (sampleProduct, index) => {
|
||||||
|
const createdProduct = await tryCreateProduct(sampleProduct)
|
||||||
|
if (index === 0 && createProduct) {
|
||||||
|
product = createdProduct
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
onNext(product);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text className="mb-2">
|
||||||
|
Products is Medusa represent the products you sell. You can set their general details including a
|
||||||
|
title and description. Each product has options and variants, and you can set a price for each variant.
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Click the button below to create sample products.
|
||||||
|
</Text>
|
||||||
|
{!isComplete && (
|
||||||
|
<div className="flex gap-2 mt-6">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="base"
|
||||||
|
onClick={() => createSample()}
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
Create sample products
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductsListNextjs;
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
|
import React from "react";
|
||||||
|
import { CheckCircleSolid, CircleMiniSolid } from "@medusajs/icons";
|
||||||
|
import { Heading, Text, clx } from "@medusajs/ui";
|
||||||
|
import ActiveCircleDottedLine from "./icons/active-circle-dotted-line";
|
||||||
|
|
||||||
|
type AccordionItemProps = AccordionPrimitive.AccordionItemProps & {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
tooltip?: string;
|
||||||
|
forceMountContent?: true;
|
||||||
|
headingSize?: "small" | "medium" | "large";
|
||||||
|
customTrigger?: React.ReactNode;
|
||||||
|
complete?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
triggerable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Accordion: React.FC<
|
||||||
|
| (AccordionPrimitive.AccordionSingleProps &
|
||||||
|
React.RefAttributes<HTMLDivElement>)
|
||||||
|
| (AccordionPrimitive.AccordionMultipleProps &
|
||||||
|
React.RefAttributes<HTMLDivElement>)
|
||||||
|
> & {
|
||||||
|
Item: React.FC<AccordionItemProps>;
|
||||||
|
} = ({ children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Root {...props}>{children}</AccordionPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item: React.FC<AccordionItemProps> = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
description,
|
||||||
|
required,
|
||||||
|
tooltip,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
complete,
|
||||||
|
headingSize = "large",
|
||||||
|
customTrigger = undefined,
|
||||||
|
forceMountContent = undefined,
|
||||||
|
active,
|
||||||
|
triggerable,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
{...props}
|
||||||
|
className={clx(
|
||||||
|
"border-grey-20 group border-t last:mb-0",
|
||||||
|
"py-1 px-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AccordionPrimitive.Header className="px-1">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center justify-center p-[10px]">
|
||||||
|
{complete ? (
|
||||||
|
<CheckCircleSolid className="text-ui-fg-interactive" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{active && (
|
||||||
|
<ActiveCircleDottedLine
|
||||||
|
size={20}
|
||||||
|
className="text-ui-fg-interactive rounded-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!active && (
|
||||||
|
<CircleMiniSolid className="text-ui-fg-muted" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Heading level="h3" className={clx("text-ui-fg-base")}>
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
</div>
|
||||||
|
<AccordionPrimitive.Trigger>
|
||||||
|
{customTrigger || <MorphingTrigger />}
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</div>
|
||||||
|
{subtitle && (
|
||||||
|
<Text as="span" size="small" className="mt-1">
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
forceMount={forceMountContent}
|
||||||
|
className={clx(
|
||||||
|
"radix-state-closed:animate-accordion-close radix-state-open:animate-accordion-open radix-state-closed:pointer-events-none px-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="inter-base-regular group-radix-state-closed:animate-accordion-close">
|
||||||
|
{description && <Text>{description}</Text>}
|
||||||
|
<div className="w-full">{children}</div>
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
</AccordionPrimitive.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Accordion.Item = Item;
|
||||||
|
|
||||||
|
const MorphingTrigger = () => {
|
||||||
|
return (
|
||||||
|
<div className="btn-ghost rounded-rounded group relative p-[6px]">
|
||||||
|
<div className="h-5 w-5">
|
||||||
|
<span className="bg-grey-50 rounded-circle group-radix-state-open:rotate-90 absolute inset-y-[31.75%] left-[48%] right-1/2 w-[1.5px] duration-300" />
|
||||||
|
<span className="bg-grey-50 rounded-circle group-radix-state-open:rotate-90 group-radix-state-open:left-1/2 group-radix-state-open:right-1/2 absolute inset-x-[31.75%] top-[48%] bottom-1/2 h-[1.5px] duration-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Accordion;
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { Text, clx } from "@medusajs/ui"
|
||||||
|
|
||||||
|
type CardProps = {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
children?: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card = ({
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
className
|
||||||
|
}: CardProps) => {
|
||||||
|
return (
|
||||||
|
<div className={clx(
|
||||||
|
"p-4 rounded-lg gap-3",
|
||||||
|
"flex items-start shadow-elevation-card-rest",
|
||||||
|
"bg-ui-bg-subtle",
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{icon}
|
||||||
|
<Text size="base" className="text-ui-fg-subtle">{children}</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
import IconProps from "../../../types/icon-type";
|
||||||
|
|
||||||
|
const ActiveCircleDottedLine: React.FC<IconProps> = ({
|
||||||
|
size = "24",
|
||||||
|
color = "currentColor",
|
||||||
|
...attributes
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg" {...attributes}>
|
||||||
|
<g filter="url(#filter0_d_8860_2802)">
|
||||||
|
<rect x="3" y="3" width="20" height="20" rx="10" fill="white"/>
|
||||||
|
<path d="M15.5 5.93589C13.884 5.3547 12.116 5.3547 10.5 5.93589" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M5.63037 11.632C5.94283 9.94471 6.82561 8.41606 8.13082 7.30209" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M8.13082 18.6979C6.82563 17.5839 5.94286 16.0552 5.63037 14.368" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M10.5 20.0641C12.116 20.6453 13.884 20.6453 15.5 20.0641" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M20.3696 11.632C20.0571 9.94471 19.1744 8.41606 17.8691 7.30209" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M17.8691 18.6979C19.1743 17.5839 20.0571 16.0552 20.3696 14.368" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_8860_2802" x="0" y="0" width="26" height="26" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feMorphology radius="3" operator="dilate" in="SourceAlpha" result="effect1_dropShadow_8860_2802"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.231373 0 0 0 0 0.509804 0 0 0 0 0.964706 0 0 0 0.2 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_8860_2802"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_8860_2802" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActiveCircleDottedLine;
|
||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,8 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
type IconProps = {
|
||||||
|
color?: string
|
||||||
|
size?: string | number
|
||||||
|
} & React.SVGAttributes<SVGElement>
|
||||||
|
|
||||||
|
export default IconProps
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { Store } from "@medusajs/medusa"
|
||||||
|
import type Medusa from "@medusajs/medusa-js"
|
||||||
|
import { ExtendedStoreDTO } from "@medusajs/medusa/dist/types/store"
|
||||||
|
|
||||||
|
export default async function prepareRegions (client: Medusa) {
|
||||||
|
let { regions } = await client.admin.regions.list()
|
||||||
|
if (!regions.length) {
|
||||||
|
let { store } = await client.admin.store.retrieve()
|
||||||
|
if (!store.currencies) {
|
||||||
|
store = (await client.admin.store.update({
|
||||||
|
currencies: ["eur"]
|
||||||
|
})).store as ExtendedStoreDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
regions = [(await client.admin.regions.create(getSampleRegion(store))).region]
|
||||||
|
}
|
||||||
|
|
||||||
|
return regions
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSampleRegion (store: Store) {
|
||||||
|
return {
|
||||||
|
name: "EU",
|
||||||
|
currency_code: store.currencies[0].code,
|
||||||
|
tax_rate: 0,
|
||||||
|
payment_providers: [
|
||||||
|
"manual"
|
||||||
|
],
|
||||||
|
fulfillment_providers: [
|
||||||
|
"manual"
|
||||||
|
],
|
||||||
|
countries: [
|
||||||
|
"gb",
|
||||||
|
"de",
|
||||||
|
"dk",
|
||||||
|
"se",
|
||||||
|
"fr",
|
||||||
|
"es",
|
||||||
|
"it"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { Region } from "@medusajs/medusa";
|
||||||
|
import type Medusa from "@medusajs/medusa-js"
|
||||||
|
|
||||||
|
export default async function prepareShippingOptions (client: Medusa, region: Region) {
|
||||||
|
let { shipping_options } = await client.admin.shippingOptions.list()
|
||||||
|
if (!shipping_options.length) {
|
||||||
|
shipping_options = [(await client.admin.shippingOptions.create({
|
||||||
|
"name": "PostFake Standard",
|
||||||
|
"region_id": region.id,
|
||||||
|
"provider_id": "manual",
|
||||||
|
"data": {
|
||||||
|
"id": "manual-fulfillment"
|
||||||
|
},
|
||||||
|
"price_type": "flat_rate",
|
||||||
|
"amount": 1000
|
||||||
|
})).shipping_option]
|
||||||
|
}
|
||||||
|
|
||||||
|
return shipping_options
|
||||||
|
}
|
||||||
@ -0,0 +1,666 @@
|
|||||||
|
import { AdminPostProductsReq, Region } from "@medusajs/medusa"
|
||||||
|
|
||||||
|
type SampleProductsOptions = {
|
||||||
|
regions: Region[]
|
||||||
|
collection_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// can't use the ProductStatus imported
|
||||||
|
// from the core within admin cusotmizations
|
||||||
|
enum ProductStatus {
|
||||||
|
PUBLISHED = "published"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function getSampleProducts ({
|
||||||
|
regions,
|
||||||
|
collection_id
|
||||||
|
}: SampleProductsOptions): AdminPostProductsReq[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "Medusa T-Shirt",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
collection_id,
|
||||||
|
discountable: true,
|
||||||
|
subtitle: null,
|
||||||
|
description: "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.",
|
||||||
|
handle: "medusa-t-shirt",
|
||||||
|
is_giftcard: false,
|
||||||
|
weight: 400,
|
||||||
|
images: [
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png",
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png",
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png",
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png"
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Color",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S / Black",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2200
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "S"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "Black"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "S / White",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2200
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "S"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "White"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "M / Black",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2200
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "Black"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "M / White",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2200
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "White"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "L / Black",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2200
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "L"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "Black"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "L / White",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2200
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "L"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "White"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "XL / Black",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2200
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "XL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "Black"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "XL / White",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2200
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "XL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "White"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Medusa Sweatshirt",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
discountable: true,
|
||||||
|
collection_id,
|
||||||
|
subtitle: null,
|
||||||
|
description: "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
|
||||||
|
handle: "sweatshirt",
|
||||||
|
is_giftcard: false,
|
||||||
|
weight: 400,
|
||||||
|
images: [
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png"
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 3350
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "S"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "M",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 3350
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "M"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "L",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 3350
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "L"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "XL",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 3350
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "XL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Medusa Sweatpants",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
discountable: true,
|
||||||
|
collection_id,
|
||||||
|
subtitle: null,
|
||||||
|
description: "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.",
|
||||||
|
handle: "sweatpants",
|
||||||
|
is_giftcard: false,
|
||||||
|
weight: 400,
|
||||||
|
images: [
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png",
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png"
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 3350
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "S"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "M",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 3350
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "M"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "L",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 3350
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "L"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "XL",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 3350
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "XL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Medusa Shorts",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
discountable: true,
|
||||||
|
collection_id,
|
||||||
|
subtitle: null,
|
||||||
|
description: "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.",
|
||||||
|
handle: "shorts",
|
||||||
|
is_giftcard: false,
|
||||||
|
weight: 400,
|
||||||
|
images: [
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png",
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png"
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2850
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "S"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "M",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2850
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "M"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "L",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2850
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "L"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "XL",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 2850
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "XL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Medusa Hoodie",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
discountable: true,
|
||||||
|
collection_id,
|
||||||
|
subtitle: null,
|
||||||
|
description: "Reimagine the feeling of a classic hoodie. With our cotton hoodie, everyday essentials no longer have to be ordinary.",
|
||||||
|
handle: "hoodie",
|
||||||
|
is_giftcard: false,
|
||||||
|
weight: 400,
|
||||||
|
images: [
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_front.png",
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/black_hoodie_back.png"
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 4150
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "S"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "M",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 4150
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "M"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "L",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 4150
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "L"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "XL",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 4150
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "XL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Medusa Longsleeve",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
discountable: true,
|
||||||
|
collection_id,
|
||||||
|
subtitle: null,
|
||||||
|
description: "Reimagine the feeling of a classic longsleeve. With our cotton longsleeve, everyday essentials no longer have to be ordinary.",
|
||||||
|
handle: "longsleeve",
|
||||||
|
is_giftcard: false,
|
||||||
|
weight: 400,
|
||||||
|
images: [
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-front.png",
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/ls-black-back.png"
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "S",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 4150
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "S"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "M",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 4150
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "M"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "L",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 4150
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "L"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "XL",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 4150
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "XL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Medusa Coffee Mug",
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
discountable: true,
|
||||||
|
collection_id,
|
||||||
|
subtitle: null,
|
||||||
|
description: "Every programmer's best friend.",
|
||||||
|
handle: "coffee-mug",
|
||||||
|
is_giftcard: false,
|
||||||
|
weight: 400,
|
||||||
|
images: [
|
||||||
|
"https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png"
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Size",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "One Size",
|
||||||
|
prices: regions.map((region) => {
|
||||||
|
return {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
amount: 1200
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "One Size"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
inventory_quantity: 100,
|
||||||
|
manage_inventory: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,502 @@
|
|||||||
|
import { OrderDetailsWidgetProps, ProductDetailsWidgetProps, WidgetConfig, WidgetProps } from "@medusajs/admin";
|
||||||
|
import { useAdminCustomPost, useAdminCustomQuery, useMedusa } from "medusa-react";
|
||||||
|
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
|
import { useNavigate, useSearchParams, useLocation } from "react-router-dom";
|
||||||
|
import { OnboardingState } from "../../../models/onboarding";
|
||||||
|
import {
|
||||||
|
AdminOnboardingUpdateStateReq,
|
||||||
|
OnboardingStateRes,
|
||||||
|
UpdateOnboardingStateInput,
|
||||||
|
} from "../../../types/onboarding";
|
||||||
|
import OrderDetailDefault from "../../components/onboarding-flow/default/orders/order-detail";
|
||||||
|
import OrdersListDefault from "../../components/onboarding-flow/default/orders/orders-list";
|
||||||
|
import ProductDetailDefault from "../../components/onboarding-flow/default/products/product-detail";
|
||||||
|
import ProductsListDefault from "../../components/onboarding-flow/default/products/products-list";
|
||||||
|
import { Button, Container, Heading, Text, clx } from "@medusajs/ui";
|
||||||
|
import Accordion from "../../components/shared/accordion";
|
||||||
|
import GetStarted from "../../components/shared/icons/get-started";
|
||||||
|
import { Order, Product } from "@medusajs/medusa";
|
||||||
|
import ProductsListNextjs from "../../components/onboarding-flow/nextjs/products/products-list";
|
||||||
|
import ProductDetailNextjs from "../../components/onboarding-flow/nextjs/products/product-detail";
|
||||||
|
import OrdersListNextjs from "../../components/onboarding-flow/nextjs/orders/orders-list";
|
||||||
|
import OrderDetailNextjs from "../../components/onboarding-flow/nextjs/orders/order-detail";
|
||||||
|
|
||||||
|
type STEP_ID =
|
||||||
|
| "create_product"
|
||||||
|
| "preview_product"
|
||||||
|
| "create_order"
|
||||||
|
| "setup_finished"
|
||||||
|
| "create_product_nextjs"
|
||||||
|
| "preview_product_nextjs"
|
||||||
|
| "create_order_nextjs"
|
||||||
|
| "setup_finished_nextjs"
|
||||||
|
|
||||||
|
type OnboardingWidgetProps = WidgetProps | ProductDetailsWidgetProps | OrderDetailsWidgetProps
|
||||||
|
|
||||||
|
export type StepContentProps = OnboardingWidgetProps & {
|
||||||
|
onNext?: Function;
|
||||||
|
isComplete?: boolean;
|
||||||
|
data?: OnboardingState;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Step = {
|
||||||
|
id: STEP_ID;
|
||||||
|
title: string;
|
||||||
|
component: React.FC<StepContentProps>;
|
||||||
|
onNext?: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
const QUERY_KEY = ["onboarding_state"];
|
||||||
|
|
||||||
|
const OnboardingFlow = (props: OnboardingWidgetProps) => {
|
||||||
|
// create custom hooks for custom endpoints
|
||||||
|
const { data, isLoading } = useAdminCustomQuery<
|
||||||
|
undefined,
|
||||||
|
OnboardingStateRes
|
||||||
|
>("/onboarding", QUERY_KEY);
|
||||||
|
const { mutate } = useAdminCustomPost<
|
||||||
|
AdminOnboardingUpdateStateReq,
|
||||||
|
OnboardingStateRes
|
||||||
|
>("/onboarding", QUERY_KEY);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
// will be used if onboarding step
|
||||||
|
// is passed as a path parameter
|
||||||
|
const { client } = useMedusa();
|
||||||
|
|
||||||
|
// get current step from custom endpoint
|
||||||
|
const currentStep: STEP_ID | undefined = useMemo(() => {
|
||||||
|
return data?.status
|
||||||
|
?.current_step as STEP_ID
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// initialize some state
|
||||||
|
const [openStep, setOpenStep] = useState(currentStep);
|
||||||
|
const [completed, setCompleted] = useState(false);
|
||||||
|
|
||||||
|
// this method is used to move from one step to the next
|
||||||
|
const setStepComplete = ({
|
||||||
|
step_id,
|
||||||
|
extraData,
|
||||||
|
onComplete,
|
||||||
|
}: {
|
||||||
|
step_id: STEP_ID;
|
||||||
|
extraData?: UpdateOnboardingStateInput;
|
||||||
|
onComplete?: () => void;
|
||||||
|
}) => {
|
||||||
|
const next = steps[findStepIndex(step_id) + 1];
|
||||||
|
mutate({ current_step: next.id, ...extraData }, {
|
||||||
|
onSuccess: onComplete
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// this is useful if you want to change the current step
|
||||||
|
// using a path parameter. It can only be changed if the passed
|
||||||
|
// step in the path parameter is the next step.
|
||||||
|
const [ searchParams ] = useSearchParams()
|
||||||
|
|
||||||
|
// the steps are set based on the
|
||||||
|
// onboarding type
|
||||||
|
const steps: Step[] = useMemo(() => {
|
||||||
|
{
|
||||||
|
switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) {
|
||||||
|
case 'nextjs':
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "create_product_nextjs",
|
||||||
|
title: "Create Products",
|
||||||
|
component: ProductsListNextjs,
|
||||||
|
onNext: (product: Product) => {
|
||||||
|
setStepComplete({
|
||||||
|
step_id: "create_product_nextjs",
|
||||||
|
extraData: { product_id: product.id },
|
||||||
|
onComplete: () => {
|
||||||
|
if (!location.pathname.startsWith(`/a/products/${product.id}`)) {
|
||||||
|
navigate(`/a/products/${product.id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "preview_product_nextjs",
|
||||||
|
title: "Preview Product in your Next.js Storefront",
|
||||||
|
component: ProductDetailNextjs,
|
||||||
|
onNext: () => {
|
||||||
|
setStepComplete({
|
||||||
|
step_id: "preview_product_nextjs",
|
||||||
|
onComplete: () => navigate(`/a/orders`),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "create_order_nextjs",
|
||||||
|
title: "Create an Order using your Next.js Storefront",
|
||||||
|
component: OrdersListNextjs,
|
||||||
|
onNext: (order: Order) => {
|
||||||
|
setStepComplete({
|
||||||
|
step_id: "create_order_nextjs",
|
||||||
|
onComplete: () => {
|
||||||
|
if (!location.pathname.startsWith(`/a/orders/${order.id}`)) {
|
||||||
|
navigate(`/a/orders/${order.id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "setup_finished_nextjs",
|
||||||
|
title: "Setup Finished: Continue Building your Ecommerce Store",
|
||||||
|
component: OrderDetailNextjs,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "create_product",
|
||||||
|
title: "Create Product",
|
||||||
|
component: ProductsListDefault,
|
||||||
|
onNext: (product: Product) => {
|
||||||
|
setStepComplete({
|
||||||
|
step_id: "create_product",
|
||||||
|
extraData: { product_id: product.id },
|
||||||
|
onComplete: () => {
|
||||||
|
if (!location.pathname.startsWith(`/a/products/${product.id}`)) {
|
||||||
|
navigate(`/a/products/${product.id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "preview_product",
|
||||||
|
title: "Preview Product",
|
||||||
|
component: ProductDetailDefault,
|
||||||
|
onNext: () => {
|
||||||
|
setStepComplete({
|
||||||
|
step_id: "preview_product",
|
||||||
|
onComplete: () => navigate(`/a/orders`),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "create_order",
|
||||||
|
title: "Create an Order",
|
||||||
|
component: OrdersListDefault,
|
||||||
|
onNext: (order: Order) => {
|
||||||
|
setStepComplete({
|
||||||
|
step_id: "create_order",
|
||||||
|
onComplete: () => {
|
||||||
|
if (!location.pathname.startsWith(`/a/orders/${order.id}`)) {
|
||||||
|
navigate(`/a/orders/${order.id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "setup_finished",
|
||||||
|
title: "Setup Finished: Start developing with Medusa",
|
||||||
|
component: OrderDetailDefault,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [location.pathname])
|
||||||
|
|
||||||
|
// used to retrieve the index of a step by its ID
|
||||||
|
const findStepIndex = useCallback((step_id: STEP_ID) => {
|
||||||
|
return steps.findIndex((step) => step.id === step_id)
|
||||||
|
}, [steps])
|
||||||
|
|
||||||
|
// used to check if a step is completed
|
||||||
|
const isStepComplete = useCallback((step_id: STEP_ID) => {
|
||||||
|
return findStepIndex(currentStep) > findStepIndex(step_id)
|
||||||
|
}, [findStepIndex, currentStep]);
|
||||||
|
|
||||||
|
// this is used to retrieve the data necessary
|
||||||
|
// to move to the next onboarding step
|
||||||
|
const getOnboardingParamStepData = useCallback(async (onboardingStep: string, data?: {
|
||||||
|
orderId?: string,
|
||||||
|
productId?: string,
|
||||||
|
}) => {
|
||||||
|
switch (onboardingStep) {
|
||||||
|
case "setup_finished_nextjs":
|
||||||
|
case "setup_finished":
|
||||||
|
if (!data?.orderId && "order" in props) {
|
||||||
|
return props.order
|
||||||
|
}
|
||||||
|
const orderId = data?.orderId || searchParams.get("order_id")
|
||||||
|
if (orderId) {
|
||||||
|
return (await client.admin.orders.retrieve(orderId)).order
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error ("Required `order_id` parameter was not passed as a parameter")
|
||||||
|
case "preview_product_nextjs":
|
||||||
|
case "preview_product":
|
||||||
|
if (!data?.productId && "product" in props) {
|
||||||
|
return props.product
|
||||||
|
}
|
||||||
|
const productId = data?.productId || searchParams.get("product_id")
|
||||||
|
if (productId) {
|
||||||
|
return (await client.admin.products.retrieve(productId)).product
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error ("Required `product_id` parameter was not passed as a parameter")
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}, [searchParams, props])
|
||||||
|
|
||||||
|
const isProductCreateStep = useMemo(() => {
|
||||||
|
return currentStep === "create_product" ||
|
||||||
|
currentStep === "create_product_nextjs"
|
||||||
|
}, [currentStep])
|
||||||
|
|
||||||
|
const isOrderCreateStep = useMemo(() => {
|
||||||
|
return currentStep === "create_order" ||
|
||||||
|
currentStep === "create_order_nextjs"
|
||||||
|
}, [currentStep])
|
||||||
|
|
||||||
|
// used to change the open step when the current
|
||||||
|
// step is retrieved from custom endpoints
|
||||||
|
useEffect(() => {
|
||||||
|
setOpenStep(currentStep);
|
||||||
|
|
||||||
|
if (findStepIndex(currentStep) === steps.length - 1) setCompleted(true);
|
||||||
|
}, [currentStep, findStepIndex]);
|
||||||
|
|
||||||
|
// used to check if the user created a product and has entered its details page
|
||||||
|
// the step is changed to the next one
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.pathname.startsWith("/a/products/prod_") && isProductCreateStep && "product" in props) {
|
||||||
|
// change to the preview product step
|
||||||
|
const currentStepIndex = findStepIndex(currentStep)
|
||||||
|
steps[currentStepIndex].onNext?.(props.product)
|
||||||
|
}
|
||||||
|
}, [location.pathname, isProductCreateStep])
|
||||||
|
|
||||||
|
// used to check if the user created an order and has entered its details page
|
||||||
|
// the step is changed to the next one.
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.pathname.startsWith("/a/orders/order_") && isOrderCreateStep && "order" in props) {
|
||||||
|
// change to the preview product step
|
||||||
|
const currentStepIndex = findStepIndex(currentStep)
|
||||||
|
steps[currentStepIndex].onNext?.(props.order)
|
||||||
|
}
|
||||||
|
}, [location.pathname, isOrderCreateStep])
|
||||||
|
|
||||||
|
// used to check if the `onboarding_step` path
|
||||||
|
// parameter is passed and, if so, moves to that step
|
||||||
|
// only if it's the next step and its necessary data is passed
|
||||||
|
useEffect(() => {
|
||||||
|
const onboardingStep = searchParams.get("onboarding_step") as STEP_ID
|
||||||
|
const onboardingStepIndex = findStepIndex(onboardingStep)
|
||||||
|
if (onboardingStep && onboardingStepIndex !== -1 && onboardingStep !== openStep) {
|
||||||
|
// change current step to the onboarding step
|
||||||
|
const openStepIndex = findStepIndex(openStep)
|
||||||
|
|
||||||
|
if (onboardingStepIndex !== openStepIndex + 1) {
|
||||||
|
// can only go forward one step
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve necessary data and trigger the next function
|
||||||
|
getOnboardingParamStepData(onboardingStep)
|
||||||
|
.then((data) => {
|
||||||
|
steps[openStepIndex].onNext?.(data)
|
||||||
|
})
|
||||||
|
.catch((e) => console.error(e))
|
||||||
|
}
|
||||||
|
}, [searchParams, openStep, getOnboardingParamStepData])
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isLoading &&
|
||||||
|
data?.status?.is_complete &&
|
||||||
|
!localStorage.getItem("override_onboarding_finish")
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// a method that will be triggered when
|
||||||
|
// the setup is started
|
||||||
|
const onStart = () => {
|
||||||
|
mutate({ current_step: steps[0].id });
|
||||||
|
navigate(`/a/products`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// a method that will be triggered when
|
||||||
|
// the setup is completed
|
||||||
|
const onComplete = () => {
|
||||||
|
setCompleted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// a method that will be triggered when
|
||||||
|
// the setup is closed
|
||||||
|
const onHide = () => {
|
||||||
|
mutate({ is_complete: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// used to get text for get started header
|
||||||
|
const getStartedText = () => {
|
||||||
|
switch(process.env.MEDUSA_ADMIN_ONBOARDING_TYPE) {
|
||||||
|
case "nextjs":
|
||||||
|
return "Learn the basics of Medusa by creating your first order using the Next.js storefront."
|
||||||
|
default:
|
||||||
|
return "Learn the basics of Medusa by creating your first order."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container className={clx(
|
||||||
|
"text-ui-fg-subtle px-0 pt-0 pb-4",
|
||||||
|
{
|
||||||
|
"mb-4": completed
|
||||||
|
}
|
||||||
|
)}>
|
||||||
|
<Accordion
|
||||||
|
type="single"
|
||||||
|
value={openStep}
|
||||||
|
onValueChange={(value) => setOpenStep(value as STEP_ID)}
|
||||||
|
>
|
||||||
|
<div className={clx(
|
||||||
|
"flex py-6 px-8",
|
||||||
|
{
|
||||||
|
"items-start": completed,
|
||||||
|
"items-center": !completed
|
||||||
|
}
|
||||||
|
)}>
|
||||||
|
<div className="w-12 h-12 p-1 flex justify-center items-center rounded-full bg-ui-bg-base shadow-elevation-card-rest mr-4">
|
||||||
|
<GetStarted />
|
||||||
|
</div>
|
||||||
|
{!completed ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Heading level="h1" className="text-ui-fg-base">Get started</Heading>
|
||||||
|
<Text>
|
||||||
|
{getStartedText()}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex items-start gap-2">
|
||||||
|
{!!currentStep ? (
|
||||||
|
<>
|
||||||
|
{currentStep === steps[steps.length - 1].id ? (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="base"
|
||||||
|
onClick={() => onComplete()}
|
||||||
|
>
|
||||||
|
Complete Setup
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="base"
|
||||||
|
onClick={() => onHide()}
|
||||||
|
>
|
||||||
|
Cancel Setup
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="base"
|
||||||
|
onClick={() => onHide()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="base"
|
||||||
|
onClick={() => onStart()}
|
||||||
|
>
|
||||||
|
Begin setup
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Heading level="h1" className="text-ui-fg-base">
|
||||||
|
Thank you for completing the setup guide!
|
||||||
|
</Heading>
|
||||||
|
<Text>
|
||||||
|
This whole experience was built using our new{" "}
|
||||||
|
<strong>widgets</strong> feature.
|
||||||
|
<br /> You can find out more details and build your own by
|
||||||
|
following{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.medusajs.com/admin/onboarding?ref=onboarding"
|
||||||
|
target="_blank"
|
||||||
|
className="text-blue-500 font-semibold"
|
||||||
|
>
|
||||||
|
our guide
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex items-start gap-2">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="base"
|
||||||
|
onClick={() => onHide()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
{(!completed ? steps : steps.slice(-1)).map((step) => {
|
||||||
|
const isComplete = isStepComplete(step.id);
|
||||||
|
const isCurrent = currentStep === step.id;
|
||||||
|
return (
|
||||||
|
<Accordion.Item
|
||||||
|
title={step.title}
|
||||||
|
value={step.id}
|
||||||
|
headingSize="medium"
|
||||||
|
active={isCurrent}
|
||||||
|
complete={isComplete}
|
||||||
|
disabled={!isComplete && !isCurrent}
|
||||||
|
key={step.id}
|
||||||
|
{...(!isComplete &&
|
||||||
|
!isCurrent && {
|
||||||
|
customTrigger: <></>,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="pl-14 pb-6 pr-7">
|
||||||
|
<step.component
|
||||||
|
onNext={step.onNext}
|
||||||
|
isComplete={isComplete}
|
||||||
|
data={data?.status}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Accordion.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Accordion>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const config: WidgetConfig = {
|
||||||
|
zone: [
|
||||||
|
"product.list.before",
|
||||||
|
"product.details.before",
|
||||||
|
"order.list.before",
|
||||||
|
"order.details.before",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OnboardingFlow;
|
||||||
@ -0,0 +1,179 @@
|
|||||||
|
# Custom API Routes
|
||||||
|
|
||||||
|
You may define custom API Routes by putting files in the `/api` directory that export functions returning an express router or a collection of express routers.
|
||||||
|
Medusa supports adding custom API Routes using a file based approach. This means that you can add files in the `/api` directory and the files path will be used as the API Route path. For example, if you add a file called `/api/store/custom/route.ts` it will be available on the `/store/custom` API Route.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||||
|
|
||||||
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
||||||
|
res.json({
|
||||||
|
message: "Hello world!",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported HTTP methods
|
||||||
|
|
||||||
|
The file based routing supports the following HTTP methods:
|
||||||
|
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- PUT
|
||||||
|
- PATCH
|
||||||
|
- DELETE
|
||||||
|
- OPTIONS
|
||||||
|
- HEAD
|
||||||
|
|
||||||
|
You can define a handler for each of these methods by exporting a function with the name of the method in the paths `route.ts` file. For example, if you want to define a handler for the `GET`, `POST`, and `PUT` methods, you can do so by exporting functions with the names `GET`, `POST`, and `PUT`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||||
|
|
||||||
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
||||||
|
// Handle GET requests
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
||||||
|
// Handle POST requests
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(req: MedusaRequest, res: MedusaResponse) {
|
||||||
|
// Handle PUT requests
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
You can define parameters in the path of your route by using wrapping the parameter name in square brackets. For example, if you want to define a route that takes a `productId` parameter, you can do so by creating a file called `/api/products/[productId]/route.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type {
|
||||||
|
MedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
ProductService,
|
||||||
|
} from "@medusajs/medusa";
|
||||||
|
|
||||||
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
||||||
|
const { productId } = req.params;
|
||||||
|
|
||||||
|
const productService: ProductService = req.scope.resolve("productService");
|
||||||
|
|
||||||
|
const product = await productService.retrieve(productId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
product,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to define a route that takes multiple parameters, you can do so by adding multiple parameters in the path. It is important that each parameter is given a unique name. For example, if you want to define a route that takes both a `productId` and a `variantId` parameter, you can do so by creating a file called `/api/products/[productId]/variants/[variantId]/route.ts`. Duplicate parameter names are not allowed, and will result in an error.
|
||||||
|
|
||||||
|
## Using the container
|
||||||
|
|
||||||
|
A global container is available on `req.scope` to allow you to use any of the registered services from the core, installed plugins or your local project:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type {
|
||||||
|
MedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
ProductService,
|
||||||
|
} from "@medusajs/medusa";
|
||||||
|
|
||||||
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
||||||
|
const productService: ProductService = req.scope.resolve("productService");
|
||||||
|
|
||||||
|
const products = await productService.list();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
products,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware
|
||||||
|
|
||||||
|
You can apply middleware to your routes by creating a file called `/api/middlewares.ts`. This file should export a configuration object with what middleware you want to apply to which routes. For example, if you want to apply a custom middleware function to the `/store/custom` route, you can do so by adding the following to your `/api/middlewares.ts` file:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type {
|
||||||
|
MiddlewaresConfig,
|
||||||
|
MedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
MedusaNextFunction,
|
||||||
|
} from "@medusajs/medusa";
|
||||||
|
|
||||||
|
async function logger(
|
||||||
|
req: MedusaRequest,
|
||||||
|
res: MedusaResponse,
|
||||||
|
next: MedusaNextFunction
|
||||||
|
) {
|
||||||
|
console.log("Request received");
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config: MiddlewaresConfig = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
matcher: "/store/custom",
|
||||||
|
middlewares: [logger],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The `matcher` property can be either a string or a regular expression. The `middlewares` property accepts an array of middleware functions.
|
||||||
|
|
||||||
|
You might only want to apply middleware to certain HTTP methods. You can do so by adding a `method` property to the route configuration object:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const config: MiddlewaresConfig = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
matcher: "/store/custom",
|
||||||
|
method: "GET",
|
||||||
|
middlewares: [logger],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The `method` property can be either a HTTP method or an array of HTTP methods. By default the middlewares will apply to all HTTP methods for the given `matcher`.
|
||||||
|
|
||||||
|
### Default middleware
|
||||||
|
|
||||||
|
Some middleware functions are applied per default:
|
||||||
|
|
||||||
|
#### Global middleware
|
||||||
|
|
||||||
|
JSON parsing is applied to all routes. This means that you can access the request body as `req.body` and it will be parsed as JSON, if the request has a `Content-Type` header of `application/json`.
|
||||||
|
|
||||||
|
If you want to use a different parser for a specific route, such as `urlencoded`, you can do so by adding the following export to your `route.ts` file:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { urlencoded } from "express";
|
||||||
|
|
||||||
|
export const config: MiddlewaresConfig = {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
matcher: "/store/custom",
|
||||||
|
middlewares: [urlencoded()],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Store middleware
|
||||||
|
|
||||||
|
For all `/store` routes, the appropriate CORS settings are applied. The STORE_CORS value can be configured in your `medusa-config.js` file.
|
||||||
|
|
||||||
|
#### Admin middleware
|
||||||
|
|
||||||
|
For all `/admin` routes, the appropriate CORS settings are applied. The ADMIN_CORS value can be configured in your `medusa-config.js` file.
|
||||||
|
|
||||||
|
All `/admin` routes also have admin authentication applied per default. If you want to disable this for a specific route, you can do so by adding the following export to your `route.ts` file:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const AUTHENTICATE = false;
|
||||||
|
```
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: MedusaRequest,
|
||||||
|
res: MedusaResponse
|
||||||
|
): Promise<void> {
|
||||||
|
res.sendStatus(200);
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||||
|
import { EntityManager } from "typeorm";
|
||||||
|
|
||||||
|
import OnboardingService from "../../../services/onboarding";
|
||||||
|
|
||||||
|
export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
||||||
|
const onboardingService: OnboardingService =
|
||||||
|
req.scope.resolve("onboardingService");
|
||||||
|
|
||||||
|
const status = await onboardingService.retrieve();
|
||||||
|
|
||||||
|
res.status(200).json({ status });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(req: MedusaRequest, res: MedusaResponse) {
|
||||||
|
const onboardingService: OnboardingService =
|
||||||
|
req.scope.resolve("onboardingService");
|
||||||
|
const manager: EntityManager = req.scope.resolve("manager");
|
||||||
|
|
||||||
|
const status = await manager.transaction(async (transactionManager) => {
|
||||||
|
return await onboardingService
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.update(req.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({ status });
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: MedusaRequest,
|
||||||
|
res: MedusaResponse
|
||||||
|
): Promise<void> {
|
||||||
|
res.sendStatus(200);
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
# Custom scheduled jobs
|
||||||
|
|
||||||
|
You may define custom scheduled jobs (cron jobs) by creating files in the `/jobs` directory.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
ProductService,
|
||||||
|
ScheduledJobArgs,
|
||||||
|
ScheduledJobConfig,
|
||||||
|
} from "@medusajs/medusa";
|
||||||
|
|
||||||
|
export default async function myCustomJob({ container }: ScheduledJobArgs) {
|
||||||
|
const productService: ProductService = container.resolve("productService");
|
||||||
|
|
||||||
|
const products = await productService.listAndCount();
|
||||||
|
|
||||||
|
// Do something with the products
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config: ScheduledJobConfig = {
|
||||||
|
name: "daily-product-report",
|
||||||
|
schedule: "0 0 * * *", // Every day at midnight
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The `handler` is a function which takes one parameter, an `object` of type `ScheduledJobArgs` with the following properties:
|
||||||
|
|
||||||
|
- `container` - a `MedusaContainer` instance which can be used to resolve services.
|
||||||
|
- `data` - an `object` containing data passed to the job when it was scheduled. This object is passed in the `config` object.
|
||||||
|
- `pluginOptions` - an `object` containing plugin options, if the job is defined in a plugin.
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
# Custom loader
|
||||||
|
|
||||||
|
The loader allows you have access to the Medusa service container. This allows you to access the database and the services registered on the container.
|
||||||
|
you can register custom registrations in the container or run custom code on startup.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/loaders/my-loader.ts
|
||||||
|
|
||||||
|
import { AwilixContainer } from 'awilix'
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param container The container in which the registrations are made
|
||||||
|
* @param config The options of the plugin or the entire config object
|
||||||
|
*/
|
||||||
|
export default (container: AwilixContainer, config: Record<string, unknown>): void | Promise<void> => {
|
||||||
|
/* Implement your own loader. */
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { generateEntityId } from "@medusajs/utils";
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class CreateOnboarding1685715079776 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "onboarding_state" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "current_step" character varying NULL, "is_complete" boolean)`
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "onboarding_state" ("id", "current_step", "is_complete") VALUES ('${generateEntityId(
|
||||||
|
"",
|
||||||
|
"onboarding"
|
||||||
|
)}' , NULL, false)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE "onboarding_state"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddOnboardingProduct1686062614694 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "onboarding_state" ADD COLUMN "product_id" character varying NULL`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "onboarding_state" DROP COLUMN "product_id"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class CorrectOnboardingFields1690996567455 implements MigrationInterface {
|
||||||
|
name = 'CorrectOnboardingFields1690996567455'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "onboarding_state" ADD CONSTRAINT "PK_891b72628471aada55d7b8c9410" PRIMARY KEY ("id")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "onboarding_state" ALTER COLUMN "is_complete" SET NOT NULL`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "onboarding_state" ALTER COLUMN "is_complete" DROP NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "onboarding_state" DROP CONSTRAINT "PK_891b72628471aada55d7b8c9410"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
# Custom migrations
|
||||||
|
|
||||||
|
You may define custom models (entities) that will be registered on the global container by creating files in the `src/models` directory that export an instance of `BaseEntity`.
|
||||||
|
In that case you also need to provide a migration in order to create the table in the database.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
### 1. Create the migration
|
||||||
|
|
||||||
|
See [How to Create Migrations](https://docs.medusajs.com/advanced/backend/migrations/) in the documentation.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/migration/my-migration.ts
|
||||||
|
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||||
|
|
||||||
|
export class MyMigration1617703530229 implements MigrationInterface {
|
||||||
|
name = "myMigration1617703530229"
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// write you migration here
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// write you migration here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { BaseEntity } from "@medusajs/medusa";
|
||||||
|
import { Column, Entity } from "typeorm";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class OnboardingState extends BaseEntity {
|
||||||
|
@Column({ nullable: true })
|
||||||
|
current_step: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
is_complete: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
product_id: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { dataSource } from "@medusajs/medusa/dist/loaders/database";
|
||||||
|
import { OnboardingState } from "../models/onboarding";
|
||||||
|
|
||||||
|
const OnboardingRepository = dataSource.getRepository(OnboardingState);
|
||||||
|
|
||||||
|
export default OnboardingRepository;
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
# Custom services
|
||||||
|
|
||||||
|
You may define custom services that will be registered on the global container by creating files in the `/services` directory that export an instance of `BaseService`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/services/my-custom.ts
|
||||||
|
|
||||||
|
import { Lifetime } from "awilix"
|
||||||
|
import { TransactionBaseService } from "@medusajs/medusa";
|
||||||
|
import { IEventBusService } from "@medusajs/types";
|
||||||
|
|
||||||
|
export default class MyCustomService extends TransactionBaseService {
|
||||||
|
static LIFE_TIME = Lifetime.SCOPED
|
||||||
|
protected readonly eventBusService_: IEventBusService
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{ eventBusService }: { eventBusService: IEventBusService },
|
||||||
|
options: Record<string, unknown>
|
||||||
|
) {
|
||||||
|
// @ts-ignore
|
||||||
|
super(...arguments)
|
||||||
|
|
||||||
|
this.eventBusService_ = eventBusService
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The first argument to the `constructor` is the global giving you access to easy dependency injection. The container holds all registered services from the core, installed plugins and from other files in the `/services` directory. The registration name is a camelCased version of the file name with the type appended i.e.: `my-custom.js` is registered as `myCustomService`, `custom-thing.js` is registered as `customThingService`.
|
||||||
|
|
||||||
|
You may use the services you define here in custom endpoints by resolving the services defined.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Router } from "express"
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router.get("/hello-product", async (req, res) => {
|
||||||
|
const myService = req.scope.resolve("myCustomService")
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: await myService.getProductMessage()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
describe('MyService', () => {
|
||||||
|
it('should do this', async () => {
|
||||||
|
expect(true).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import { TransactionBaseService } from "@medusajs/medusa";
|
||||||
|
import OnboardingRepository from "../repositories/onboarding";
|
||||||
|
import { OnboardingState } from "../models/onboarding";
|
||||||
|
import { EntityManager, IsNull, Not } from "typeorm";
|
||||||
|
import { UpdateOnboardingStateInput } from "../types/onboarding";
|
||||||
|
|
||||||
|
type InjectedDependencies = {
|
||||||
|
manager: EntityManager;
|
||||||
|
onboardingRepository: typeof OnboardingRepository;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OnboardingService extends TransactionBaseService {
|
||||||
|
protected onboardingRepository_: typeof OnboardingRepository;
|
||||||
|
|
||||||
|
constructor({ onboardingRepository }: InjectedDependencies) {
|
||||||
|
super(arguments[0]);
|
||||||
|
|
||||||
|
this.onboardingRepository_ = onboardingRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieve(): Promise<OnboardingState | undefined> {
|
||||||
|
const onboardingRepo = this.activeManager_.withRepository(
|
||||||
|
this.onboardingRepository_
|
||||||
|
);
|
||||||
|
|
||||||
|
const status = await onboardingRepo.findOne({
|
||||||
|
where: { id: Not(IsNull()) },
|
||||||
|
});
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(data: UpdateOnboardingStateInput): Promise<OnboardingState> {
|
||||||
|
return await this.atomicPhase_(
|
||||||
|
async (transactionManager: EntityManager) => {
|
||||||
|
const onboardingRepository = transactionManager.withRepository(
|
||||||
|
this.onboardingRepository_
|
||||||
|
);
|
||||||
|
|
||||||
|
const status = await this.retrieve();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
status[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await onboardingRepository.save(status);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OnboardingService;
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
# Custom subscribers
|
||||||
|
|
||||||
|
You may define custom eventhandlers, `subscribers` by creating files in the `/subscribers` directory.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import MyCustomService from "../services/my-custom";
|
||||||
|
import {
|
||||||
|
OrderService,
|
||||||
|
SubscriberArgs,
|
||||||
|
SubscriberConfig,
|
||||||
|
} from "@medusajs/medusa";
|
||||||
|
|
||||||
|
type OrderPlacedEvent = {
|
||||||
|
id: string;
|
||||||
|
no_notification: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function orderPlacedHandler({
|
||||||
|
data,
|
||||||
|
eventName,
|
||||||
|
container,
|
||||||
|
}: SubscriberArgs<OrderPlacedEvent>) {
|
||||||
|
const orderService: OrderService = container.resolve(OrderService);
|
||||||
|
|
||||||
|
const order = await orderService.retrieve(data.id, {
|
||||||
|
relations: ["items", "items.variant", "items.variant.product"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do something with the order
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config: SubscriberConfig = {
|
||||||
|
event: OrderService.Events.PLACED,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The `handler` is a function which takes one parameter, an `object` of type `SubscriberArgs<T>` with the following properties:
|
||||||
|
|
||||||
|
- `data` - an `object` of type `T` containing information about the event.
|
||||||
|
- `eventName` - a `string` containing the name of the event.
|
||||||
|
- `container` - a `MedusaContainer` instance which can be used to resolve services.
|
||||||
|
- `pluginOptions` - an `object` containing plugin options, if the subscriber is defined in a plugin.
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { OnboardingState } from "../models/onboarding";
|
||||||
|
|
||||||
|
export type UpdateOnboardingStateInput = {
|
||||||
|
current_step?: string;
|
||||||
|
is_complete?: boolean;
|
||||||
|
product_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AdminOnboardingUpdateStateReq {}
|
||||||
|
|
||||||
|
export type OnboardingStateRes = {
|
||||||
|
status: OnboardingState;
|
||||||
|
};
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext"
|
||||||
|
},
|
||||||
|
"include": ["src/admin"],
|
||||||
|
"exclude": ["**/*.spec.js"]
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2019",
|
||||||
|
"allowJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"skipDefaultLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"checkJs": false
|
||||||
|
},
|
||||||
|
"include": ["src/"],
|
||||||
|
"exclude": [
|
||||||
|
"**/__tests__",
|
||||||
|
"**/__fixtures__",
|
||||||
|
"node_modules",
|
||||||
|
"build",
|
||||||
|
".cache"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
"inlineSourceMap": true
|
||||||
|
},
|
||||||
|
"exclude": ["src/admin", "**/*.spec.js"]
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["dist", "node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue