feat: Update to API references look and feel (#343)

Co-authored-by: Vadim Smirnov <smirnou.vadzim@gmail.com>
Co-authored-by: zakariasaad <zakaria.elas@gmail.com>
Co-authored-by: Vilfred Sikker <vilfredsikker@gmail.com>
Co-authored-by: olivermrbl <oliver@mrbltech.com>
This commit is contained in:
Kasper Fabricius Kristensen
2021-08-20 10:26:29 +02:00
committed by GitHub
parent 40400b483c
commit 143f06aa39
85 changed files with 19022 additions and 7116 deletions

View File

@@ -5308,7 +5308,7 @@
}
}
},
"/returns/{id}receive": {
"/returns/{id}/receive": {
"post": {
"operationId": "PostReturnsReturnReceive",
"summary": "Receive a Return",
@@ -5881,7 +5881,7 @@
},
"delete": {
"operationId": "DeleteStoreCurrenciesCode",
"summary": "Remvoe a Currency Code",
"summary": "Remove a Currency Code",
"description": "Removes a Currency Code from the available currencies.",
"parameters": [
{
@@ -6085,7 +6085,7 @@
"/variants": {
"get": {
"operationId": "GetVariants",
"summary": "List Product Variants.",
"summary": "List Product Variants",
"description": "Retrieves a list of Product Variants",
"tags": [
"Product Variant"
@@ -7546,7 +7546,7 @@
"swaps": {
"type": "array",
"items": {
"$ref": "#/components/schemas/refund"
"$ref": "#/components/schemas/swap"
}
},
"gift_card_transactions": {

View File

@@ -955,4 +955,4 @@
"idempotency_key": "10804103-2f4f-41ef-b44e-7049459f157d"
}
}
}
}

41
docs/content/nav.yml Normal file
View File

@@ -0,0 +1,41 @@
- title: Tutorials
id: tutorials
items:
- id: overview
title: Overview
- id: getting-started
title: Getting started
- id: adding-plugins
title: Adding plugins
- id: creating-a-custom-endpoint
title: Creating a custom endpoint
- id: linking-medusa-cloud
title: Linking Medusa Cloud
- title: Guides
id: guides
items:
- id: overview
title: Overview
- id: local-development
title: Local development
items:
- id: overview
title: Overview
- id: create-project-from-starter
title: Create project from starter
items:
- id: medusa-starter
title: Medusa starter
- id: gatsby-starter-medusa
title: Gatsby starter
- id: environment-variables
title: Environment variables
- title: Reference
id: reference
items:
- id: admin
title: admin
- id: store
title: store
- title: Contributing
id: contributing

View File

@@ -0,0 +1,89 @@
---
title: Getting started
---
# Quick Start w. Docker
This quick start is intended for developers, that have already configured their local development environment and familiarised them selves with all the technologies and frameworks used throughout the Medusa eco-system.
If this is not the case, please head over to our Getting Started tutorial for a thorough walkthrough.
## Introduction
With all the tools and technologies in place, let's get started by setting up a default project. Our starter is shipped with a very basic configuration, that includes the following plugins:
- Stripe as payment provider
- SendGrid as email notification provider
- Manual fulfilment as fulfilment provider
Additionally, we will spin up a PostgreSQL database and a Redis server, both required for Medusa to run. In this quick start, we will use docker to seamlessly set up these resources.
## Get started
1. Clone our starter project from Github
```bash
git clone https://github.com/medusajs/medusa-starter-default.git my-medusa-starter
```
2. Once cloned, we will jump into our project directory and get started with our configuration.
```bash
cd my-medusa-starter
```
3. Get your environment variables ready using our template
```bash
mv .env.template .env
```
4. Setup accounts for included plugins. This step is optional but required for placing orders.
Create a Stripe account and add your API key and webhook secret to `.env`
Create a SendGrid account and add your API key to `.env`
```bash
...
STRIPE_API_KEY="some_stripe_key"
STRIPE_WEBHOOK_SECRET="some_webhook_secret"
SENDGRID_API_KEY="some_sendgrid_key"
..
```
5. Start your server
```bash
docker-compose up --build
```
We will use docker-compose and Docker to start up our development environment. Running the above command will do the following:
1. Build images for our Medusa project, a PostgreSQL database and a Redis server
2. Run migrations for our newly created database
3. Seed our database with some entities, that will allow us to easily get started.
These include:
- A user with email `admin@medusa-test.com` and password `supersecret`
- A region called Default Region with a small subset of countries
- A shipping option called Standard Shipping, that costs 10 EUR
- A product called Cool Test Product
- A variant of that product that costs 195 EUR
Once done, our server will be accessible at `http://localhost:9000`.
## Try it out
Let's try out our Medusa server by fetching some products.
```bash
curl -X GET localhost:9000/store/products | python -m json.tool
```
## What's next?
Add custom endpoint to your Medusa project (Insert link)
Install and configure additional plugins (Insert link)
Build a storefront using our Gatsby starter (Insert link)

8
docs/pages/index-page.md Normal file
View File

@@ -0,0 +1,8 @@
---
templateKey: index-page
pageTitle: Medusa docs
introTtitle: Documentation
introSubtitle: Explore and learn how to use Medusa.
introDescriptionTitle: Quickstart
introDescriptionSubtitle: Get up and running within 5 minutes, with helpful starters that lay the foundation for growth.
---

View File

@@ -33,7 +33,6 @@ describe("/store/customers", () => {
describe("POST /store/customers", () => {
beforeEach(async () => {
const manager = dbConnection.manager;
await manager.insert(Customer, {
id: "test_customer",
first_name: "John",

View File

@@ -36,6 +36,7 @@
"test:fixtures": "jest --config=docs-util/jest.config.js --runInBand"
},
"dependencies": {
"@docusaurus/theme-search-algolia": "^2.0.0-beta.3",
"global": "^4.4.0",
"import-from": "^3.0.0",
"oas-normalize": "^2.3.1",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
import React from "react"
import { ThemeProvider as Provider } from "./src/theme"
export const wrapPageElement = ({ element }) => {
return <Provider>{element}</Provider>
}

View File

@@ -1,31 +0,0 @@
/**
* Configure your Gatsby site with this file.
*
* See: https://www.gatsbyjs.com/docs/gatsby-config/
*/
module.exports = {
plugins: [
`gatsby-plugin-react-helmet`,
{
resolve: `gatsby-plugin-anchor-links`,
options: {
offset: -300,
},
},
`gatsby-transformer-json`,
`gatsby-plugin-emotion`,
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/../docs/api/store`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/../docs/api`,
},
},
],
}

View File

@@ -1,6 +0,0 @@
import React from "react"
import { ThemeProvider as Provider } from "./src/theme"
export const wrapPageElement = ({ element }) => {
return <Provider>{element}</Provider>
}

13
www/reference/.babelrc Normal file
View File

@@ -0,0 +1,13 @@
{
"presets": [
[
"babel-preset-gatsby",
{
"reactRuntime": "automatic",
"targets": {
"browsers": [">0.25%", "not dead"]
}
}
]
]
}

View File

@@ -0,0 +1,16 @@
import React from "react"
import { NavigationProvider } from "./src/context/navigation-context"
export const onServiceWorkerUpdateReady = () => {
const answer = window.confirm(
"We have updated the docs site. Reload to display the latest version?"
)
if (answer) {
window.location.reload()
}
}
export const wrapPageElement = ({ element }) => {
return <NavigationProvider>{element}</NavigationProvider>
}

View File

@@ -0,0 +1,86 @@
module.exports = {
siteMetadata: {
title: "Medusa",
description: "Open-source headless commerce engine",
author: "Medusa core team",
},
plugins: [
`gatsby-plugin-react-helmet`,
{
resolve: `gatsby-plugin-anchor-links`,
options: {
offset: -100,
duration: 1000,
},
},
`gatsby-transformer-json`,
`gatsby-plugin-emotion`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: "store-api",
path: `${__dirname}/../../docs/api/store-spec3.json`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: "admin-api",
path: `${__dirname}/../../docs/api/admin-spec3.json`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `docs`,
path: `${__dirname}/../../docs/content/`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/../../docs/pages`,
name: `pages`,
},
},
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
{
resolve: `gatsby-remark-autolink-headers`,
options: {
elements: [`h2`, `h3`, `h4`],
},
},
],
},
},
`gatsby-plugin-theme-ui`,
{
resolve: `gatsby-plugin-algolia-docsearch`,
options: {
apiKey: process.env.ALGOLIA_DOCSEARCH_API_KEY, // required
indexName: process.env.ALGOLIA_DOCSEARCH_INDEX_NAME, // required
inputSelector: "#algolia-doc-search", // required
debug: false, // (bool) Optional. Default `false`
},
},
// `gatsby-plugin-preact`,
// {
// resolve: `gatsby-source-openapi-aggregate`,
// options: {
// specs: [
// {
// name: "admin-spec",
// resolve: () =>
// fromJson(
// path.resolve(__dirname, "../../docs/api/admin-spec3.json")
// ),
// },
// ],
// },
// },
],
}

View File

@@ -0,0 +1,551 @@
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)
const fixtures = require("../../docs/api/fixtures.json")
const convertToKebabCase = string => {
return string
.replace(/\s+/g, "-")
.replace("'", "")
.replace(".", "")
.replace('"', "")
.toLowerCase()
}
const createCustomNode = ({ name, node, createNode }) => {
const tags = node.tags
const nodePaths = Object.entries(node.paths).map(([path, values]) => {
return {
name: path,
methods: Object.entries(values).map(([method, values]) => {
const requestBodyValues =
values?.requestBody?.content?.["application/json"]?.schema
return {
method: method,
...values,
requestBody: {
required: requestBodyValues?.required,
type: requestBodyValues?.type,
properties: Object.entries(requestBodyValues?.properties || {}).map(
([property, values]) => {
let ref = null
let component_id = null
let nestedModelNode = null
const { items, anyOf, oneOf, ...rest } = values
if (items || anyOf || oneOf) {
if (items?.anyOf) {
component_id = items.anyOf[0]["$ref"].substring(
items.anyOf[0]["$ref"].lastIndexOf("/") + 1
)
ref = node.components.schemas[component_id]
} else if (items) {
const refPath = items["$ref"]
if (refPath) {
component_id = refPath.substring(
items["$ref"].lastIndexOf("/") + 1
)
ref = node.components.schemas[component_id]
}
} else if (anyOf) {
component_id = anyOf[0]["$ref"].substring(
anyOf[0]["$ref"].lastIndexOf("/") + 1
)
ref = node.components.schemas[component_id]
} else if (oneOf) {
component_id = oneOf[0]["$ref"].substring(
oneOf[0]["$ref"].lastIndexOf("/") + 1
)
ref = node.components.schemas[component_id]
}
}
if (ref) {
const { properties, ...rest } = ref
let nestedModelProperties = []
if (properties) {
Object.entries(properties).map(([key, values]) => {
nestedModelProperties.push({
property: key,
...values,
})
})
nestedModelNode = {
properties: nestedModelProperties,
...rest,
}
}
}
return {
property: property,
nestedModel: nestedModelNode,
...rest,
}
}
),
},
responses: Object.entries(values.responses).map(
([response, values]) => {
const properties =
values.content?.["application/json"]?.schema?.properties
return {
status: response,
content: properties
? Object.entries(properties).map(([property, values]) => {
const toSet = {}
let concept
if (values.items && values.items["$ref"]) {
const [, ...path] = values.items["$ref"].split("/")
concept = path[path.length - 1]
if (values.type === "array") {
if (concept in fixtures.resources) {
//make the array key plural
let prop
if (property.slice(-1) !== "s") {
prop = property + "s"
} else {
prop = property
}
toSet[prop] = [fixtures.resources[concept]]
}
} else if (concept in fixtures.resources) {
toSet[property] = fixtures.resources[concept]
}
} else {
if (fixtures.resources[property]) {
toSet[property] = fixtures.resources[property]
} else if (values["$ref"]) {
const [, ...path] = values["$ref"].split("/")
toSet[property] =
fixtures.resources[path[path.length - 1]]
}
}
const json =
Object.keys(toSet).length > 0
? JSON.stringify(toSet, undefined, 2)
: null
return {
property: property,
json: json,
...values,
}
})
: null,
description: values.description,
}
}
),
}
}),
}
})
const nodeSections = nodePaths.reduce((acc, current) => {
// Not bulleproof and kind of naive
const section = current.methods.find(method => method.tags).tags[0]
if (!section) {
return acc
}
const existingSection = acc.find(s => s.section.section_name === section)
//Because section_name does not always equal the resourceId
const tag = tags.find(
tag => tag.name.toLowerCase() === section.toLowerCase()
)
let resourceId
if (tag) resourceId = tag["x-resourceId"]
const schema =
node.components.schemas[section.toLowerCase()] ||
node.components.schemas[resourceId] ||
null
//Create a schemaNode so we can access descriptions and attributes of objects
let schemaNode
if (schema) {
let props = []
Object.entries(schema.properties).map(([key, values]) => {
let ref = null
let component_id = null
let nestedModelNode = null
let { items, anyOf, oneOf, ...rest } = values
if (items || anyOf || oneOf) {
if (items?.anyOf) {
component_id = items.anyOf[0]["$ref"].substring(
items.anyOf[0]["$ref"].lastIndexOf("/") + 1
)
ref = node.components.schemas[component_id]
} else if (items) {
const refPath = items["$ref"]
if (refPath) {
component_id = refPath.substring(
items["$ref"].lastIndexOf("/") + 1
)
ref = node.components.schemas[component_id]
}
} else if (anyOf) {
component_id = anyOf[0]["$ref"].substring(
anyOf[0]["$ref"].lastIndexOf("/") + 1
)
ref = node.components.schemas[component_id]
}
if (ref) {
const { properties, ...rest } = ref
let nestedModelProperties = []
if (properties) {
Object.entries(properties).map(([key, values]) => {
nestedModelProperties.push({
property: key,
...values,
})
})
nestedModelNode = {
properties: nestedModelProperties,
...rest,
}
}
}
}
props.push({
property: key,
nestedModel: nestedModelNode,
...rest,
})
})
let object = fixtures.resources[resourceId] || null
if (object) object = JSON.stringify(object, undefined, 2)
schemaNode = {
object: object,
description: schema.description,
properties: props,
}
}
/** We want the query to return the following:
*
* sections [
* section: {
* section_name: Orders
* paths: [<array-of-paths>]
* }
* ]
*
* The reason that we wrap it in a section is so the query returns Section type, the alternative is
* sections [
* Order: {...},
* Customer: {...}
* ...
* ]
*
* Which isn't structured as a reuseable type
*/
if (!existingSection) {
acc.push({
section: {
section_name: section,
paths: [{ ...current }],
schema: schemaNode,
},
})
} else {
existingSection.section.paths.push({ ...current })
}
return acc
}, [])
const result = {
name: name,
paths: nodePaths,
sections: nodeSections,
rawNode: node,
// required fields
id: name,
parent: null, // or null if it's a source node without a parent
children: [],
internal: {
type: name,
contentDigest: `store-api-${node.internal.contentDigest}`,
mediaType: node.internal.mediaType, // optional
content: node.internal.content, // optional
description: `A cleaned version of file-system-api json files`, // optional
},
}
createNode(result)
}
exports.onCreateNode = ({ node, getNode, actions }) => {
const { createNodeField, createNode } = actions
if (node.internal.type === `MarkdownRemark`) {
const slug = createFilePath({ node, getNode })
createNodeField({
node,
name: `slug`,
value: slug,
})
}
if (node.internal.type === "ApiJson" && node.components) {
if (node.info.title === "Medusa Storefront API") {
createCustomNode({ name: "Store", node: node, createNode: createNode })
}
if (node.info.title === "Medusa Admin API") {
createCustomNode({ name: "Admin", node: node, createNode: createNode })
}
}
}
const createAllPages = (sections, api, siteData, createPage, template) => {
sections.forEach(edge => {
const baseURL = convertToKebabCase(edge.section.section_name)
createPage({
path: `api/${api}/${baseURL}`,
component: template,
context: {
data: siteData.data[api],
api: api,
title: edge.section.section_name,
description: edge.section.schema?.description || "",
to: { section: baseURL, method: null },
},
})
edge.section.paths.forEach(p => {
p.methods.forEach(method => {
const methodURL = convertToKebabCase(method.summary)
createPage({
path: `api/${api}/${baseURL}/${methodURL}`,
component: template,
context: {
data: siteData.data[api],
api: api,
title: method.summary,
description: method.description || "",
to: { section: baseURL, method: methodURL },
},
})
})
})
})
}
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
const template = path.resolve(`src/templates/reference-page.js`)
// Query for markdown nodes to use in creating pages.
// You can query for whatever data you want to create pages for e.g.
// products, portfolio items, landing pages, etc.
// Variables can be added as the second function parameter
return graphql(
`
query Pages {
admin {
sections {
section {
section_name
paths {
name
methods {
tags
summary
description
method
operationId
responses {
status
description
content {
_ref
type
property
description
json
items {
type
_ref
}
}
}
requestBody {
type
required
properties {
description
enum
format
property
type
nestedModel {
title
properties {
property
type
description
}
}
}
}
parameters {
description
in
name
required
schema {
type
}
}
}
}
schema {
object
description
properties {
property
type
description
format
nestedModel {
title
properties {
property
type
description
}
}
}
}
}
}
}
store {
sections {
section {
section_name
paths {
name
methods {
tags
summary
description
method
operationId
responses {
status
description
content {
_ref
property
description
json
items {
_ref
}
}
}
requestBody {
type
required
properties {
description
property
type
nestedModel {
title
properties {
property
type
}
}
}
}
parameters {
description
in
name
required
schema {
type
}
}
}
}
schema {
object
description
properties {
property
type
description
format
nestedModel {
title
properties {
property
type
}
}
}
}
}
}
}
}
`,
{ limit: 1000 }
).then(result => {
if (result.errors) {
throw result.errors
}
//create entrypoint
createPage({
path: `api`,
component: template,
context: {
data: result.data.store,
api: "store",
title: "Store",
description: "Storefront API",
},
})
const apis = [
{ title: "Store", description: "Storefront API", slug: "store" },
{ title: "Admin", description: "Admin API", slug: "admin" },
]
apis.forEach(api => {
//create main page for API
createPage({
path: `api/${api.slug}`,
component: template,
context: {
data: result.data[api.slug],
api: api.slug,
title: api.title,
description: api.description,
},
})
//create pages for all sections and methods
createAllPages(
result.data[api.slug].sections,
api.slug,
result,
createPage,
template
)
})
})
}

View File

@@ -0,0 +1,6 @@
import React from "react"
import { NavigationProvider } from "./src/context/navigation-context"
export const wrapPageElement = ({ element }) => {
return <NavigationProvider>{element}</NavigationProvider>
}

View File

@@ -18,13 +18,23 @@
"@emotion/styled": "^11.0.0",
"@rebass/forms": "^4.0.6",
"emotion-theming": "^10.0.27",
"gatsby": "^2.24.66",
"gatsby": "^3.9.1",
"gatsby-plugin-algolia-docsearch": "^1.0.5",
"gatsby-plugin-anchor-links": "^1.1.1",
"gatsby-plugin-emotion": "^5.0.0",
"gatsby-plugin-preact": "^5.9.0",
"gatsby-plugin-react-helmet": "^3.3.12",
"gatsby-source-filesystem": "^2.3.31",
"gatsby-plugin-theme-ui": "^0.10.1",
"gatsby-remark-autolink-headers": "^4.6.0",
"gatsby-source-filesystem": "^3.9.0",
"gatsby-source-openapi-aggregate": "^0.3.0",
"gatsby-transformer-json": "^3.0.0",
"gatsby-transformer-remark": "^4.6.0",
"gatsby-transformer-yaml": "^2.4.13",
"lodash.throttle": "^4.1.1",
"preact": "^10.5.14",
"preact-render-to-string": "^5.1.19",
"prismjs": "^1.24.1",
"react": "^16.12.0",
"react-collapsible": "^2.8.1",
"react-dom": "^16.12.0",
@@ -32,7 +42,8 @@
"react-highlight.js": "^1.0.7",
"react-intersection-observer": "^8.29.0",
"react-markdown": "^5.0.3",
"rebass": "^4.0.7"
"react-virtualized": "^9.22.3",
"theme-ui": "^0.10.0"
},
"devDependencies": {
"prettier": "2.1.2"

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="33px" height="33px" viewBox="0 0 33 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>GitHub</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="GitHub-Mark" transform="translate(-136.000000, -331.000000)" fill="#000">
<path d="M152.608,331.455 C143.614,331.455 136.32,338.748 136.32,347.745 C136.32,354.942 140.987,361.047 147.46,363.201 C148.275,363.351 148.572,362.848 148.572,362.416 C148.572,362.029 148.558,361.005 148.55,359.646 C144.019,360.63 143.063,357.462 143.063,357.462 C142.322,355.58 141.254,355.079 141.254,355.079 C139.775,354.069 141.366,354.089 141.366,354.089 C143.001,354.204 143.861,355.768 143.861,355.768 C145.314,358.257 147.674,357.538 148.602,357.121 C148.75,356.069 149.171,355.351 149.636,354.944 C146.019,354.533 142.216,353.135 142.216,346.893 C142.216,345.115 142.851,343.66 143.893,342.522 C143.725,342.11 143.166,340.453 144.053,338.211 C144.053,338.211 145.42,337.773 148.532,339.881 C149.831,339.519 151.225,339.339 152.61,339.332 C153.994,339.339 155.387,339.519 156.688,339.881 C159.798,337.773 161.163,338.211 161.163,338.211 C162.052,340.453 161.493,342.11 161.326,342.522 C162.37,343.66 163,345.115 163,346.893 C163,353.151 159.191,354.528 155.563,354.931 C156.147,355.434 156.668,356.428 156.668,357.947 C156.668,360.125 156.648,361.882 156.648,362.416 C156.648,362.852 156.942,363.359 157.768,363.2 C164.236,361.041 168.899,354.94 168.899,347.745 C168.899,338.748 161.605,331.455 152.608,331.455" id="Fill-51"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,9 @@
<svg width="602" height="122" viewBox="0 0 602 122" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="602" height="122" fill="white"/>
<path d="M95.0625 0H129.431V119.437H108.713V18.0375L75.5625 119.437H53.8688L20.7188 19.2562V119.437H0V0H34.6125L64.8375 95.7937L95.0625 0Z" fill="#89959C"/>
<path d="M233.756 92.3826C228.881 110.42 213.525 121.633 192.319 121.633C165.75 121.633 148.688 103.595 148.688 76.5388C148.688 49.4826 165.75 31.4451 192.806 31.4451C222.788 31.4451 238.387 53.8701 230.1 81.9014H169.406C171.112 96.0389 178.913 104.326 192.562 104.326C202.069 104.326 209.625 99.9389 212.55 92.1389H233.756V92.3826ZM169.894 68.2514H211.819C213.037 56.0639 205.969 48.0201 192.562 48.0201C179.888 48.0201 171.844 55.0889 169.894 68.2514Z" fill="#89959C"/>
<path d="M312.244 0.000396729H331.744V119.438H312.244V106.763C305.663 116.269 295.669 121.875 283.238 121.875C260.813 121.875 244.725 103.838 244.725 76.7816C244.725 49.7254 260.325 31.6879 283.238 31.6879C295.669 31.6879 305.663 37.0504 312.244 46.8004V0.000396729V0.000396729ZM312.975 76.7816C312.975 59.9629 303.469 48.7504 289.088 48.7504C274.463 48.7504 264.956 59.9629 264.956 76.7816C264.956 93.6004 274.463 104.813 289.088 104.813C303.469 104.569 312.975 93.6004 312.975 76.7816Z" fill="#89959C"/>
<path d="M403.894 34.1264H423.394V119.439H403.894V104.083C397.8 115.539 388.294 121.389 376.35 121.389C352.706 121.389 348.806 97.7451 348.806 82.6326V33.8826H368.306V82.3889C368.306 93.6014 370.987 104.326 383.175 104.326C395.85 104.326 403.894 93.1139 403.894 77.2701V34.1264Z" fill="#89959C"/>
<path d="M437.775 88.7263H456.544C455.325 99.2076 463.125 106.033 476.531 106.033C487.013 106.033 493.838 101.889 493.838 95.7951C493.838 78.7326 438.994 90.9201 438.994 58.9889C438.994 41.9264 454.838 31.4451 475.556 31.4451C498.469 31.4451 515.044 44.1201 511.387 63.6201H492.375C494.813 53.8701 487.013 47.2888 475.313 47.2888C465.319 47.2888 458.981 51.6764 458.981 57.7701C458.981 75.0764 514.069 61.9139 514.069 94.3326C514.069 111.639 498.469 121.633 476.287 121.633C452.156 121.633 436.313 109.445 437.775 88.7263Z" fill="#89959C"/>
<path d="M601.331 70.9316V119.438H583.293V104.569C576.956 115.538 566.475 121.875 552.581 121.875C534.544 121.875 523.331 111.394 523.331 96.5254C523.331 79.2191 538.444 68.9816 562.331 68.9816C568.912 68.9816 575.737 69.7129 581.831 71.1754C581.831 58.2566 577.2 48.7504 563.55 48.7504C553.312 48.7504 546.975 53.8691 548.925 63.3754H529.181C524.55 44.3629 541.369 31.6879 563.55 31.6879C588.656 31.6879 601.331 47.0441 601.331 70.9316ZM582.562 85.8004C576.956 83.8504 570.131 82.8754 564.037 82.8754C550.144 82.8754 543.319 87.9941 543.319 95.7941C543.319 102.619 548.437 106.275 556.725 106.275C569.156 106.275 579.394 98.2316 582.562 85.8004Z" fill="#89959C"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,16 @@
<svg width="905" height="234" viewBox="0 0 905 234" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M189.638 38.0267L136.744 7.55792C119.438 -2.43583 98.2313 -2.43583 80.925 7.55792L27.7875 38.0267C10.725 48.0204 0 66.5454 0 86.2892V147.47C0 167.458 10.725 185.739 27.7875 195.733L80.6812 226.445C97.9875 236.439 119.194 236.439 136.5 226.445L189.394 195.733C206.7 185.739 217.181 167.458 217.181 147.47V86.2892C217.669 66.5454 206.944 48.0204 189.638 38.0267ZM108.712 171.358C78.7312 171.358 54.3562 146.983 54.3562 117.002C54.3562 87.0204 78.7312 62.6454 108.712 62.6454C138.694 62.6454 163.312 87.0204 163.312 117.002C163.312 146.983 138.938 171.358 108.712 171.358Z" fill="#0A3149"/>
<path d="M398.808 55.3289H433.176V174.766H412.458V73.3664L379.308 174.766H357.614L324.464 74.5852V174.766H303.745V55.3289H338.358L368.583 151.123L398.808 55.3289Z" fill="#0A3149"/>
<path d="M537.501 147.712C532.626 165.749 517.27 176.962 496.064 176.962C469.495 176.962 452.433 158.924 452.433 131.868C452.433 104.812 469.495 86.774 496.552 86.774C526.533 86.774 542.133 109.199 533.845 137.23H473.151C474.858 151.368 482.658 159.655 496.308 159.655C505.814 159.655 513.37 155.268 516.295 147.468H537.501V147.712ZM473.639 123.58H515.564C516.783 111.393 509.714 103.349 496.308 103.349C483.633 103.349 475.589 110.418 473.639 123.58Z" fill="#0A3149"/>
<path d="M615.989 55.3293H635.489V174.767H615.989V162.092C609.408 171.598 599.414 177.204 586.983 177.204C564.558 177.204 548.47 159.167 548.47 132.111C548.47 105.054 564.07 87.0168 586.983 87.0168C599.414 87.0168 609.408 92.3793 615.989 102.129V55.3293ZM616.72 132.111C616.72 115.292 607.214 104.079 592.833 104.079C578.208 104.079 568.701 115.292 568.701 132.111C568.701 148.929 578.208 160.142 592.833 160.142C607.214 159.898 616.72 148.929 616.72 132.111Z" fill="#0A3149"/>
<path d="M707.639 89.4553H727.139V174.768H707.639V159.412C701.545 170.868 692.039 176.718 680.095 176.718C656.451 176.718 652.552 153.074 652.552 137.962V89.2115H672.052V137.718C672.052 148.93 674.733 159.655 686.92 159.655C699.595 159.655 707.639 148.443 707.639 132.599V89.4553Z" fill="#0A3149"/>
<path d="M741.52 144.055H760.289C759.07 154.537 766.87 161.362 780.277 161.362C790.758 161.362 797.583 157.218 797.583 151.124C797.583 134.062 742.739 146.249 742.739 114.318C742.739 97.2553 758.583 86.774 779.302 86.774C802.214 86.774 818.789 99.449 815.133 118.949H796.12C798.558 109.199 790.758 102.618 779.058 102.618C769.064 102.618 762.726 107.005 762.726 113.099C762.726 130.405 817.814 117.243 817.814 149.662C817.814 166.968 802.214 176.962 780.033 176.962C755.901 176.962 740.058 164.774 741.52 144.055Z" fill="#0A3149"/>
<path d="M905.077 126.261V174.767H887.039V159.898C880.702 170.867 870.22 177.204 856.327 177.204C838.289 177.204 827.077 166.723 827.077 151.854C827.077 134.548 842.189 124.311 866.077 124.311C872.658 124.311 879.483 125.042 885.577 126.504C885.577 113.586 880.945 104.079 867.295 104.079C857.058 104.079 850.72 109.198 852.67 118.704H832.927C828.295 99.6918 845.114 87.0168 867.295 87.0168C892.402 87.0168 905.077 102.373 905.077 126.261ZM886.308 141.129C880.702 139.179 873.877 138.204 867.783 138.204C853.889 138.204 847.064 143.323 847.064 151.123C847.064 157.948 852.183 161.604 860.47 161.604C872.902 161.604 883.139 153.561 886.308 141.129Z" fill="#0A3149"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="904.8" height="234" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,109 @@
import React, { useEffect, useState } from "react"
import { Flex, Image, Box } from "theme-ui"
import styled from "@emotion/styled"
import Logo from "../../assets/logo.svg"
import LogoMuted from "../../assets/logo-muted.svg"
import SideBarItem from "./sidebar-item"
import SideBarSelector from "./sidebar-selector"
const SideBarContainer = styled(Flex)`
--side-bar-width: 220px;
@media screen and (min-width: 1680px) {
--side-bar-width: 280px;
}
@media screen and (max-width: 848px) {
display: none;
}
`
const SideBarFade = styled(Box)`
position: absolute;
top: 0;
left: 0;
width: calc(var(--side-bar-width) - 1px);
height: 50px;
pointer-events: none;
box-shadow: inset 0 50px 25px calc(-1 * 25px) white;
`
const Sidebar = ({ data, api }) => {
const [scrollPos, setScrollPos] = useState(0)
useEffect(() => {
const nav = document.querySelector("#nav")
const handleScroll = e => {
const pos = e.srcElement.scrollTop / 50
if (pos < 1) {
setScrollPos(pos)
}
}
nav.addEventListener("scroll", handleScroll)
return () => nav.removeEventListener("scroll", handleScroll)
}, [])
return (
<SideBarContainer
sx={{
position: "sticky",
top: "0",
bottom: "0",
height: "100vh",
backgroundColor: "light",
boxShadow: "sidebarShadow",
minWidth: "var(--side-bar-width)",
flexDirection: "column",
}}
>
<Flex
sx={{
px: "4",
pt: "3",
background: "light",
width: "calc(var(--side-bar-width) - 1px)",
flexDirection: "column",
}}
>
<Flex>
<Image
src={Logo}
alt="Medusa logo"
sx={{
height: "28px",
}}
/>
</Flex>
<Flex py={4}>
<SideBarSelector api={api} />
</Flex>
</Flex>
<Flex
id="nav"
sx={{
flex: 1,
position: "relative",
px: "3",
pb: "3",
mr: "1px",
flexDirection: "column",
overflowY: "scroll",
pr: "6px",
scrollbarColor: "faded light",
}}
>
<SideBarFade opacity={scrollPos} />
{data.sections.map((s, i) => {
return <SideBarItem item={s} key={i} />
})}
</Flex>
<Flex sx={{ py: 4, px: 4, borderTop: "1px solid #efefef" }}>
<Image src={LogoMuted} alt="Medusa Type" sx={{ height: "10px" }} />
</Flex>
</SideBarContainer>
)
}
export default Sidebar

View File

@@ -0,0 +1,138 @@
import React, { useContext } from "react"
import Collapsible from "react-collapsible"
import { Flex, Box, Text } from "theme-ui"
import styled from "@emotion/styled"
import { convertToKebabCase } from "../../utils/convert-to-kebab-case"
import ChevronDown from "../icons/chevron-down"
import NavigationContext from "../../context/navigation-context"
const StyledCollapsible = styled(Collapsible)`
margin-bottom: 10px;
`
const Container = styled(Box)`
div.Collapsible span.Collapsible__trigger.is-open {
svg {
transform: rotate(180deg);
}
}
`
const SideBarItem = ({ item }) => {
const {
openSection,
openSections,
currentHash,
currentSection,
goTo,
} = useContext(NavigationContext)
const { section } = item
const subItems = section.paths
.map(p => {
return p.methods
})
.reduce((pre, cur) => {
return pre.concat(cur)
})
.map(m => {
return {
title: m.summary,
path: convertToKebabCase(m.summary),
}
})
const handleClick = () => {
const id = convertToKebabCase(section.section_name)
const element = document.querySelector(`#${id}`)
if (element) {
element.scrollIntoView()
if (!openSections.includes(id)) {
openSection(id)
}
}
}
const handleSubClick = path => {
const id = convertToKebabCase(section.section_name)
goTo({ section: id, method: path })
}
return (
<Container id={`nav-${convertToKebabCase(section.section_name)}`}>
<StyledCollapsible
trigger={
<Flex
sx={{
fontSize: "1",
pl: "16px",
pr: "10px",
alignItems: "center",
borderRadius: "small",
cursor: "pointer",
mr: "4px",
mb: "5px",
height: "25px",
justifyContent: "space-between",
"&:hover, &.active": {
backgroundColor: "faded",
},
}}
className={
currentSection === convertToKebabCase(section.section_name)
? "active"
: null
}
>
{section.section_name} <ChevronDown />
</Flex>
}
open={
currentSection === convertToKebabCase(section.section_name) ||
openSections.includes(convertToKebabCase(section.section_name))
}
onTriggerOpening={handleClick}
transitionTime={1}
>
{subItems.map((si, i) => {
const path = convertToKebabCase(si.path)
return (
<Flex
key={i}
className={currentHash === path ? "active" : null}
onClick={() => handleSubClick(path)}
id={`nav-${path}`}
sx={{
ml: "10px",
pl: "10px",
pr: "10px",
alignItems: "center",
borderRadius: "small",
cursor: "pointer",
mb: "8px",
textDecoration: "none",
color: "black",
height: "25px",
"&:hover": {
backgroundColor: "faded",
},
"&.active": {
backgroundColor: "faded",
},
}}
>
<Text
sx={{
fontSize: "0",
}}
>
{si.title}
</Text>
</Flex>
)
})}
</StyledCollapsible>
</Container>
)
}
export default SideBarItem

View File

@@ -0,0 +1,56 @@
import React, { useContext } from "react"
import { Flex, Select } from "theme-ui"
import { navigate } from "gatsby-link"
import NavigationContext from "../../context/navigation-context"
import ChevronDown from "../icons/chevron-down"
const SideBarSelector = ({ api }) => {
const { reset } = useContext(NavigationContext)
const handleSelect = e => {
reset()
navigate(`/api/${e.target.value}`)
}
return (
<Flex
sx={{
marginLeft: "-16px",
marginRight: "-10px",
width: "calc(100% + 26px)",
"& div": {
width: "100%",
},
}}
>
<Select
arrow={<ChevronDown fill={"dark"} styles={{ ml: "-28px" }} />}
sx={{
paddingLeft: "16px",
marginRight: "-5px",
borderRadius: "small",
borderColor: "faded",
width: "100%",
fontSize: "1",
fontFamily: "body",
transition: "all .1s ease-in-out",
"&:focus": {
outline: "none !important",
},
boxShadow: "ctaBoxShadow",
"&:hover": {
boxShadow: "buttonBoxShadowHover",
},
}}
value={api}
onChange={handleSelect}
>
<option value="admin">Admin</option>
<option value="store">Storefront</option>
</Select>
</Flex>
)
}
export default SideBarSelector

View File

@@ -0,0 +1,54 @@
import React from "react"
import { Flex, Box, Text } from "theme-ui"
const CodeBox = ({ header, children }) => {
return (
<Box
sx={{
background: "fadedContrast",
borderRadius: "small",
boxShadow: "0 0 0 1px rgb(0 0 0 / 7%)",
alignSelf: "flex-start",
marginLeft: "auto",
marginRight: "auto",
width: "100%",
mb: "4",
}}
>
<Box
sx={{
bg: "faded",
p: "8px 10px",
letterSpacing: "0.01em",
borderRadius: "8px 8px 0 0",
}}
>
<Text variant="small" sx={{ fontWeight: "400" }}>
{header}
</Text>
</Box>
<Box
sx={{
position: "relative",
boxSizing: "content-box",
maxHeight: "calc(90vh - 20px)",
minHeight: "10px",
}}
>
<Flex
sx={{
flexDirection: "column",
position: "relative",
minHeight: "inherit",
maxHeight: "inherit",
overflowY: "auto",
}}
>
{children}
</Flex>
</Box>
</Box>
)
}
export default CodeBox

View File

@@ -0,0 +1,130 @@
import React, { useState } from "react"
import Collapsible from "react-collapsible"
import { Flex, Box, Text, Heading } from "theme-ui"
import Markdown from "react-markdown"
import Description from "./description"
const NestedCollapsible = ({ properties, title }) => {
const [isOpen, setIsOpen] = useState(false)
return (
<Box
mt={2}
sx={{
"& .child-attrs": {
cursor: "pointer",
fontSize: "12px",
p: "6px 10px",
boxSizing: "border-box",
width: "max-content",
borderRadius: "small",
border: "1px solid",
borderColor: "faded",
},
"& .child-attrs.is-open": {
width: "100%",
borderBottom: "none",
borderBottomLeftRadius: "0",
borderBottomRightRadius: "0",
},
}}
>
<Collapsible
transitionTime={50}
triggerClassName={"child-attrs"}
triggerOpenedClassName={"child-attrs"}
triggerTagName="div"
trigger={
<Flex
sx={{
alignItems: "baseline",
fontSize: "13px",
fontWeight: "400",
}}
>
<Box
mr={1}
className={isOpen ? "rotated" : null}
sx={{
userSelect: "none",
transition: "all 0.2s ease",
transform: "rotate(45deg)",
"&.rotated": {
transform: "rotate(0deg)",
},
}}
>
&times;
</Box>{" "}
<Text
sx={{ fontSize: "0", fontFamily: "body", userSelect: "none" }}
>{`${isOpen ? "Hide" : "Show"} child attributes`}</Text>
</Flex>
}
onTriggerOpening={() => setIsOpen(true)}
onTriggerClosing={() => setIsOpen(false)}
>
<Box
sx={{
padding: "2",
borderRadius: "small",
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
border: "hairline",
borderColor: "faded",
}}
mb="2"
>
<Heading as="h3" p={2} sx={{ fontFamily: "body", fontWeight: "500" }}>
{title}
</Heading>
{properties.map((param, i) => {
return (
<Box
p={2}
sx={{
borderTop: "hairline",
}}
key={i}
>
<Flex
sx={{
fontSize: "0",
alignItems: "baseline",
pb: "1",
fontFamily: "monospace",
}}
>
<Box mr={2} fontSize={"12px"}>
{param.property}
</Box>
<Text color={"gray"} fontSize={"10px"}>
{param.type}
</Text>
{param.required && (
<Text ml={1} fontSize={"10px"} variant="labels.required">
required
</Text>
)}
</Flex>
<Description>
<Text
sx={{
fontSize: "0",
lineHeight: "26px",
fontFamily: "body",
}}
>
<Markdown>{param.description}</Markdown>
</Text>
</Description>
</Box>
)
})}
</Box>
</Collapsible>
</Box>
)
}
export default NestedCollapsible

View File

@@ -0,0 +1,20 @@
import React from "react"
import { Box } from "theme-ui"
const Description = ({ children }) => {
return (
<Box
sx={{
code: {
backgroundColor: "faded",
borderRadius: "4px",
p: "3px",
},
}}
>
{children}
</Box>
)
}
export default Description

View File

@@ -0,0 +1,45 @@
import React from "react"
import { Flex, Text } from "theme-ui"
import CodeBox from "./code-box"
const EndpointContainer = ({ endpoints }) => {
if (!endpoints) return null
return (
<CodeBox header="ENDPOINTS">
<Flex
py={2}
sx={{
flexDirection: "column",
}}
>
{endpoints.map((e, i) => {
const method = e.method.toUpperCase()
const endpoint = e.endpoint
return (
<Flex
key={i}
sx={{ fontSize: "0", fontFamily: "monospace", px: "3", py: "2" }}
>
<Text
variant={`labels.${method}`}
sx={{
width: "55px",
textAlign: "right",
}}
mr={2}
>
{method}
</Text>
<Text sx={{ color: "dark" }}>
{endpoint.replace(/{(.*?)}/g, ":$1")}
</Text>
</Flex>
)
})}
</Flex>
</CodeBox>
)
}
export default EndpointContainer

View File

@@ -0,0 +1,31 @@
import React from "react"
import { Box, Flex } from "theme-ui"
import Topbar from "../topbar"
import Section from "./section"
const Content = ({ data, api }) => {
return (
<Flex
sx={{
flexDirection: "column",
}}
>
<Topbar data={data} api={api} />
<Box
sx={{
maxHeight: "calc(100vh - 50px)",
overflowY: "scroll",
"@media screen and (max-width: 848px)": {
mt: "50px",
},
}}
>
{data.sections.map((s, i) => {
return <Section key={i} data={s} />
})}
</Box>
</Flex>
)
}
export default Content

View File

@@ -0,0 +1,31 @@
import React, { useRef, useEffect } from "react"
import { Box } from "theme-ui"
import "../../prism-medusa-theme/prism.css"
import Prism from "prismjs"
import "prismjs/components/prism-json"
import CodeBox from "./code-box"
const JsonContainer = ({ json, header }) => {
const jsonRef = useRef()
//INVESTIGATE: @theme-ui/prism might be a better solution
useEffect(() => {
if (jsonRef.current) {
Prism.highlightAllUnder(jsonRef.current)
}
}, [])
if (typeof json !== "string" || json === "{}") return null
return (
<Box ref={jsonRef} sx={{ position: "sticky", top: "20px" }}>
<CodeBox header={header}>
<pre>
<code className={"language-json"}>{json}</code>
</pre>
</CodeBox>
</Box>
)
}
export default JsonContainer

View File

@@ -0,0 +1,110 @@
import React, { useContext, useEffect, useRef } from "react"
import Markdown from "react-markdown"
import { Flex, Text, Box, Heading } from "theme-ui"
import { convertToKebabCase } from "../../utils/convert-to-kebab-case"
import Parameters from "./parameters"
import Route from "./route"
import JsonContainer from "./json-container"
import Description from "./description"
import ResponsiveContainer from "./responsive-container"
import { formatMethodParams } from "../../utils/format-parameters"
import useInView from "../../hooks/use-in-view"
import NavigationContext from "../../context/navigation-context"
const Method = ({ data, section, pathname }) => {
const { parameters, requestBody, description, method, summary } = data
const jsonResponse = data.responses[0].content?.[0].json
const { updateHash, updateMetadata } = useContext(NavigationContext)
const methodRef = useRef(null)
const [containerRef, isInView] = useInView({
root: null,
rootMargin: "0px 0px -80% 0px",
threshold: 0,
})
useEffect(() => {
if (isInView) {
updateHash(section, convertToKebabCase(summary))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isInView])
const handleMetaChange = () => {
updateMetadata({
title: summary,
description: description,
})
if (methodRef.current) {
methodRef.current.scrollIntoView({
behavior: "smooth",
})
}
}
return (
<Flex
py={"5vw"}
sx={{
borderTop: "hairline",
flexDirection: "column",
}}
id={convertToKebabCase(summary)}
ref={methodRef}
>
<Flex>
<Heading
as="h2"
mb={4}
sx={{
fontSize: "4",
fontWeight: "500",
cursor: "pointer",
}}
ref={containerRef}
onClick={() => handleMetaChange()}
>
{summary}
</Heading>
</Flex>
<ResponsiveContainer>
<Flex
className="info"
sx={{
flexDirection: "column",
pr: "5",
"@media screen and (max-width: 848px)": {
pr: "0",
},
}}
>
<Route path={pathname} method={method} />
<Description>
<Text
sx={{
lineHeight: "26px",
}}
mt={3}
>
<Markdown>{description}</Markdown>
</Text>
</Description>
<Box mt={4}>
<Parameters
params={formatMethodParams({ parameters, requestBody })}
/>
</Box>
</Flex>
<Box className="code">
<JsonContainer
json={jsonResponse}
header={"RESPONSE"}
method={convertToKebabCase(summary)}
/>
</Box>
</ResponsiveContainer>
</Flex>
)
}
export default Method

View File

@@ -0,0 +1,68 @@
import React from "react"
import { Flex, Text, Box } from "theme-ui"
import Markdown from "react-markdown"
import NestedCollapsible from "./collapsible"
import Description from "./description"
const Parameters = ({ params, type }) => {
return (
<Flex
sx={{
flexDirection: "column",
}}
>
<Text pb="2">{type === "attr" ? "Attributes" : "Parameters"}</Text>
{params.properties.length > 0 ? (
params.properties.map((prop, i) => {
const nested = prop.nestedModel || prop.items?.properties || null
return (
<Box
py={2}
sx={{
borderTop: "hairline",
fontFamily: "monospace",
fontSize: "0",
}}
key={i}
>
<Flex sx={{ alignItems: "baseline", fontSize: "0" }}>
<Text mr={2}>{prop.property || prop.name}</Text>
<Text color={"gray"}>
{prop.type || prop.schema?.type || nested?.title}
</Text>
{prop.required ? (
<Text ml={1} variant="labels.required">
required
</Text>
) : null}
</Flex>
<Description>
<Text
sx={{
fontSize: "0",
lineHeight: "26px",
fontFamily: "body",
}}
>
<Markdown>{prop.description}</Markdown>
</Text>
</Description>
{nested?.properties && (
<NestedCollapsible
properties={nested.properties}
title={nested.title}
/>
)}
</Box>
)
})
) : (
<Text sx={{ fontSize: "0", py: "3", fontFamily: "monospace" }}>
No parameters
</Text>
)}
</Flex>
)
}
export default Parameters

View File

@@ -0,0 +1,29 @@
import React from "react"
import styled from "@emotion/styled"
import { Flex } from "theme-ui"
const ResponsiveFlex = styled(Flex)`
.info {
width: 55%;
}
.code {
width: 45%;
}
@media screen and (max-width: 848px) {
flex-direction: column;
align-items: center;
.info,
.code {
width: 100%;
}
}
`
const ResponsiveContainer = ({ children }) => {
return <ResponsiveFlex>{children}</ResponsiveFlex>
}
export default ResponsiveContainer

View File

@@ -0,0 +1,17 @@
import React from "react"
import { Flex, Text } from "theme-ui"
import { formatRoute } from "../../utils/format-route"
const Route = ({ method, path }) => {
const fixedMethod = method.toUpperCase()
return (
<Flex sx={{ fontFamily: "monospace" }}>
<Text variant={`labels.${fixedMethod}`} mr={1}>
{fixedMethod}
</Text>
<Text>{formatRoute(path)}</Text>
</Flex>
)
}
export default Route

View File

@@ -0,0 +1,205 @@
import React, { useState, useRef, useEffect, useContext } from "react"
import { Flex, Box, Heading, Text, Button } from "theme-ui"
import Method from "./method"
import Parameters from "./parameters"
import { convertToKebabCase } from "../../utils/convert-to-kebab-case"
import EndpointContainer from "./endpoint-container"
import Markdown from "react-markdown"
import JsonContainer from "./json-container"
import ResponsiveContainer from "./responsive-container"
import Description from "./description"
import NavigationContext from "../../context/navigation-context"
import ChevronDown from "../icons/chevron-down"
import useInView from "../../hooks/use-in-view"
const Section = ({ data }) => {
const { section } = data
const [isExpanded, setIsExpanded] = useState(false)
const { openSections, updateSection, updateMetadata } = useContext(
NavigationContext
)
const endpoints = section.paths
.map(p => {
let path = p.name
let ep = []
p.methods.forEach(m => {
ep.push({ method: m.method, endpoint: path })
})
return ep
})
.flat()
const sectionRef = useRef(null)
const scrollIntoView = () => {
if (sectionRef.current) {
sectionRef.current.scrollIntoView({
behavior: "smooth",
})
}
}
const handleExpand = () => {
updateMetadata({
title: section.section_name,
description: section.schema?.description,
})
setIsExpanded(true)
scrollIntoView()
}
useEffect(() => {
if (isExpanded) {
scrollIntoView()
}
}, [isExpanded])
useEffect(() => {
const shouldOpen = openSections.includes(
convertToKebabCase(section.section_name)
)
if (shouldOpen) {
setIsExpanded(true)
}
}, [section.section_name, openSections, openSections.length])
const [containerRef, isInView] = useInView({
root: null,
rootMargin: "0px 0px -80% 0px",
threshold: 1.0,
})
useEffect(() => {
const handleInView = () => {
if (isInView) {
updateSection(convertToKebabCase(section.section_name))
}
}
handleInView()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isInView])
return (
<section ref={sectionRef} id={convertToKebabCase(section.section_name)}>
<Box
sx={{
borderBottom: "hairline",
padding: "5vw",
backgroundColor: isExpanded ? "transparent" : "fadedContrast",
}}
>
<Flex>
<Heading
as="h1"
sx={{
fontWeight: "500",
fontSize: "22",
mb: "3",
cursor: "pointer",
}}
ref={containerRef}
className={`header-${convertToKebabCase(section.section_name)}`}
onClick={handleExpand}
>
{section.section_name}
</Heading>
</Flex>
<Flex
sx={{
flexDirection: "column",
}}
>
<ResponsiveContainer>
<Flex
sx={{
flexDirection: "column",
lineHeight: "26px",
pr: "5",
"@media screen and (max-width: 848px)": {
pr: "0",
},
}}
className="info"
>
<Description>
<Text mb={4}>
<Markdown>{section.schema?.description}</Markdown>
</Text>
</Description>
{isExpanded && section.schema ? (
<Parameters params={section.schema} type={"attr"} />
) : null}
</Flex>
<Flex
className="code"
sx={{
flexDirection: "column",
}}
>
<EndpointContainer endpoints={endpoints} />
{isExpanded ? (
<JsonContainer
json={section.schema?.object}
header={`${section.section_name.toUpperCase()} OBJECT`}
/>
) : null}
</Flex>
</ResponsiveContainer>
{isExpanded ? (
<Box mt={4}>
{section.paths.map((p, i) => {
return (
<Flex
key={i}
sx={{
flexDirection: "column",
}}
>
{p.methods.map((m, i) => {
return (
<Method
key={i}
data={m}
section={convertToKebabCase(section.section_name)}
pathname={p.name}
/>
)
})}
</Flex>
)
})}
</Box>
) : (
<Flex
sx={{
justifyContent: "center",
alignItems: "center",
width: "100%",
}}
mt={4}
>
<Button
onClick={handleExpand}
sx={{
display: "flex",
alignItems: "center",
borderRadius: "24px",
bg: "light",
fontWeight: "500",
}}
>
SHOW <ChevronDown fill={"dark"} styles={{ mr: "-10px" }} />
</Button>
</Flex>
)}
</Flex>
</Box>
</section>
)
}
export default Section

View File

@@ -0,0 +1,24 @@
import React from "react"
import { Box } from "theme-ui"
const ChevronDown = ({ fill = "darkContrast", styles }) => {
return (
<Box
as="svg"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
sx={{
...styles,
alignSelf: "center",
pointerEvents: "none",
fill: `${fill}`,
}}
>
<path d="M7.41 7.84l4.59 4.58 4.59-4.58 1.41 1.41-6 6-6-6z" />
</Box>
)
}
export default ChevronDown

View File

@@ -0,0 +1,18 @@
import { Image } from "@theme-ui/components"
import React from "react"
import Logo from "../../assets/github.svg"
const GitHub = () => {
return (
<Image
src={Logo}
sx={{
height: "20px",
fill: "#000",
}}
/>
)
}
export default GitHub

View File

@@ -0,0 +1,26 @@
import React from "react"
import { Flex, Box } from "theme-ui"
import Sidebar from "./sidebar"
const Layout = ({ data, api, children }) => {
return (
<Flex sx={{ p: "0", m: "0", overflow: "hidden" }}>
<Flex
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
fontFamily: "body",
flexGrow: "1",
}}
>
<Sidebar data={data} api={api} />
<Box>{children}</Box>
</Flex>
</Flex>
)
}
export default Layout

View File

@@ -0,0 +1,115 @@
import { Flex, Box, Link, Select } from "@theme-ui/components"
import React, { useContext } from "react"
import { navigate } from "gatsby-link"
import GitHub from "../components/icons/github"
import NavigationContext from "../context/navigation-context"
import { convertToKebabCase } from "../utils/convert-to-kebab-case"
import ChevronDown from "./icons/chevron-down"
const Topbar = ({ data, api }) => {
const { goTo, reset, currentSection } = useContext(NavigationContext)
const handleChange = e => {
const parts = e.target.value.split(" ")
if (parts[0] === api) {
goTo({ section: parts[1] })
} else {
reset()
navigate(`/api/${api === "admin" ? "store" : "admin"}`)
}
}
return (
<Flex
sx={{
justifyContent: "space-between",
alignItems: "center",
width: "100%",
px: "5vw",
height: "headerHeight",
boxShadow: "topbarShadow",
"@media screen and (max-width: 848px)": {
position: "fixed",
top: "0",
left: "0",
right: "0",
zIndex: "9999",
backgroundColor: "light",
},
}}
>
<Box>
<Box
sx={{
display: "none",
alignItems: "center",
"@media screen and (max-width: 848px)": {
display: "block",
},
}}
>
<Select
arrow={<ChevronDown fill={"dark"} />}
sx={{
border: "none",
width: "100%",
fontSize: "2",
fontFamily: "body",
fontWeight: "500",
flexGrow: "1",
px: "0",
transition: "all .1s ease-in-out",
"&:focus": {
outline: "none !important",
},
}}
onChange={handleChange}
value={`${api} ${currentSection}`}
>
<optgroup label={`${api.slice(0, 1).toUpperCase()}${api.slice(1)}`}>
{data.sections.map((s, i) => {
return (
<option
key={i}
value={`${api} ${convertToKebabCase(
s.section.section_name
)}`}
>
{s.section.section_name}
</option>
)
})}
</optgroup>
<optgroup label={api === "admin" ? "Store" : "Admin"}>
<option>{`Go to ${
api === "admin" ? "Storefront API" : "Admin API"
}`}</option>
</optgroup>
</Select>
</Box>
</Box>
<Flex
sx={{
alignItems: "center",
}}
>
<Link variant="topbar" mr={3} href="https://docs.medusa-commerce.com">
Docs
</Link>
<Link
sx={{
pt: "4px",
}}
variant="topbar"
href="https://github.com/medusajs/medusa"
>
<GitHub />
</Link>
</Flex>
</Flex>
)
}
export default Topbar

View File

@@ -0,0 +1,157 @@
import React, { useReducer } from "react"
import scrollParent from "../utils/scroll-parent"
import types from "./types"
export const defaultNavigationContext = {
api: "null",
setApi: () => {},
currentSection: null,
updateSection: () => {},
currentHash: null,
updateHash: () => {},
openSections: [],
openSection: () => {},
metadata: null,
updateMetadata: () => {},
reset: () => {},
}
const NavigationContext = React.createContext(defaultNavigationContext)
export default NavigationContext
const reducer = (state, action) => {
switch (action.type) {
case types.SET_API: {
return {
...state,
api: action.payload,
}
}
case types.UPDATE_HASH:
return {
...state,
currentSection: action.payload.section,
currentHash: action.payload.method,
}
case types.UPDATE_SECTION:
return {
...state,
currentSection: action.payload,
currentHash: null,
}
case types.OPEN_SECTION:
const obj = state.openSections
obj.push(action.payload)
return {
...state,
openSections: obj,
}
case types.RESET:
return {
...state,
openSections: [],
currentSection: null,
currentHash: null,
}
case types.UPDATE_METADATA:
return {
...state,
metadata: {
title: action.payload.title,
description: action.payload.description,
},
}
default:
return state
}
}
const scrollNav = id => {
const nav = document.querySelector("#nav")
if (nav) {
const element = nav.querySelector(`#nav-${id}`)
if (element) {
scrollParent(nav, element)
}
}
}
const scrollToElement = async id => {
const element = document.querySelector(`#${id}`)
if (element) {
element.scrollIntoView({
block: "start",
inline: "nearest",
})
} else {
setTimeout(() => {
scrollToElement(id)
}, 100)
}
}
export const NavigationProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultNavigationContext)
const setApi = api => {
dispatch({ type: types.SET_API, payload: api })
}
const updateMetadata = metadata => {
dispatch({ type: types.UPDATE_METADATA, payload: metadata })
}
const updateHash = (section, method) => {
dispatch({
type: types.UPDATE_HASH,
payload: { method: method, section: section },
})
window.history.replaceState(
null,
"",
`/api/${state.api}/${section}/${method}`
)
scrollNav(method)
}
const updateSection = section => {
dispatch({ type: types.UPDATE_SECTION, payload: section })
window.history.replaceState(null, "", `/api/${state.api}/${section}`)
scrollNav(section)
}
const openSection = sectionName => {
dispatch({ type: types.OPEN_SECTION, payload: sectionName })
}
const goTo = to => {
const { section, method } = to
if (!state.openSections.includes(section)) {
openSection(section)
}
scrollToElement(method || section)
}
const reset = () => {
dispatch({ type: types.RESET })
}
return (
<NavigationContext.Provider
value={{
...state,
openSection,
updateSection,
updateHash,
setApi,
goTo,
reset,
updateMetadata,
dispatch,
}}
>
{children}
</NavigationContext.Provider>
)
}

View File

@@ -0,0 +1,10 @@
const types = {
SET_API: "SET_API",
UPDATE_HASH: "UPDATE_HASH",
UPDATE_SECTION: "UPDATE_SECTION",
OPEN_SECTION: "OPEN_SECTION",
RESET: "RESET",
UPDATE_METADATA: "UPDATE_METADATA",
}
export default types

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/no-anonymous-default-export
export default ["40em", "52em", "64em"]

View File

@@ -163,6 +163,12 @@ export const buttons = {
boxShadow: "ctaBoxShadowHover",
},
},
trigger: {
bg: "faded",
alignItems: "baseline",
fontSize: "13px",
fontWeight: "400",
},
}
export default buttons

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-anonymous-default-export
export default {
invalidInput: {
color: "dark",

View File

@@ -1,6 +1,3 @@
import React from "react"
import { ThemeProvider as Provider } from "emotion-theming"
import "../fonts/index.css"
import breakpoints from "./breakpoints"
@@ -10,19 +7,67 @@ import shadows from "./shadows"
import forms from "./forms"
import labels from "./labels"
export const theme = {
labels,
// eslint-disable-next-line import/no-anonymous-default-export
export default {
sizes: {
headerHeight: "50px",
},
links: {
topbar: {
fontFamily: "body",
fontSize: "2",
textDecoration: "none",
color: "dark",
},
},
text: {
labels,
largest: {
fontFamily: "body",
fontSize: "22px",
fontweight: "300",
lineHeight: "1.5",
letterSpacing: "normal",
},
large: {
fontFamily: "body",
fontSize: "18px",
fontWeight: "300",
lineHeight: "1.22",
letterSpacing: "-0.5px",
},
medium: {
fontFamily: "body",
fontSize: "16px",
fontWeight: "300",
lineHeight: "1.22",
letterSpacing: "normal",
},
base: {
fontFamily: "body",
fontsize: "14px",
fontWeight: "300",
lineHeight: "1.22",
letterSpacing: "-0.25px",
},
small: {
fontFamily: "body",
fontSize: "12px",
fontWeight: "300",
lineHeight: "12px",
letterSpacing: "0px",
},
},
colors: {
primary: "#53725D",
secondary: "#79B28A",
medusaGreen: "#53725D",
danger: "#FF7675",
muted: "#E3E8EE",
lightest: "#fefefe",
light: "#f0f0f0",
dark: "#454545",
darkest: "#212121",
placeholder: "#a3acb9",
dark: "#0a3149",
darkContrast: "#0a314940",
light: "#ffffff",
faded: "#eef0f5",
fadedContrast: "#eef0f540",
},
borders: {
hairline: "1px solid #E3E8EE",
@@ -80,8 +125,7 @@ export const theme = {
},
forms,
buttons,
radii: {
small: "8px",
},
}
export const ThemeProvider = ({ children }) => (
<Provider theme={theme}>{children}</Provider>
)

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-anonymous-default-export
export default {
pill: `
rgba(0, 0, 0, 0) 0px 0px 0px 0px,
@@ -108,4 +109,11 @@ export default {
rgba(62, 207, 142, 0.25) 0px 3px 9px 0px;
rgba(62, 207, 142, 0.25) 0px 2px 5px 0px;
`,
sidebarShadow: `
inset -1px 0 0 0 #eef0f5
`,
topbarShadow: `
0 2px 5px 0 rgb(59 65 94 / 10%), 0 1px 1px 0 rgb(0 0 0 / 7%)
`,
}

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/no-anonymous-default-export
export default [0, 4, 8, 16, 32, 64]

View File

@@ -0,0 +1,29 @@
import { useEffect, useRef, useState } from "react"
const useInView = options => {
const containerRef = useRef(null)
const [isInView, setIsInView] = useState(false)
const callback = entries => {
const [entry] = entries
setIsInView(entry.isIntersecting)
}
useEffect(() => {
const ref = containerRef.current
const observer = new IntersectionObserver(callback, options)
if (ref) {
observer.observe(ref)
}
return () => {
if (ref) {
observer.unobserve(ref)
}
}
}, [containerRef, options])
return [containerRef, isInView]
}
export default useInView

View File

@@ -0,0 +1,123 @@
code[class*="language-"],
pre[class*="language-"] {
color: #0a3149;
background: none;
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
font-size: 12px;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: transparent;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: blue;
}
.token.punctuation {
color: #0a3149;
}
.token.namespace {
opacity: 0.7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #0a3149;
}
.token.number {
color: #e56f4a;
}
.token.boolean {
color: #3a97d4;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #09825d;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #0a3149;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #e6db74;
}
.token.keyword {
color: #3a97d4;
}
.token.keyword.null {
color: grey;
}
.token.regex,
.token.important {
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@@ -0,0 +1,43 @@
import React, { useContext, useEffect, useState } from "react"
import { Helmet } from "react-helmet"
import Layout from "../components/layout"
import Content from "../components/content"
import NavigationContext from "../context/navigation-context"
export default function ReferencePage({
pageContext: { data, api, title, description, to },
}) {
const { setApi, goTo, metadata } = useContext(NavigationContext)
const [siteData, setSiteData] = useState({
title: title,
description: description,
})
useEffect(() => {
setApi(api)
if (to) {
goTo(to)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (metadata) {
setSiteData({
title: metadata.title,
description: metadata.description,
})
}
}, [metadata])
return (
<Layout data={data} api={api}>
<Helmet>
<title>{`${siteData.title} | Medusa Commerce API Reference`}</title>
<meta name="description" content={siteData.description} />
</Helmet>
<Content data={data} api={api} />
</Layout>
)
}

View File

@@ -0,0 +1,8 @@
export const convertToKebabCase = string => {
return string
.replace(/\s+/g, "-")
.replace("'", "")
.replace(".", "")
.replace('"', "")
.toLowerCase()
}

View File

@@ -0,0 +1,29 @@
export const formatMethodParams = method => {
const { parameters, requestBody } = method
const params = []
if (parameters && parameters.length > 0) {
parameters.map(p => {
return params.push({
property: p.name,
description: p.description,
required: p.required,
type: p.schema.type,
})
})
}
if (requestBody) {
const { required, properties } = requestBody
properties.map(p => {
return params.push({
property: p.property,
description: p.description,
required: required ? required.some(req => req === p.property) : false,
type: p.type,
nestedModel: p.nestedModel,
})
})
}
return { properties: params }
}

View File

@@ -0,0 +1,3 @@
export const formatRoute = string => {
return string.replace(/{(.*?)}/g, ":$1")
}

View File

@@ -0,0 +1,215 @@
import { graphql } from "gatsby"
export const StoreSections = graphql`
fragment StoreSections on StoreSections {
sections {
...StoreSection
}
}
`
export const StoreSection = graphql`
fragment StoreSection on StoreSectionsSection {
section_name
paths {
...StorePath
}
schema {
object
description
properties {
property
type
description
format
nestedModel {
title
properties {
property
type
}
}
}
}
}
`
export const StorePath = graphql`
fragment StorePath on StoreSectionsSectionPaths {
name
methods {
...StoreMethod
}
}
`
export const StoreMethod = graphql`
fragment StoreMethod on StoreSectionsSectionPathsMethods {
tags
summary
description
method
operationId
responses {
...StoreResponse
}
requestBody {
...StoreRequestBody
}
parameters {
description
in
name
required
schema {
type
}
}
}
`
export const StoreResponse = graphql`
fragment StoreResponse on StoreSectionsSectionPathsMethodsResponses {
status
description
content {
_ref
property
description
json
items {
_ref
}
}
}
`
export const StoreRequestBody = graphql`
fragment StoreRequestBody on StoreSectionsSectionPathsMethodsRequestBody {
type
required
properties {
description
property
type
nestedModel {
title
properties {
property
type
}
}
}
}
`
export const AdminSections = graphql`
fragment AdminSections on AdminSections {
sections {
...AdminSection
}
}
`
export const AdminSection = graphql`
fragment AdminSection on AdminSectionsSection {
section_name
paths {
...AdminPath
}
schema {
object
description
properties {
property
type
description
format
nestedModel {
title
properties {
property
type
description
}
}
}
}
}
`
export const AdminPath = graphql`
fragment AdminPath on AdminSectionsSectionPaths {
name
methods {
...AdminMethod
}
}
`
export const AdminMethod = graphql`
fragment AdminMethod on AdminSectionsSectionPathsMethods {
tags
summary
description
method
operationId
responses {
...AdminResponse
}
requestBody {
...AdminRequestBody
}
parameters {
description
in
name
required
schema {
type
}
}
}
`
export const AdminResponse = graphql`
fragment AdminResponse on AdminSectionsSectionPathsMethodsResponses {
status
description
content {
_ref
type
property
description
json
items {
type
_ref
}
}
}
`
export const AdminRequestBody = graphql`
fragment AdminRequestBody on AdminSectionsSectionPathsMethodsRequestBody {
type
required
properties {
description
enum
format
property
type
nestedModel {
title
properties {
property
type
description
}
}
}
}
`

View File

@@ -0,0 +1,20 @@
const scrollParent = (parent, child) => {
const parentRect = parent.getBoundingClientRect()
const parentViewableArea = {
height: parent.clientHeight,
width: parent.clientWidth,
}
const childRect = child.getBoundingClientRect()
const isViewable =
childRect.top >= parentRect.top &&
childRect.top <= parentRect.top + parentViewableArea.height
if (!isViewable) {
const pos = childRect.top + parent.scrollTop - parentRect.top
parent.scrollTop = pos > 0 ? pos : 0
}
}
export default scrollParent

View File

Before

Width:  |  Height:  |  Size: 225 B

After

Width:  |  Height:  |  Size: 225 B

File diff suppressed because it is too large Load Diff

View File

@@ -1,92 +0,0 @@
import React from "react"
import { Flex, Text } from "rebass"
import styled from "@emotion/styled"
import Markdown from "react-markdown"
import RouteSection from "./route-section"
import JsonBox from "./json-box"
import EndpointOverview from "./endpoint-overview"
import Parameters from "./parameters"
const convertToKebabCase = string => {
return string.replace(/\s+/g, "-").toLowerCase()
}
const EndpointContainer = styled(Flex)`
min-height: 90vh;
position: relative;
border-top: hairline;
code {
background-color: #e3e8ee;
border-radius: 5px;
padding: 4px;
}
`
const DocsReader = ({ tags, spec }) => {
return (
<Flex flexDirection="column" width="100%">
{Object.entries(tags).map(([tagName, endpoints]) => (
<EndpointContainer id={convertToKebabCase(tagName)} p={4}>
<Flex flexDirection="row" width="100%">
<Flex py={5} flexDirection="column" p={4} width="100%">
<EndpointOverview
title={tagName}
description={""}
routes={endpoints}
spec={spec}
/>
<Flex
flexDirection="row"
width={"100%"}
sx={{
position: "relative",
borderTop: "hairline",
}}
>
<Flex width={"100%"} flexDirection="column">
{endpoints.map((endpoint, i) => (
<Flex
id={convertToKebabCase(endpoint.summary)}
py={4}
flexDirection="row"
width="100%"
>
<Flex
pr={5}
width={"55%"}
flexDirection="column"
sx={{ lineHeight: "26px" }}
>
<Text mb={3} fontSize={3}>
{endpoint.summary}
</Text>
<RouteSection
basePath={""}
method={endpoint.method}
path={endpoint.path}
/>
<Markdown>{endpoint.description}</Markdown>
<Parameters spec={spec} endpoint={endpoint} />
</Flex>
<Flex py={5} width="45%" flex="1">
<JsonBox
name={tagName}
endpoint={endpoint}
spec={spec}
/>
</Flex>
</Flex>
))}
</Flex>
</Flex>
</Flex>
</Flex>
</EndpointContainer>
))}
</Flex>
)
}
export default DocsReader

View File

@@ -1,65 +0,0 @@
import React from "react"
import { Flex, Box, Text } from "rebass"
import RoutesOverview from "./routes-overview"
import ParamSection from "./param-section"
import JsonBox from "./json-box"
const EndpointOverview = ({ title, description, routes, spec }) => {
let schema = {}
let attrs = []
let resourceId
if (spec) {
const tag = spec.tags.find(t => t.name === title)
if (tag && tag["x-resourceId"]) {
resourceId = tag["x-resourceId"]
schema = spec.components.schemas[tag["x-resourceId"]]
for (const [name, details] of Object.entries(schema.properties)) {
if (!attrs.find(a => a.name === name)) {
attrs.push({
name,
...details,
})
}
}
}
}
return (
<Flex flexDirection="row" pb={4}>
<Flex
flexDirection="column"
width="55%"
pr={5}
sx={{ lineHeight: "26px" }}
>
<Text mb={3} fontSize={4}>
{title}
</Text>
<Text mb={4}>{description || schema.description}</Text>
{attrs.length > 0 && (
<Box
sx={{
borderBottom: "hairline",
}}
my="2"
>
<Text my={2}>Attributes</Text>
{attrs.map(p => (
<ParamSection param={p} spec={spec} />
))}
</Box>
)}
</Flex>
<Flex width={"45%"} flexDirection="column">
{routes && <RoutesOverview content={routes} />}
{resourceId && <JsonBox text={"OBJECT"} resourceId={resourceId} />}
</Flex>
</Flex>
)
}
export default EndpointOverview

View File

@@ -1,102 +0,0 @@
import React from "react"
import { Flex, Box, Text } from "rebass"
import styled from "@emotion/styled"
import Highlight from "react-highlight.js"
import "highlight.js/styles/a11y-light.css"
import fixtures from "../../../docs/api/fixtures.json"
import { deref } from "../utils/deref"
export const ResponseContainer = styled(Flex)`
border: 1px solid #e3e8ee;
border-radius: 5px;
margin-left: auto;
margin-right: auto;
width: 100%;
max-height: calc(90vh - 20px);
overflow-y: scroll;
align-self: flex-start;
font-size: 1;
position: sticky;
top: 20px;
code {
background: #f7fafc !important;
}
`
const JsonBox = ({ text, resourceId, endpoint, spec }) => {
let json = {}
const toSet = {}
if (endpoint) {
const props =
endpoint?.responses?.["200"]?.content?.["application/json"]?.schema
?.properties
if (props) {
for (const [name, details] of Object.entries(props)) {
let cleanDets = details
if (details.$ref) {
const [, ...path] = details.$ref.split("/")
cleanDets = deref(path, spec)
}
if (
cleanDets["x-resourceId"] &&
cleanDets["x-resourceId"] in fixtures.resources
) {
toSet[name] = fixtures.resources[cleanDets["x-resourceId"]]
} else if (cleanDets.type === "array") {
toSet[name] = cleanDets
if (cleanDets.items.$ref) {
const [, ...path] = cleanDets.items.$ref.split("/")
let cleanObj = deref(path, spec)
if (
cleanObj["x-resourceId"] &&
cleanObj["x-resourceId"] in fixtures.resources
) {
cleanObj = fixtures.resources[cleanObj["x-resourceId"]]
}
toSet[name] = [cleanObj]
}
}
}
}
}
if (resourceId) {
json = fixtures.resources[resourceId]
} else {
json = toSet
}
return (
<ResponseContainer flexDirection="column" as="pre">
<Text
fontSize={0}
fontFamily="body"
py={2}
px={3}
color="#4f566b"
backgroundColor="#e3e8ee"
>
{text || "RESPONSE"}
</Text>
<Box
w={1}
flex="1"
sx={{ overflowY: "scroll" }}
backgroundColor="#f7fafc"
>
<Highlight language="json">
{JSON.stringify(json, undefined, 2)}
</Highlight>
</Box>
</ResponseContainer>
)
}
export default JsonBox

View File

@@ -1,24 +0,0 @@
import React from "react"
import { Flex, Box } from "rebass"
const Layout = ({ children }) => {
return (
<Flex
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
overflowY: "scroll",
}}
fontFamily={"body"}
flexDirection="column"
flexGrow="1"
>
<Box>{children}</Box>
</Flex>
)
}
export default Layout

View File

@@ -1,161 +0,0 @@
import React from "react"
import styled from "@emotion/styled"
import { Flex, Box, Text } from "rebass"
import Markdown from "react-markdown"
import Collapsible from "react-collapsible"
import { deref } from "../utils/deref"
const ExpandContainer = styled.div`
.child-attrs {
cursor: pointer;
font-size: 12px;
box-sizing: border-box;
padding-left: 10px;
padding-right: 10px;
width: max-content;
border-radius: 5px;
border: 1px solid #e3e8ee;
color: #afafaf;
&:hover {
color: #212121;
}
}
.child-attrs.is-open {
width: 100%;
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
`
const Expand = ({ schema, spec }) => {
if (schema.$ref) {
const [, ...path] = schema.$ref.split("/")
schema = deref(path, spec)
}
const properties = schema.properties
let aggregated = []
for (const [name, details] of Object.entries(properties)) {
let cleanDets = details
if (name === "$ref") {
const [, ...path] = details.split("/")
cleanDets = deref(path, spec)
}
if (!aggregated.find(a => a.name === name)) {
aggregated.push({
name,
...cleanDets,
})
}
}
return (
<ExpandContainer>
<Collapsible
transitionTime={50}
triggerClassName={"child-attrs"}
triggerOpenedClassName={"child-attrs"}
triggerTagName="div"
trigger={"Show nested attributes"}
triggerWhenOpen={"Hide"}
>
<Box
sx={{
padding: "10px",
borderRadius: "5px",
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
border: "hairline",
}}
mb="2"
>
<Text my={2}>{schema.title}</Text>
{aggregated.map(param => {
let type = param.type
if (!type && param.schema) {
type = param.schema.type
}
return (
<Box
py={2}
sx={{
borderTop: "hairline",
}}
>
<Flex
fontSize="1"
alignItems="baseline"
pb={1}
fontFamily="monospace"
>
<Box mr={2} fontSize={"12px"}>
{param.name}
</Box>
<Text color={"gray"} fontSize={"10px"}>
{type}
</Text>
{param.required && (
<Text ml={1} fontSize={"10px"} variant="labels.required">
required
</Text>
)}
</Flex>
<Text fontSize={0}>
<Markdown>{param.description}</Markdown>
</Text>
</Box>
)
})}
</Box>
</Collapsible>
</ExpandContainer>
)
}
const ParamSection = ({ routeParam, param, spec }) => {
let type = param.type
if (!type && param.schema) {
type = param.schema.type
}
return (
<Box
py={2}
sx={{
borderTop: "hairline",
}}
>
<Flex fontSize="1" alignItems="baseline" pb={1} fontFamily="monospace">
<Box mr={2} fontSize={"12px"}>
{param.name}
</Box>
<Text color={"gray"} fontSize={"10px"}>
{type}
</Text>
{(param.required || routeParam) && (
<Text ml={1} fontSize={"10px"} variant="labels.required">
required
</Text>
)}
</Flex>
<Text fontSize={0}>
<Markdown>{param.description}</Markdown>
</Text>
{param.anyOf &&
param.anyOf.map(schema => <Expand schema={schema} spec={spec} />)}
{param.oneOf &&
param.oneOf.map(schema => <Expand schema={schema} spec={spec} />)}
{param.type === "array" && param.items?.properties && (
<Expand schema={param.items} spec={spec} />
)}
</Box>
)
}
export default ParamSection

View File

@@ -1,49 +0,0 @@
import React from "react"
import { Box, Text } from "rebass"
import ParamSection from "./param-section"
import { deref } from "../utils/deref"
const Parameters = ({ endpoint, spec }) => {
const aggregated = endpoint.parameters || []
const reqBody = endpoint.requestBody || {}
const props = reqBody.content?.["application/json"]?.schema?.properties
if (props) {
for (const [name, details] of Object.entries(props)) {
let cleanDets = details
if (name === "$ref") {
const [, ...path] = details.split("/")
cleanDets = deref(path, spec)
}
if (!aggregated.find(a => a.name === name)) {
aggregated.push({
name,
...cleanDets,
})
}
}
}
if (!aggregated.length) {
return null
}
return (
<Box
sx={{
borderBottom: "hairline",
}}
my="2"
>
<Text my={2}>Parameters</Text>
{aggregated.map(p => (
<ParamSection param={p} spec={spec} />
))}
</Box>
)
}
export default Parameters

View File

@@ -1,19 +0,0 @@
import React from "react"
import { Flex, Box, Text } from "rebass"
const RouteSection = ({ basePath, path, method }) => {
path = path.replace(/{(.*?)}/g, ":$1")
return (
<Box py={2}>
<Flex fontFamily="monospace">
<Text mr={2} variant={`labels.${method}`}>
{method}
</Text>
<Text>{`${basePath}${path === "/" ? "" : path}`}</Text>
</Flex>
</Box>
)
}
export default RouteSection

View File

@@ -1,64 +0,0 @@
import React from "react"
import styled from "@emotion/styled"
import { Flex, Box, Text } from "rebass"
const StyledRoutesOverview = styled(Flex)`
border: 1px solid #e3e8ee;
border-radius: 5px;
margin-left: auto;
margin-right: auto;
width: 100%;
max-height: calc(90vh - 20px);
overflow-y: scroll;
align-self: flex-start;
font-size: 1;
top: 20px;
bottom: 20px;
`
const RoutesOverview = ({ content }) => {
if (!content) return null
return (
<StyledRoutesOverview mb={3} flexDirection="column">
<Text
fontSize={0}
fontFamily="body"
py={2}
px={3}
color="#4f566b"
backgroundColor="#e3e8ee"
>
ENDPOINTS
</Text>
<Box
w={1}
px={4}
py={2}
flex="1"
sx={{ overflowY: "scroll" }}
backgroundColor="#f7fafc"
>
<Flex fontSize={1} flexDirection="column">
{content.map(route => (
<Flex mb={2} width="100%">
<Text
width="55px"
mr={2}
variant={`labels.${route.method}`}
textAlign="right"
>
{route.method}
</Text>
<Text color="#4f566b">
{route.path?.replace(/{(.*?)}/g, ":$1")}
</Text>
</Flex>
))}
</Flex>
</Box>
</StyledRoutesOverview>
)
}
export default RoutesOverview

View File

@@ -1,107 +0,0 @@
import React from "react"
import { Flex } from "rebass"
import { Label, Select as RebassSelect } from "@rebass/forms"
import styled from "@emotion/styled"
import Typography from "./typography"
const StyledSelect = styled(RebassSelect)`
${Typography.Base}
padding-right: 28px;
${props =>
props.isCurrencyInput &&
`
box-shadow: none;
border: none;
&:hover {
box-shadow: none;
}
`}
`
const StyledLabel = styled.div`
${Typography.Base}
${props =>
props.inline
? `
text-align: right;
padding-right: 15px;
`
: `
padding-bottom: 10px;
`}
${props =>
props.required &&
`
&:after {
color: rgba(255, 0, 0, 0.5);
content: " *";
}
`}
`
const Select = React.forwardRef(
(
{
name = "",
label = "",
defaultValue = "",
options = [],
placeholder = "",
value,
onChange,
inline,
required,
selectHeight,
isCurrencyInput,
...props
},
ref
) => {
return (
<Flex
alignItems={inline && "center"}
flexDirection={inline ? "row" : "column"}
{...props}
>
{label && (
<Label
flex={"30% 0 0"}
maxWidth={"200px"}
htmlFor={name}
display={props.start ? "flex" : inline && "inline !important"}
>
<StyledLabel required={required} inline={inline}>
{label}
</StyledLabel>
</Label>
)}
<StyledSelect
isCurrencyInput={isCurrencyInput}
flex="50% 0 0"
variant="buttons.primary"
name={name}
height={selectHeight || "inherit"}
minWidth={"unset"}
width={"unset"}
ref={ref}
value={value}
defaultValue={defaultValue}
onChange={onChange}
>
{placeholder && <option>{placeholder}</option>}
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.label || option.value}
</option>
))}
</StyledSelect>
</Flex>
)
}
)
export default Select

View File

@@ -1,130 +0,0 @@
import React, { useState, useEffect } from "react"
import { navigate } from "gatsby"
import { Flex, Box, Text } from "rebass"
import { AnchorLink } from "gatsby-plugin-anchor-links"
import styled from "@emotion/styled"
import Collapsible from "react-collapsible"
import Select from "./select"
const convertToKebabCase = string => {
return string.replace(/\s+/g, "-").toLowerCase()
}
const StyledNavItem = styled(Flex)`
padding-left: 16px;
padding-right: 10px;
align-items: center;
border-radius: 5pt;
cursor: pointer;
margin-bottom: 5px;
height: 25px;
&:hover {
background-color: #e0e0e059;
}
`
const StyledAnchorLink = styled(AnchorLink)`
display: flex;
margin-left: 10px;
padding-left: 10px;
padding-right: 10px;
align-items: center;
border-radius: 5pt;
cursor: pointer;
margin-bottom: 5px;
text-decoration: none;
align-items: center;
color: black;
height: 25px;
[fill*="red"] {
fill: #454545;
}
&:hover {
${props =>
!props.active &&
`
background-color: #e0e0e059;
`}
}
&.active {
background-color: #e0e0e0;
}
`
const SideBarContainer = styled(Flex)`
position: sticky;
top: 0;
height: 100vh;
overflow-y: scroll;
background-color: #f0f0f0;
min-width: 250px;
flex-direction: column;
`
const SideBar = ({ tags }) => {
const [api, setApi] = useState("store")
useEffect(() => {
const pathname = window.location.pathname
const matches = pathname.match(/api\/(store|admin)/)
if (pathname.length > 1) {
setApi(matches[1])
}
}, [])
return (
<SideBarContainer>
<Flex
width="100%"
alignContent="center"
justifyContent={"center"}
sx={{ borderBottom: "hairline" }}
px={4}
py={3}
mb={3}
flexDirection="column"
>
<Flex width={"100%"} alignContent="center">
<Text
fontFamily="Medusa"
fontSize="26px"
color="#454b54"
fontWeight={300}
>
medusa
</Text>
</Flex>
<Flex pt={3} justifyContent="space-between">
<Select
value={api}
onChange={e => navigate(`/api/${e.target.value}`)}
options={[
{ value: "admin", label: "Admin" },
{ value: "store", label: "Storefront" },
]}
/>
</Flex>
</Flex>
{Object.entries(tags).map(([tag, details]) => {
return (
<Box pt={1} px={3}>
<Collapsible
transitionTime={50}
trigger={<StyledNavItem fontSize={1}>{tag}</StyledNavItem>}
>
{details.map(e => (
<StyledAnchorLink to={`#${convertToKebabCase(e.summary)}`}>
<Text fontSize={0}>{e.summary}</Text>
</StyledAnchorLink>
))}
</Collapsible>
</Box>
)
})}
</SideBarContainer>
)
}
export default SideBar

View File

@@ -1,54 +0,0 @@
import { css } from "@emotion/core"
const Largest = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 22px;
font-weight: 300;
line-height: 1.5;
letter-spacing: normal;
`
const Large = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 18px;
font-weight: 300;
line-height: 1.22;
letter-spacing: -0.5px;
`
const Medium = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 16px;
font-weight: 300;
line-height: 1.22;
letter-spacing: normal;
`
const Base = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 14px;
font-weight: 300;
line-height: 1.22;
letter-spacing: -0.25px;
`
const Small = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 12px;
font-weight: 300;
line-height: 12px;
letter-spacing: 0px;
`
export default {
Largest,
Large,
Medium,
Base,
Small,
}

View File

@@ -1,38 +0,0 @@
const useSpec = raw => {
let tags = {}
for (const [path, methods] of Object.entries(raw.paths)) {
for (const [method, specification] of Object.entries(methods)) {
for (const t of specification.tags) {
if (t in tags) {
tags = {
...tags,
[t]: [
...tags[t],
{
method: method.toUpperCase(),
path,
...specification,
},
],
}
} else {
tags = {
...tags,
[t]: [
{
method: method.toUpperCase(),
path,
...specification,
},
],
}
}
}
}
}
return { tags, spec: raw }
}
export default useSpec

View File

@@ -1,27 +0,0 @@
import React from "react"
import { Flex } from "rebass"
import { Helmet } from "react-helmet"
import rawSpec from "../../../../docs/api/admin-spec3.json"
import Layout from "../../components/layout"
import SideBar from "../../components/sidebar"
import DocsReader from "../../components/docs-reader"
import useSpec from "../../hooks/use-spec"
export default function Home() {
const { tags, spec } = useSpec(rawSpec)
return (
<Layout>
<Helmet>
<title>Admin API Docs | Medusa Commerce</title>
</Helmet>
<Flex>
<SideBar tags={tags} />
<DocsReader tags={tags} spec={spec} />
</Flex>
</Layout>
)
}

View File

@@ -1,27 +0,0 @@
import React from "react"
import { Flex } from "rebass"
import { Helmet } from "react-helmet"
import rawSpec from "../../../../docs/api/store-spec3.json"
import Layout from "../../components/layout"
import SideBar from "../../components/sidebar"
import DocsReader from "../../components/docs-reader"
import useSpec from "../../hooks/use-spec"
export default function Home() {
const { tags, spec } = useSpec(rawSpec)
return (
<Layout>
<Helmet>
<title>Storefront API Docs | Medusa Commerce</title>
</Helmet>
<Flex>
<SideBar tags={tags} />
<DocsReader tags={tags} spec={spec} />
</Flex>
</Layout>
)
}

View File

@@ -1,27 +0,0 @@
import React from "react"
import { Flex } from "rebass"
import { Helmet } from "react-helmet"
import rawSpec from "../../../docs/api/admin-spec3.json"
import Layout from "../components/layout"
import SideBar from "../components/sidebar"
import DocsReader from "../components/docs-reader"
import useSpec from "../hooks/use-spec"
export default function Home() {
const { tags, spec } = useSpec(rawSpec)
return (
<Layout>
<Helmet>
<title>API Docs | Medusa Commerce</title>
</Helmet>
<Flex>
<SideBar tags={tags} />
<DocsReader tags={tags} spec={spec} />
</Flex>
</Layout>
)
}

View File

@@ -1 +0,0 @@
export default ["40em", "52em", "64em"]

View File

@@ -1 +0,0 @@
export default [0, 4, 8, 16, 32, 64]

View File

@@ -1,9 +0,0 @@
export const deref = (path, spec) => {
let cleanDets = spec
for (const part of path) {
if (part === "#") continue
cleanDets = cleanDets[part]
}
return cleanDets
}

5998
yarn.lock

File diff suppressed because it is too large Load Diff