api-ref: custom API reference (#4770)
* initialized next.js project * finished markdown sections * added operation schema component * change page metadata * eslint fixes * fixes related to deployment * added response schema * resolve max stack issue * support for different property types * added support for property types * added loading for components * added more loading * type fixes * added oneOf type * removed console * fix replace with push * refactored everything * use static content for description * fixes and improvements * added code examples section * fix path name * optimizations * fixed tag navigation * add support for admin and store references * general enhancements * optimizations and fixes * fixes and enhancements * added search bar * loading enhancements * added loading * added code blocks * added margin top * add empty response text * fixed oneOf parameters * added path and query parameters * general fixes * added base path env variable * small fix for arrays * enhancements * design enhancements * general enhancements * fix isRequired * added enum values * enhancements * general fixes * general fixes * changed oas generation script * additions to the introduction section * added copy button for code + other enhancements * fix response code block * fix metadata * formatted store introduction * move sidebar logic to Tags component * added test env variables * fix code block bug * added loading animation * added expand param + loading * enhance operation loading * made responsive + improvements * added loading provider * fixed loading * adjustments for small devices * added sidebar label for endpoints * added feedback component * fixed analytics * general fixes * listen to scroll for other headings * added sample env file * update api ref files + support new fields * fix for external docs link * added new sections * fix last item in sidebar not showing * move docs content to www/docs * change redirect url * revert change * resolve build errors * configure rewrites * changed to environment variable url * revert changing environment variable name * add environment variable for API path * fix links * fix tailwind settings * remove vercel file * reconfigured api route * move api page under api * fix page metadata * fix external link in navigation bar * update api spec * updated api specs * fixed google lint error * add max-height on request samples * add padding before loading * fix for one of name * fix undefined types * general fixes * remove response schema example * redesigned navigation bar * redesigned sidebar * fixed up paddings * added feedback component + report issue * fixed up typography, padding, and general styling * redesigned code blocks * optimization * added error timeout * fixes * added indexing with algolia + fixes * fix errors with algolia script * redesign operation sections * fix heading scroll * design fixes * fix padding * fix padding + scroll issues * fix scroll issues * improve scroll performance * fixes for safari * optimization and fixes * fixes to docs + details animation * padding fixes for code block * added tab animation * fixed incorrect link * added selection styling * fix lint errors * redesigned details component * added detailed feedback form * api reference fixes * fix tabs * upgrade + fixes * updated documentation links * optimizations to sidebar items * fix spacing in sidebar item * optimizations and fixes * fix endpoint path styling * remove margin * final fixes * change margin on small devices * generated OAS * fixes for mobile * added feedback modal * optimize dark mode button * fixed color mode useeffect * minimize dom size * use new style system * radius and spacing design system * design fixes * fix eslint errors * added meta files * change cron schedule * fix docusaurus configurations * added operating system to feedback data * change content directory name * fixes to contribution guidelines * revert renaming content * added api-reference to documentation workflow * fixes for search * added dark mode + fixes * oas fixes * handle bugs * added code examples for clients * changed tooltip text * change authentication to card * change page title based on selected section * redesigned mobile navbar * fix icon colors * fix key colors * fix medusa-js installation command * change external regex in algolia * change changeset * fix padding on mobile * fix hydration error * update depedencies
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
NEXT_PUBLIC_SEGMENT_API_KEY=
|
||||
NEXT_PUBLIC_BASE_PATH=
|
||||
NEXT_PUBLIC_ALGOLIA_INDEX_NAME=
|
||||
NEXT_PUBLIC_ALGOLIA_API_KEY=
|
||||
NEXT_PUBLIC_ALGOLIA_APP_ID=
|
||||
NEXT_PUBLIC_ENV=
|
||||
NEXT_PUBLIC_BASE_URL=
|
||||
NEXT_PUBLIC_DOCS_URL=
|
||||
ALGOLIA_WRITE_API_KEY=
|
||||
@@ -0,0 +1,4 @@
|
||||
next.config.js
|
||||
specs
|
||||
node_modules
|
||||
public
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"../.eslintrc.js"
|
||||
],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/prefer-ts-expect-error": "off"
|
||||
},
|
||||
"settings": {
|
||||
"next": {
|
||||
"rootDir": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env.test
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# unnecessary specs
|
||||
specs/admin.oas.json
|
||||
specs/store.oas.json
|
||||
@@ -0,0 +1 @@
|
||||
nmMode: hardlinks-local
|
||||
@@ -0,0 +1,34 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
@@ -0,0 +1,21 @@
|
||||
import Feedback from "@/components/Feedback"
|
||||
|
||||
This API reference includes Medusa's Admin APIs, which are REST APIs exposed by the Medusa backend. They are typically used to perform admin functionalities or create an admin dashboard to access and manipulate your commerce store's data.
|
||||
|
||||
All endpoints are prefixed with `/admin`. So, during development, the endpoints will be available under the path `http://localhost:9000/admin`. For production, replace `http://localhost:9000` with your Medusa backend URL.
|
||||
|
||||
There are different ways you can send requests to these endpoints, including:
|
||||
|
||||
- Using Medusa's [JavaScript Client](https://docs.medusajs.com/js-client/overview)
|
||||
- Using the [Medusa React](https://docs.medusajs.com/medusa-react/overview) library
|
||||
- Using cURL
|
||||
|
||||
Aside from this API reference, check out the [Commerce Modules](https://docs.medusajs.com/modules/overview) section of the documentation for guides on how to use these APIs in different scenarios.
|
||||
|
||||
<Feedback
|
||||
event="survey_api-ref"
|
||||
extraData={{
|
||||
area: "admin",
|
||||
section: "introduction"
|
||||
}}
|
||||
/>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
import CodeTabs from "@/components/CodeTabs"
|
||||
import Space from "@/components/Space"
|
||||
|
||||
### Just Getting Started?
|
||||
|
||||
Check out the [quickstart guide](https://docs.medusajs.com/create-medusa-app).
|
||||
|
||||
<Space bottom={8} />
|
||||
|
||||
### Client Libraries
|
||||
|
||||
<CodeTabs
|
||||
tabs={[
|
||||
{
|
||||
label: 'Medusa JS Client',
|
||||
value: 'js-client',
|
||||
code: {
|
||||
source: `npm install @medusajs/medusa-js`,
|
||||
lang: `bash`,
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Medusa React',
|
||||
value: 'medusa-react',
|
||||
code: {
|
||||
source: `npm install medusa-react @tanstack/react-query @medusajs/medusa`,
|
||||
lang: `bash`,
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -0,0 +1,21 @@
|
||||
import Feedback from "@/components/Feedback"
|
||||
|
||||
This API reference includes Medusa's Store APIs, which are REST APIs exposed by the Medusa backend. They are typically used to create a storefront for your commerce store, such as a webshop or a commerce mobile app.
|
||||
|
||||
All endpoints are prefixed with `/store`. So, during development, the endpoints will be available under the path `http://localhost:9000/store`. For production, replace `http://localhost:9000` with your Medusa backend URL.
|
||||
|
||||
There are different ways you can send requests to these endpoints, including:
|
||||
|
||||
- Using Medusa's [JavaScript Client](https://docs.medusajs.com/js-client/overview)
|
||||
- Using the [Medusa React](https://docs.medusajs.com/medusa-react/overview) library
|
||||
- Using cURL
|
||||
|
||||
Aside from this API reference, check out the [Commerce Modules](https://docs.medusajs.com/modules/overview) section of the documentation for guides on how to use these APIs in different scenarios.
|
||||
|
||||
<Feedback
|
||||
event="survey_api-ref"
|
||||
extraData={{
|
||||
area: "store",
|
||||
section: "introduction"
|
||||
}}
|
||||
/>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
import SidebarProvider from "@/providers/sidebar"
|
||||
import Sidebar from "@/components/Sidebar"
|
||||
import clsx from "clsx"
|
||||
import "../../../css/globals.css"
|
||||
import BaseSpecsProvider from "@/providers/base-specs"
|
||||
import Navbar from "@/components/Navbar"
|
||||
import ColorModeProvider from "@/providers/color-mode"
|
||||
import { Inter } from "next/font/google"
|
||||
import { Roboto_Mono } from "next/font/google"
|
||||
import AnalyticsProvider from "@/providers/analytics"
|
||||
import NavbarProvider from "@/providers/navbar"
|
||||
import ModalProvider from "../../../providers/modal"
|
||||
|
||||
export const metadata = {
|
||||
title: "Medusa API Reference",
|
||||
description: "Check out Medusa's API reference",
|
||||
}
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
weight: ["400", "500"],
|
||||
})
|
||||
|
||||
const robotoMono = Roboto_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-roboto-mono",
|
||||
})
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={clsx("h-full w-full")}>
|
||||
<body
|
||||
className={clsx(
|
||||
inter.variable,
|
||||
robotoMono.variable,
|
||||
"bg-docs-bg dark:bg-docs-bg-dark font-base text-medium h-full w-full",
|
||||
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark"
|
||||
)}
|
||||
>
|
||||
<AnalyticsProvider>
|
||||
<ModalProvider>
|
||||
<ColorModeProvider>
|
||||
<BaseSpecsProvider>
|
||||
<SidebarProvider>
|
||||
<NavbarProvider>
|
||||
<div className="w-full">
|
||||
<Navbar />
|
||||
<div className="max-w-xxl mx-auto flex w-full px-1.5">
|
||||
<Sidebar />
|
||||
<main className="lg:w-api-ref-main relative mt-4 w-full flex-1 lg:mt-7">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</NavbarProvider>
|
||||
</SidebarProvider>
|
||||
</BaseSpecsProvider>
|
||||
</ColorModeProvider>
|
||||
</ModalProvider>
|
||||
</AnalyticsProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
@@ -0,0 +1,64 @@
|
||||
import AreaProvider from "@/providers/area"
|
||||
import AdminDescription from "../../_mdx/admin.mdx"
|
||||
import StoreDescription from "../../_mdx/store.mdx"
|
||||
import ClientLibraries from "../../_mdx/client-libraries.mdx"
|
||||
import Section from "@/components/Section"
|
||||
import Tags from "@/components/Tags"
|
||||
import type { Area } from "@/types/openapi"
|
||||
import DividedLayout from "@/layouts/Divided"
|
||||
import capitalize from "@/utils/capitalize"
|
||||
import PageTitleProvider from "../../../providers/page-title"
|
||||
|
||||
type ReferencePageProps = {
|
||||
params: {
|
||||
area: Area
|
||||
}
|
||||
}
|
||||
|
||||
const ReferencePage = async ({ params: { area } }: ReferencePageProps) => {
|
||||
return (
|
||||
<AreaProvider area={area}>
|
||||
<PageTitleProvider>
|
||||
<h1 className="!text-h2 block lg:hidden">
|
||||
Medusa {capitalize(area)} API Reference
|
||||
</h1>
|
||||
<DividedLayout
|
||||
mainContent={
|
||||
<Section>
|
||||
<h1 className="!text-h2 hidden lg:block">
|
||||
Medusa {capitalize(area)} API Reference
|
||||
</h1>
|
||||
{area.includes("admin") && <AdminDescription />}
|
||||
{area.includes("store") && <StoreDescription />}
|
||||
</Section>
|
||||
}
|
||||
codeContent={<ClientLibraries />}
|
||||
className="flex-col-reverse"
|
||||
/>
|
||||
<Tags />
|
||||
</PageTitleProvider>
|
||||
</AreaProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReferencePage
|
||||
|
||||
export function generateMetadata({ params: { area } }: ReferencePageProps) {
|
||||
return {
|
||||
title: `Medusa ${capitalize(area)} API Reference`,
|
||||
description: `REST API reference for the Medusa ${area} API. This reference includes code snippets and examples for Medusa JS Client and cURL.`,
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return [
|
||||
{
|
||||
area: "admin",
|
||||
},
|
||||
{
|
||||
area: "store",
|
||||
},
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
@@ -0,0 +1,150 @@
|
||||
import OpenAPIParser from "@readme/openapi-parser"
|
||||
import algoliasearch from "algoliasearch"
|
||||
import type { ExpandedDocument, Operation } from "../../../types/openapi"
|
||||
import path from "path"
|
||||
import getPathsOfTag from "../../../utils/get-paths-of-tag"
|
||||
import getSectionId from "../../../utils/get-section-id"
|
||||
import { NextResponse } from "next/server"
|
||||
import { JSDOM } from "jsdom"
|
||||
import capitalize from "../../../utils/capitalize"
|
||||
import getUrl from "../../../utils/get-url"
|
||||
|
||||
export async function GET() {
|
||||
const algoliaClient = algoliasearch(
|
||||
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "",
|
||||
process.env.ALGOLIA_WRITE_API_KEY || ""
|
||||
)
|
||||
const index = algoliaClient.initIndex(
|
||||
process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME || ""
|
||||
)
|
||||
|
||||
// retrieve tags and their operations to index them
|
||||
const indices: Record<string, any>[] = []
|
||||
for (const area of ["store", "admin"]) {
|
||||
const defaultIndexData = {
|
||||
version: ["current"],
|
||||
lang: "en",
|
||||
_tags: ["api", area],
|
||||
}
|
||||
// find and parse static headers from pages
|
||||
const dom = await JSDOM.fromURL(getUrl(area))
|
||||
const headers = dom.window.document.querySelectorAll("h2")
|
||||
headers.forEach((header) => {
|
||||
if (!header.textContent) {
|
||||
return
|
||||
}
|
||||
|
||||
const objectID = getSectionId([header.textContent])
|
||||
const url = getUrl(area, objectID)
|
||||
indices.push({
|
||||
objectID: getObjectId(area, `${objectID}-mdx-section`),
|
||||
hierarchy: getHierarchy(area, [header.textContent]),
|
||||
type: `content`,
|
||||
content: header.textContent,
|
||||
url,
|
||||
url_without_variables: url,
|
||||
url_without_anchor: url,
|
||||
...defaultIndexData,
|
||||
})
|
||||
})
|
||||
|
||||
// find and index tag and operations
|
||||
const baseSpecs = (await OpenAPIParser.parse(
|
||||
path.join(process.cwd(), `specs/${area}/openapi.yaml`)
|
||||
)) as ExpandedDocument
|
||||
|
||||
await Promise.all(
|
||||
baseSpecs.tags?.map(async (tag) => {
|
||||
const tagName = getSectionId([tag.name])
|
||||
const url = getUrl(area, tagName)
|
||||
indices.push({
|
||||
objectID: getObjectId(area, tagName),
|
||||
hierarchy: getHierarchy(area, [tag.name]),
|
||||
type: "lvl1",
|
||||
content: null,
|
||||
url,
|
||||
url_without_variables: url,
|
||||
url_without_anchor: url,
|
||||
...defaultIndexData,
|
||||
})
|
||||
const paths = await getPathsOfTag(tagName, area)
|
||||
|
||||
Object.values(paths.paths).forEach((path) => {
|
||||
Object.values(path).forEach((op) => {
|
||||
const operation = op as Operation
|
||||
const operationName = getSectionId([
|
||||
tag.name,
|
||||
operation.operationId,
|
||||
])
|
||||
const url = getUrl(area, operationName)
|
||||
indices.push({
|
||||
objectID: getObjectId(area, operationName),
|
||||
hierarchy: getHierarchy(area, [tag.name, operation.summary]),
|
||||
type: "content",
|
||||
content: operation.summary,
|
||||
content_camel: operation.summary,
|
||||
url,
|
||||
url_without_variables: url,
|
||||
url_without_anchor: url,
|
||||
...defaultIndexData,
|
||||
})
|
||||
|
||||
// index its description
|
||||
const operationDescriptionId = getSectionId([
|
||||
tag.name,
|
||||
operation.operationId,
|
||||
operation.description.substring(
|
||||
0,
|
||||
Math.min(20, operation.description.length)
|
||||
),
|
||||
])
|
||||
|
||||
indices.push({
|
||||
objectID: getObjectId(area, operationDescriptionId),
|
||||
hierarchy: getHierarchy(area, [
|
||||
tag.name,
|
||||
operation.summary,
|
||||
operation.description,
|
||||
]),
|
||||
type: "content",
|
||||
content: operation.description,
|
||||
content_camel: operation.description,
|
||||
url,
|
||||
url_without_variables: url,
|
||||
url_without_anchor: url,
|
||||
...defaultIndexData,
|
||||
})
|
||||
})
|
||||
})
|
||||
}) || []
|
||||
)
|
||||
}
|
||||
|
||||
if (indices.length) {
|
||||
await index.saveObjects(indices, {
|
||||
autoGenerateObjectIDIfNotExist: true,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: "done",
|
||||
})
|
||||
}
|
||||
|
||||
function getObjectId(area: string, objectName: string): string {
|
||||
return `${area}_${objectName}`
|
||||
}
|
||||
|
||||
function getHierarchy(area: string, levels: string[]): Record<string, string> {
|
||||
const heirarchy: Record<string, string> = {
|
||||
lvl0: `${capitalize(area)} API Reference`,
|
||||
}
|
||||
|
||||
let counter = 1
|
||||
levels.forEach((level) => {
|
||||
heirarchy[`lvl${counter}`] = level
|
||||
counter++
|
||||
})
|
||||
|
||||
return heirarchy
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import path from "path"
|
||||
import OpenAPIParser from "@readme/openapi-parser"
|
||||
import getPathsOfTag from "@/utils/get-paths-of-tag"
|
||||
import type { ExpandedDocument } from "@/types/openapi"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const area = searchParams.get("area")
|
||||
const expand = searchParams.get("expand")
|
||||
if (area !== "admin" && area !== "store") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: `area ${area} is not allowed`,
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
)
|
||||
}
|
||||
const baseSpecs = (await OpenAPIParser.parse(
|
||||
path.join(process.cwd(), `specs/${area}/openapi.yaml`)
|
||||
)) as ExpandedDocument
|
||||
|
||||
if (expand) {
|
||||
const paths = await getPathsOfTag(expand, area)
|
||||
if (paths) {
|
||||
baseSpecs.expandedTags = {}
|
||||
baseSpecs.expandedTags[expand] = paths.paths
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(baseSpecs, {
|
||||
status: 200,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import path from "path"
|
||||
import getPathsOfTag from "@/utils/get-paths-of-tag"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const tagName = searchParams.get("tagName") || ""
|
||||
const area = searchParams.get("area")
|
||||
|
||||
if (area !== "admin" && area !== "store") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: `area ${area} is not allowed`,
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// this is just to ensure that vercel picks up these files on build
|
||||
path.join(process.cwd(), "specs/admin/code_samples")
|
||||
path.join(process.cwd(), "specs/admin/components")
|
||||
path.join(process.cwd(), "specs/admin/paths")
|
||||
path.join(process.cwd(), "specs/store/code_samples")
|
||||
path.join(process.cwd(), "specs/store/components")
|
||||
path.join(process.cwd(), "specs/store/paths")
|
||||
|
||||
// get path files
|
||||
const paths = await getPathsOfTag(tagName, area)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
paths: paths.paths,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { MetadataRoute } from "next"
|
||||
import OpenAPIParser from "@readme/openapi-parser"
|
||||
import path from "path"
|
||||
import getBaseUrl from "../../utils/get-base-url"
|
||||
import type { ExpandedDocument, Operation } from "../../types/openapi"
|
||||
import getUrl from "../../utils/get-url"
|
||||
import getSectionId from "../../utils/get-section-id"
|
||||
import getPathsOfTag from "../../utils/get-paths-of-tag"
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
const results = [
|
||||
{
|
||||
url: `${baseUrl}/api/admin`,
|
||||
lastModified: new Date(),
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/api/store`,
|
||||
lastModified: new Date(),
|
||||
},
|
||||
]
|
||||
|
||||
for (const area of ["store", "admin"]) {
|
||||
const baseSpecs = (await OpenAPIParser.parse(
|
||||
path.join(process.cwd(), `specs/${area}/openapi.yaml`)
|
||||
)) as ExpandedDocument
|
||||
|
||||
await Promise.all(
|
||||
baseSpecs.tags?.map(async (tag) => {
|
||||
const tagName = getSectionId([tag.name])
|
||||
const url = getUrl(area, tagName)
|
||||
results.push({
|
||||
url,
|
||||
lastModified: new Date(),
|
||||
})
|
||||
|
||||
const paths = await getPathsOfTag(tagName, area)
|
||||
|
||||
Object.values(paths.paths).forEach((path) => {
|
||||
Object.values(path).forEach((op) => {
|
||||
const operation = op as Operation
|
||||
const operationName = getSectionId([
|
||||
tag.name,
|
||||
operation.operationId,
|
||||
])
|
||||
const url = getUrl(area, operationName)
|
||||
results.push({
|
||||
url,
|
||||
lastModified: new Date(),
|
||||
})
|
||||
})
|
||||
})
|
||||
}) || []
|
||||
)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
@@ -0,0 +1,4 @@
|
||||
User-Agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://docs.medusajs.com/sitemap.xml
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap>
|
||||
<loc>https://docs.medusajs.com/assets/sitemap.xml</loc>
|
||||
</sitemap>
|
||||
<sitemap>
|
||||
<loc>https://docs.medusajs.com/sitemap-docs.xml</loc>
|
||||
</sitemap>
|
||||
</sitemapindex>
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
|
||||
export type BadgeProps = {
|
||||
className?: string
|
||||
variant:
|
||||
| "purple"
|
||||
| "purple-dark"
|
||||
| "orange"
|
||||
| "orange-dark"
|
||||
| "green"
|
||||
| "green-dark"
|
||||
| "blue"
|
||||
| "blue-dark"
|
||||
| "red"
|
||||
} & React.HTMLAttributes<HTMLSpanElement>
|
||||
|
||||
const Badge: React.FC<BadgeProps> = ({ className, variant, children }) => {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"text-compact-x-small-plus px-0.4 rounded-sm border border-solid py-px text-center",
|
||||
variant === "purple" &&
|
||||
"bg-medusa-tag-purple-bg dark:bg-medusa-tag-purple-bg-dark text-medusa-tag-purple-text dark:text-medusa-tag-purple-text-dark border-medusa-tag-purple-border dark:border-medusa-tag-purple-border-dark",
|
||||
variant === "purple-dark" &&
|
||||
"bg-medusa-tag-purple-bg-dark text-medusa-tag-purple-text-dark border-medusa-tag-purple-border-dark",
|
||||
variant === "orange" &&
|
||||
"bg-medusa-tag-orange-bg dark:bg-medusa-tag-orange-bg-dark text-medusa-tag-orange-text dark:text-medusa-tag-orange-text-dark border-medusa-tag-orange-border dark:border-medusa-tag-orange-border-dark",
|
||||
variant === "orange-dark" &&
|
||||
"bg-medusa-tag-orange-bg-dark text-medusa-tag-orange-text-dark border-medusa-tag-orange-border-dark",
|
||||
variant === "green" &&
|
||||
"bg-medusa-tag-green-bg dark:bg-medusa-tag-green-bg-dark text-medusa-tag-green-text dark:text-medusa-tag-green-text-dark border-medusa-tag-green-border dark:border-medusa-tag-green-border-dark",
|
||||
variant === "green-dark" &&
|
||||
"bg-medusa-tag-green-bg-dark text-medusa-tag-green-text-dark border-medusa-tag-green-border-dark",
|
||||
variant === "blue" &&
|
||||
"bg-medusa-tag-blue-bg dark:bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text dark:text-medusa-tag-blue-text-dark border-medusa-tag-blue-border dark:border-medusa-tag-blue-border-dark",
|
||||
variant === "blue-dark" &&
|
||||
"bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text-dark border-medusa-tag-blue-border-dark",
|
||||
variant === "red" &&
|
||||
"bg-medusa-tag-red-bg dark:bg-medusa-tag-red-bg-dark text-medusa-tag-red-text dark:text-medusa-tag-red-text-dark border-medusa-tag-red-border dark:border-medusa-tag-red-border-dark",
|
||||
"badge",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default Badge
|
||||
@@ -0,0 +1,33 @@
|
||||
import clsx from "clsx"
|
||||
|
||||
export type ButtonProps = {
|
||||
isSelected?: boolean
|
||||
disabled?: boolean
|
||||
variant?: "primary" | "secondary"
|
||||
darkVariant?: "primary" | "secondary"
|
||||
} & React.HTMLAttributes<HTMLButtonElement>
|
||||
|
||||
const Button = ({
|
||||
className,
|
||||
children,
|
||||
variant = "primary",
|
||||
darkVariant,
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
variant === "primary" && "btn-primary",
|
||||
variant === "secondary" && "btn-secondary",
|
||||
darkVariant && darkVariant === "primary" && "dark:btn-primary",
|
||||
darkVariant && darkVariant === "secondary" && "dark:btn-secondary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,43 @@
|
||||
import clsx from "clsx"
|
||||
import Link from "next/link"
|
||||
import IconArrowUpRightOnBox from "../Icons/ArrowUpRightOnBox"
|
||||
|
||||
type CardProps = {
|
||||
title: string
|
||||
text?: string
|
||||
href?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Card = ({ title, text, href, className }: CardProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-medusa-bg-subtle dark:bg-medusa-bg-subtle-dark w-full rounded",
|
||||
"shadow-card-rest dark:shadow-card-rest-dark py-0.75 relative px-1",
|
||||
"flex items-center justify-between gap-1 transition-shadow",
|
||||
href && "hover:shadow-card-hover dark:hover:shadow-card-hover-dark",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-compact-medium-plus text-medusa-fg-base dark:text-medusa-fg-base-dark">
|
||||
{title}
|
||||
</span>
|
||||
{text && <span className="text-compact-medium">{text}</span>}
|
||||
</div>
|
||||
|
||||
{href && (
|
||||
<>
|
||||
<IconArrowUpRightOnBox />
|
||||
<Link
|
||||
href={href}
|
||||
className="absolute left-0 top-0 h-full w-full rounded"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import clsx from "clsx"
|
||||
import { Highlight, HighlightProps, themes } from "prism-react-renderer"
|
||||
import CopyButton from "../CopyButton"
|
||||
import IconCopy from "../Icons/Copy"
|
||||
import { useColorMode } from "../../providers/color-mode"
|
||||
|
||||
export type CodeBlockProps = {
|
||||
source: string
|
||||
lang?: string
|
||||
className?: string
|
||||
collapsed?: boolean
|
||||
} & Omit<HighlightProps, "code" | "language" | "children">
|
||||
|
||||
const CodeBlock = ({
|
||||
source,
|
||||
lang = "",
|
||||
className,
|
||||
collapsed = false,
|
||||
...rest
|
||||
}: CodeBlockProps) => {
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-medusa-code-bg-base dark:bg-medusa-code-bg-base-dark relative mb-1 rounded",
|
||||
"border-medusa-code-border w-full max-w-full border",
|
||||
"xs:after:content-[''] xs:after:rounded xs:after:absolute xs:after:right-0 xs:after:top-0 xs:after:w-[calc(10%+24px)] xs:after:h-full xs:after:bg-code-fade xs:dark:after:bg-code-fade-dark",
|
||||
collapsed && "max-h-[400px] overflow-auto",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Highlight
|
||||
theme={{
|
||||
...themes.vsDark,
|
||||
plain: {
|
||||
...themes.vsDark.plain,
|
||||
backgroundColor: colorMode === "light" ? "#111827" : "#1C1C1F",
|
||||
},
|
||||
}}
|
||||
code={source.trim()}
|
||||
language={lang}
|
||||
{...rest}
|
||||
>
|
||||
{({
|
||||
className: preClassName,
|
||||
style,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<>
|
||||
<pre
|
||||
style={{ ...style, fontStretch: "100%" }}
|
||||
className={clsx(
|
||||
"xs:max-w-[90%] relative !mt-0 break-words bg-transparent !outline-none",
|
||||
"overflow-auto break-words rounded",
|
||||
preClassName
|
||||
)}
|
||||
>
|
||||
<code
|
||||
className={clsx(
|
||||
"text-code-body font-monospace table min-w-full pb-1.5 print:whitespace-pre-wrap",
|
||||
tokens.length > 1 && "pt-1 pr-1",
|
||||
tokens.length <= 1 && "py-0.5 px-1"
|
||||
)}
|
||||
>
|
||||
{tokens.map((line, i) => {
|
||||
const lineProps = getLineProps({ line })
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
{...lineProps}
|
||||
className={clsx("table-row", lineProps.className)}
|
||||
>
|
||||
{tokens.length > 1 && (
|
||||
<span
|
||||
className={clsx(
|
||||
"text-medusa-fg-subtle mr-1 table-cell select-none",
|
||||
"bg-medusa-code-bg-base dark:bg-medusa-code-bg-base-dark sticky left-0 w-[1%] px-1 text-right"
|
||||
)}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</code>
|
||||
</pre>
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute z-50 hidden gap-1 md:flex",
|
||||
tokens.length === 1 && "right-0.75 top-[10px]",
|
||||
tokens.length > 1 && "right-1 top-1"
|
||||
)}
|
||||
>
|
||||
<CopyButton text={source} tooltipClassName="font-base">
|
||||
<IconCopy className="fill-medusa-code-icon dark:fill-medusa-code-icon-dark" />
|
||||
</CopyButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodeBlock
|
||||
@@ -0,0 +1,117 @@
|
||||
"use client"
|
||||
|
||||
import clsx from "clsx"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import CodeBlock, { CodeBlockProps } from "../CodeBlock"
|
||||
|
||||
type TabType = {
|
||||
label: string
|
||||
value: string
|
||||
code?: CodeBlockProps
|
||||
codeBlock?: React.ReactNode
|
||||
}
|
||||
|
||||
type CodeTabsProps = {
|
||||
tabs: TabType[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CodeTabs = ({ tabs, className }: CodeTabsProps) => {
|
||||
const [selectedTab, setSelectedTab] = useState(tabs[0])
|
||||
const tabRefs: (HTMLButtonElement | null)[] = useMemo(() => [], [])
|
||||
const codeTabSelectorRef = useRef<HTMLSpanElement | null>(null)
|
||||
const codeTabsWrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const changeTabSelectorCoordinates = useCallback(
|
||||
(selectedTabElm: HTMLElement) => {
|
||||
if (!codeTabSelectorRef?.current || !codeTabsWrapperRef?.current) {
|
||||
return
|
||||
}
|
||||
const selectedTabsCoordinates = selectedTabElm.getBoundingClientRect()
|
||||
const tabsWrapperCoordinates =
|
||||
codeTabsWrapperRef.current.getBoundingClientRect()
|
||||
codeTabSelectorRef.current.style.left = `${
|
||||
selectedTabsCoordinates.left - tabsWrapperCoordinates.left
|
||||
}px`
|
||||
codeTabSelectorRef.current.style.width = `${selectedTabsCoordinates.width}px`
|
||||
codeTabSelectorRef.current.style.height = `${selectedTabsCoordinates.height}px`
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (codeTabSelectorRef?.current && tabRefs.length) {
|
||||
const selectedTabElm = tabRefs.find(
|
||||
(tab) => tab?.getAttribute("aria-selected") === "true"
|
||||
)
|
||||
if (selectedTabElm) {
|
||||
changeTabSelectorCoordinates(
|
||||
selectedTabElm.parentElement || selectedTabElm
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [codeTabSelectorRef, tabRefs, changeTabSelectorCoordinates, selectedTab])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"relative my-1 w-full max-w-full overflow-auto",
|
||||
className
|
||||
)}
|
||||
ref={codeTabsWrapperRef}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
"xs:absolute xs:border xs:border-solid xs:border-medusa-code-border dark:xs:border-medusa-code-border-dark xs:bg-medusa-code-bg-base dark:xs:bg-medusa-code-bg-base-dark",
|
||||
"xs:transition-all xs:duration-200 xs:ease-ease xs:top-[13px] xs:z-[1] xs:rounded-full"
|
||||
)}
|
||||
ref={codeTabSelectorRef}
|
||||
></span>
|
||||
<ul
|
||||
className={clsx(
|
||||
"bg-medusa-code-bg-header dark:bg-medusa-code-bg-header-dark py-0.75 flex !list-none rounded-t px-1",
|
||||
"border-medusa-code-border dark:border-medusa-code-border-dark border border-b-0 border-transparent",
|
||||
"gap-0.25 mb-0"
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={clsx(
|
||||
"text-compact-small-plus xs:border-0 py-0.25 px-0.75 relative z-[2] rounded-full border",
|
||||
selectedTab.value !== tab.value &&
|
||||
"text-medusa-code-text-subtle dark:text-medusa-code-text-subtle-dark border-transparent",
|
||||
selectedTab.value === tab.value &&
|
||||
"text-medusa-code-text-base dark:text-medusa-code-text-base-dark bg-medusa-code-bg-base dark:bg-medusa-code-bg-base-dark xs:!bg-transparent",
|
||||
selectedTab.value !== tab.value &&
|
||||
"hover:bg-medusa-code-bg-base dark:hover:bg-medusa-code-bg-base-dark"
|
||||
)}
|
||||
ref={(tabControl) => tabRefs.push(tabControl)}
|
||||
onClick={() => {
|
||||
setSelectedTab(tab)
|
||||
}}
|
||||
aria-selected={selectedTab.value === tab.value}
|
||||
role="tab"
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<>
|
||||
{selectedTab.code && (
|
||||
<CodeBlock
|
||||
{...selectedTab.code}
|
||||
className={clsx(
|
||||
"!mt-0 !rounded-t-none",
|
||||
selectedTab.code.className
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedTab.codeBlock && <>{selectedTab.codeBlock}</>}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodeTabs
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react"
|
||||
import clsx from "clsx"
|
||||
import dynamic from "next/dynamic"
|
||||
import { TooltipProps } from "../Tooltip"
|
||||
import SpinnerLoading from "../Loading/Spinner"
|
||||
|
||||
const Tooltip = dynamic<TooltipProps>(async () => import("../Tooltip"), {
|
||||
loading: () => <SpinnerLoading />,
|
||||
}) as React.FC<TooltipProps>
|
||||
|
||||
export type CopyButtonProps = {
|
||||
text: string
|
||||
buttonClassName?: string
|
||||
tooltipClassName?: string
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const CopyButton = ({
|
||||
text,
|
||||
buttonClassName = "",
|
||||
tooltipClassName = "",
|
||||
children,
|
||||
}: CopyButtonProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const copyTimeout = useRef<number | undefined>(undefined)
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
const copy = (await import("copy-text-to-clipboard")).default
|
||||
copy(text)
|
||||
setIsCopied(true)
|
||||
copyTimeout.current = window.setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, 1000)
|
||||
}, [text])
|
||||
|
||||
useEffect(() => () => window.clearTimeout(copyTimeout.current), [])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
text={isCopied ? `Copied!` : `Copy to Clipboard`}
|
||||
tooltipClassName={tooltipClassName}
|
||||
>
|
||||
<span
|
||||
className={clsx("cursor-pointer", buttonClassName)}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyButton
|
||||
@@ -0,0 +1,24 @@
|
||||
"use server"
|
||||
|
||||
import type { OpenAPIV3 } from "openapi-types"
|
||||
import Section from "../Section"
|
||||
import MDXContentServer from "../MDXContent/Server"
|
||||
|
||||
export type DescriptionProps = {
|
||||
specs: OpenAPIV3.Document
|
||||
}
|
||||
|
||||
const Description = ({ specs }: DescriptionProps) => {
|
||||
return (
|
||||
<Section>
|
||||
<MDXContentServer
|
||||
content={specs.info.description}
|
||||
scope={{
|
||||
specs,
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Description
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useAnalytics } from "../../providers/analytics"
|
||||
import { useModal } from "../../providers/modal"
|
||||
import Label from "../Label"
|
||||
import TextArea from "../TextArea"
|
||||
import ModalFooter from "../Modal/Footer"
|
||||
|
||||
const DetailedFeedback = () => {
|
||||
const [improvementFeedback, setImprovementFeedback] = useState("")
|
||||
const [positiveFeedback, setPositiveFeedback] = useState("")
|
||||
const [additionalFeedback, setAdditionalFeedback] = useState("")
|
||||
const { loaded, track } = useAnalytics()
|
||||
const { closeModal } = useModal()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-1 overflow-auto py-1.5 px-2 lg:min-h-[400px]">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label>What should be improved in this API reference?</Label>
|
||||
<TextArea
|
||||
rows={4}
|
||||
value={improvementFeedback}
|
||||
onChange={(e) => setImprovementFeedback(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label>Is there a feature you like in this API reference?</Label>
|
||||
<TextArea
|
||||
rows={4}
|
||||
value={positiveFeedback}
|
||||
onChange={(e) => setPositiveFeedback(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Label>Do you have any additional notes or feedback?</Label>
|
||||
<TextArea
|
||||
rows={4}
|
||||
value={additionalFeedback}
|
||||
onChange={(e) => setAdditionalFeedback(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ModalFooter
|
||||
actions={[
|
||||
{
|
||||
children: "Save",
|
||||
onClick: (e) => {
|
||||
if (
|
||||
!loaded ||
|
||||
(!improvementFeedback &&
|
||||
!positiveFeedback &&
|
||||
!additionalFeedback)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const buttonElm = e.target as HTMLButtonElement
|
||||
// buttonElm.disabled = true
|
||||
buttonElm.classList.add("cursor-not-allowed")
|
||||
buttonElm.textContent = "Please wait"
|
||||
track(
|
||||
"api-ref-general-feedback",
|
||||
{
|
||||
feedbackData: {
|
||||
improvementFeedback,
|
||||
positiveFeedback,
|
||||
additionalFeedback,
|
||||
},
|
||||
},
|
||||
function () {
|
||||
buttonElm.textContent = "Thank you!"
|
||||
setTimeout(() => {
|
||||
closeModal()
|
||||
}, 1000)
|
||||
}
|
||||
)
|
||||
},
|
||||
variant: "primary",
|
||||
},
|
||||
]}
|
||||
className="mt-1"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DetailedFeedback
|
||||
@@ -0,0 +1,65 @@
|
||||
import clsx from "clsx"
|
||||
import IconPlusMini from "../../Icons/PlusMini"
|
||||
|
||||
type DetailsSummaryProps = {
|
||||
title: string
|
||||
subtitle?: string
|
||||
badge?: React.ReactNode
|
||||
expandable?: boolean
|
||||
open?: boolean
|
||||
className?: string
|
||||
titleClassName?: string
|
||||
} & React.HTMLAttributes<HTMLElement>
|
||||
|
||||
const DetailsSummary = ({
|
||||
title,
|
||||
subtitle,
|
||||
badge,
|
||||
expandable = true,
|
||||
open = false,
|
||||
className,
|
||||
titleClassName,
|
||||
...rest
|
||||
}: DetailsSummaryProps) => {
|
||||
return (
|
||||
<summary
|
||||
className={clsx(
|
||||
"py-0.75 flex items-center justify-between",
|
||||
expandable && "cursor-pointer",
|
||||
!expandable &&
|
||||
"border-medusa-border-base dark:border-medusa-border-base-dark border-y",
|
||||
"no-marker",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<span className="gap-0.25 flex flex-col">
|
||||
<span
|
||||
className={clsx(
|
||||
"text-compact-medium-plus text-medusa-fg-base dark:text-medusa-fg-base-dark",
|
||||
titleClassName
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
{subtitle && (
|
||||
<span className="text-compact-medium text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark">
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{(badge || expandable) && (
|
||||
<span className="flex gap-0.5">
|
||||
{badge}
|
||||
{expandable && (
|
||||
<IconPlusMini
|
||||
className={clsx("transition-transform", open && "rotate-45")}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</summary>
|
||||
)
|
||||
}
|
||||
|
||||
export default DetailsSummary
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Suspense, cloneElement, useRef, useState } from "react"
|
||||
import Loading from "../Loading"
|
||||
import clsx from "clsx"
|
||||
import { CSSTransition } from "react-transition-group"
|
||||
|
||||
export type DetailsProps = {
|
||||
openInitial?: boolean
|
||||
summaryContent?: React.ReactNode
|
||||
summaryElm?: React.ReactNode
|
||||
} & React.HTMLAttributes<HTMLDetailsElement>
|
||||
|
||||
const Details = ({
|
||||
openInitial = false,
|
||||
summaryContent,
|
||||
summaryElm,
|
||||
children,
|
||||
...props
|
||||
}: DetailsProps) => {
|
||||
const [open, setOpen] = useState(openInitial)
|
||||
const [showContent, setShowContent] = useState(openInitial)
|
||||
const ref = useRef<HTMLDetailsElement>(null)
|
||||
|
||||
const handleToggle = () => {
|
||||
if (open) {
|
||||
setShowContent(false)
|
||||
} else {
|
||||
setOpen(true)
|
||||
setShowContent(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<details
|
||||
{...props}
|
||||
ref={ref}
|
||||
open={open}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
onToggle={(event) => {
|
||||
// this is to avoid event propagation
|
||||
// when details are nested, which is a bug
|
||||
// in react. Learn more here:
|
||||
// https://github.com/facebook/react/issues/22718
|
||||
event.stopPropagation()
|
||||
}}
|
||||
className={clsx(
|
||||
"border-medusa-border-base dark:border-medusa-border-base-dark border-y",
|
||||
"overflow-hidden",
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{summaryContent && (
|
||||
<summary onClick={handleToggle} className="cursor-pointer">
|
||||
{summaryContent}
|
||||
</summary>
|
||||
)}
|
||||
{summaryElm &&
|
||||
cloneElement(summaryElm as React.ReactElement, {
|
||||
open,
|
||||
onClick: handleToggle,
|
||||
})}
|
||||
<CSSTransition
|
||||
unmountOnExit
|
||||
in={showContent}
|
||||
timeout={150}
|
||||
onEnter={(node: HTMLElement) => {
|
||||
node.classList.add(
|
||||
"!mb-2",
|
||||
"!mt-0",
|
||||
"translate-y-1",
|
||||
"transition-transform"
|
||||
)
|
||||
}}
|
||||
onExit={(node: HTMLElement) => {
|
||||
node.classList.add("transition-transform", "!-translate-y-1")
|
||||
setTimeout(() => {
|
||||
setOpen(false)
|
||||
}, 100)
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<Loading className="!mb-2 !mt-0" />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</CSSTransition>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
export default Details
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { request } from "@octokit/request"
|
||||
import Link from "@/components/MDXComponents/Link"
|
||||
|
||||
type SolutionsProps = {
|
||||
feedback: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
type GitHubSearchItem = {
|
||||
url: string
|
||||
html_url: string
|
||||
title: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const Solutions: React.FC<SolutionsProps> = ({ feedback, message }) => {
|
||||
const [possibleSolutionsQuery, setPossibleSolutionsQuery] =
|
||||
useState<string>("")
|
||||
const [possibleSolutions, setPossibleSolutions] = useState<
|
||||
GitHubSearchItem[]
|
||||
>([])
|
||||
|
||||
function constructQuery(searchQuery: string) {
|
||||
return `${searchQuery} repo:medusajs/medusa is:closed is:issue`
|
||||
}
|
||||
|
||||
async function searchGitHub(query: string) {
|
||||
return request(`GET /search/issues`, {
|
||||
q: query,
|
||||
sort: "updated",
|
||||
per_page: 3,
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!feedback) {
|
||||
let query = constructQuery(
|
||||
// Github does not allow queries longer than 256 characters
|
||||
message ? message.substring(0, 256) : document.title
|
||||
)
|
||||
searchGitHub(query)
|
||||
.then(async (result) => {
|
||||
if (!result.data.items.length && message) {
|
||||
query = constructQuery(document.title)
|
||||
result = await searchGitHub(query)
|
||||
}
|
||||
|
||||
setPossibleSolutionsQuery(query)
|
||||
setPossibleSolutions(result.data.items)
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
} else {
|
||||
setPossibleSolutionsQuery("")
|
||||
setPossibleSolutions([])
|
||||
}
|
||||
}, [feedback, message])
|
||||
|
||||
return (
|
||||
<>
|
||||
{possibleSolutions.length > 0 && (
|
||||
<div className="text-compact-large-plus font-normal">
|
||||
<span className="my-1 mx-0 inline-block">
|
||||
If you faced a problem, here are some possible solutions from
|
||||
GitHub:
|
||||
</span>
|
||||
<ul>
|
||||
{possibleSolutions.map((solution) => (
|
||||
<li key={solution.url} className="mb-0.5 last:mb-0">
|
||||
<Link href={solution.html_url} target="_blank" rel="noreferrer">
|
||||
{solution.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<span>
|
||||
Explore more issues in{" "}
|
||||
<a
|
||||
href={`https://github.com/medusajs/medusa/issues?q=${possibleSolutionsQuery}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
the GitHub repository
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Solutions
|
||||
@@ -0,0 +1,207 @@
|
||||
"use client"
|
||||
|
||||
import React, { useRef, useState } from "react"
|
||||
import { CSSTransition, SwitchTransition } from "react-transition-group"
|
||||
import Solutions from "./Solutions/index"
|
||||
import Button from "../Button"
|
||||
import { ExtraData, useAnalytics } from "@/providers/analytics"
|
||||
import { usePathname } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { useArea } from "../../providers/area"
|
||||
import clsx from "clsx"
|
||||
import TextArea from "../TextArea"
|
||||
import Label from "../Label"
|
||||
|
||||
type FeedbackProps = {
|
||||
event: string
|
||||
question?: string
|
||||
positiveBtn?: string
|
||||
negativeBtn?: string
|
||||
positiveQuestion?: string
|
||||
negativeQuestion?: string
|
||||
submitBtn?: string
|
||||
submitMessage?: string
|
||||
showPossibleSolutions?: boolean
|
||||
className?: string
|
||||
extraData?: ExtraData
|
||||
sectionTitle?: string
|
||||
vertical?: boolean
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const Feedback: React.FC<FeedbackProps> = ({
|
||||
event,
|
||||
question = "Was this section helpful?",
|
||||
positiveBtn = "Yes",
|
||||
negativeBtn = "No",
|
||||
positiveQuestion = "What was most helpful?",
|
||||
negativeQuestion = "What can we improve?",
|
||||
submitBtn = "Submit",
|
||||
submitMessage = "Thank you for helping improve our documentation!",
|
||||
showPossibleSolutions = true,
|
||||
className = "",
|
||||
extraData = {},
|
||||
sectionTitle = "",
|
||||
vertical = false,
|
||||
}) => {
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [submittedFeedback, setSubmittedFeedback] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const inlineFeedbackRef = useRef<HTMLDivElement>(null)
|
||||
const inlineQuestionRef = useRef<HTMLDivElement>(null)
|
||||
const inlineMessageRef = useRef<HTMLDivElement>(null)
|
||||
const [positiveFeedback, setPositiveFeedback] = useState(false)
|
||||
const [message, setMessage] = useState("")
|
||||
const nodeRef: React.RefObject<HTMLDivElement> = submittedFeedback
|
||||
? inlineMessageRef
|
||||
: showForm
|
||||
? inlineQuestionRef
|
||||
: inlineFeedbackRef
|
||||
|
||||
const pathname = usePathname()
|
||||
const { loaded, track } = useAnalytics()
|
||||
const { area } = useArea()
|
||||
|
||||
function handleFeedback(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||
if (!loaded) {
|
||||
return
|
||||
}
|
||||
const feedback = (e.target as Element).classList.contains("positive")
|
||||
setPositiveFeedback(feedback)
|
||||
setShowForm(true)
|
||||
submitFeedback(e, feedback)
|
||||
}
|
||||
|
||||
function submitFeedback(
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||
feedback = false
|
||||
) {
|
||||
if (showForm) {
|
||||
setLoading(true)
|
||||
}
|
||||
track(
|
||||
event,
|
||||
{
|
||||
url: pathname,
|
||||
label: document.title,
|
||||
feedback:
|
||||
(feedback !== null && feedback) ||
|
||||
(feedback === null && positiveFeedback)
|
||||
? "yes"
|
||||
: "no",
|
||||
message: message?.length ? message : null,
|
||||
os: window.navigator.userAgent,
|
||||
...extraData,
|
||||
},
|
||||
function () {
|
||||
if (showForm) {
|
||||
setLoading(false)
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setShowForm(false)
|
||||
setSubmittedFeedback(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("mt-3", className)}>
|
||||
<SwitchTransition mode="out-in">
|
||||
<CSSTransition
|
||||
key={
|
||||
showForm
|
||||
? "show_form"
|
||||
: !submittedFeedback
|
||||
? "feedback"
|
||||
: "submitted_feedback"
|
||||
}
|
||||
nodeRef={nodeRef}
|
||||
timeout={300}
|
||||
addEndListener={(done) => {
|
||||
nodeRef.current?.addEventListener("transitionend", done, false)
|
||||
}}
|
||||
classNames={{
|
||||
enter: "animate-fadeIn animate-fill-forwards animate-fast",
|
||||
exit: "animate-fadeOut animate-fill-forwards animate-fast",
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{!showForm && !submittedFeedback && (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex",
|
||||
!vertical && "flex-row items-center",
|
||||
vertical && "flex-col justify-center gap-1"
|
||||
)}
|
||||
ref={inlineFeedbackRef}
|
||||
>
|
||||
<Label className="mr-1.5">{question}</Label>
|
||||
<div className={clsx("flex flex-row items-center gap-0.5")}>
|
||||
<Button
|
||||
onClick={handleFeedback}
|
||||
className="positive w-fit"
|
||||
variant="secondary"
|
||||
>
|
||||
{positiveBtn}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleFeedback}
|
||||
className="w-fit"
|
||||
variant="secondary"
|
||||
>
|
||||
{negativeBtn}
|
||||
</Button>
|
||||
<Link
|
||||
href={`https://github.com/medusajs/medusa/issues/new?assignees=&labels=type%3A+docs&template=docs.yml&title=API%20Ref%28${area}%29%3A%20Issue%20in%20${encodeURI(
|
||||
sectionTitle
|
||||
)}`}
|
||||
className="btn-secondary"
|
||||
>
|
||||
Report Issue
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showForm && !submittedFeedback && (
|
||||
<div className="flex flex-col gap-1" ref={inlineQuestionRef}>
|
||||
<Label>
|
||||
{positiveFeedback ? positiveQuestion : negativeQuestion}
|
||||
</Label>
|
||||
<TextArea
|
||||
rows={4}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
onClick={submitFeedback}
|
||||
disabled={loading}
|
||||
className="w-fit"
|
||||
variant="secondary"
|
||||
>
|
||||
{submitBtn}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{submittedFeedback && (
|
||||
<div>
|
||||
<div
|
||||
className="text-compact-large-plus flex flex-col"
|
||||
ref={inlineMessageRef}
|
||||
>
|
||||
<span>{submitMessage}</span>
|
||||
{showPossibleSolutions && (
|
||||
<Solutions message={message} feedback={positiveFeedback} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</CSSTransition>
|
||||
</SwitchTransition>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Feedback
|
||||
@@ -0,0 +1,26 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconAlert = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M18 10C18 12.1217 17.1571 14.1566 15.6569 15.6569C14.1566 17.1571 12.1217 18 10 18C7.87827 18 5.84344 17.1571 4.34315 15.6569C2.84285 14.1566 2 12.1217 2 10C2 7.87827 2.84285 5.84344 4.34315 4.34315C5.84344 2.84285 7.87827 2 10 2C12.1217 2 14.1566 2.84285 15.6569 4.34315C17.1571 5.84344 18 7.87827 18 10ZM10 5C10.1989 5 10.3897 5.07902 10.5303 5.21967C10.671 5.36032 10.75 5.55109 10.75 5.75V10.25C10.75 10.4489 10.671 10.6397 10.5303 10.7803C10.3897 10.921 10.1989 11 10 11C9.80109 11 9.61032 10.921 9.46967 10.7803C9.32902 10.6397 9.25 10.4489 9.25 10.25V5.75C9.25 5.55109 9.32902 5.36032 9.46967 5.21967C9.61032 5.07902 9.80109 5 10 5ZM10 15C10.2652 15 10.5196 14.8946 10.7071 14.7071C10.8946 14.5196 11 14.2652 11 14C11 13.7348 10.8946 13.4804 10.7071 13.2929C10.5196 13.1054 10.2652 13 10 13C9.73478 13 9.48043 13.1054 9.29289 13.2929C9.10536 13.4804 9 13.7348 9 14C9 14.2652 9.10536 14.5196 9.29289 14.7071C9.48043 14.8946 9.73478 15 10 15Z"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconAlert
|
||||
@@ -0,0 +1,27 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconArrowUpRightOnBox = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9.55356 5.32683H5.24268C4.7779 5.32683 4.33216 5.51146 4.00351 5.84011C3.67487 6.16875 3.49023 6.6145 3.49023 7.07927V15.2574C3.49023 15.7221 3.67487 16.1679 4.00351 16.4965C4.33216 16.8252 4.7779 17.0098 5.24268 17.0098H13.4208C13.8855 17.0098 14.3313 16.8252 14.6599 16.4965C14.9886 16.1679 15.1732 15.7221 15.1732 15.2574V11.0207M7.50323 13.0137L17.5098 2.99023M17.5098 2.99023H13.4208M17.5098 2.99023V7.07927"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconArrowUpRightOnBox
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react"
|
||||
import IconProps from "../types"
|
||||
|
||||
const IconBarsThree = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M3.125 5.00006H16.875M3.125 10H16.875M3.125 15.0001H16.875"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconBarsThree
|
||||
@@ -0,0 +1,27 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconChevronDownMini = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M15 8L10 13L5 8"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconChevronDownMini
|
||||
@@ -0,0 +1,32 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconChevronRightMini = ({
|
||||
iconColorClassName,
|
||||
containerClassName,
|
||||
...props
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={containerClassName}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8 6L12 10L8 14"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconChevronRightMini
|
||||
@@ -0,0 +1,31 @@
|
||||
import IconProps from "../types"
|
||||
|
||||
const IconCopy = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M2 4.25C2 3.65326 2.23705 3.08097 2.65901 2.65901C3.08097 2.23705 3.65326 2 4.25 2H10.75C11.3467 2 11.919 2.23705 12.341 2.65901C12.7629 3.08097 13 3.65326 13 4.25V5.5H9.25C8.25544 5.5 7.30161 5.89509 6.59835 6.59835C5.89509 7.30161 5.5 8.25544 5.5 9.25V13H4.25C3.65326 13 3.08097 12.7629 2.65901 12.341C2.23705 11.919 2 11.3467 2 10.75V4.25Z"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
|
||||
}
|
||||
/>
|
||||
<path
|
||||
d="M9.25 7C8.65326 7 8.08097 7.23705 7.65901 7.65901C7.23705 8.08097 7 8.65326 7 9.25V15.75C7 16.3467 7.23705 16.919 7.65901 17.341C8.08097 17.7629 8.65326 18 9.25 18H15.75C16.3467 18 16.919 17.7629 17.341 17.341C17.7629 16.919 18 16.3467 18 15.75V9.25C18 8.65326 17.7629 8.08097 17.341 7.65901C16.919 7.23705 16.3467 7 15.75 7H9.25Z"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconCopy
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react"
|
||||
import IconProps from "../types"
|
||||
|
||||
const IconCopyOutline = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M13.75 6.875V5C13.75 4.50272 13.5525 4.02581 13.2008 3.67417C12.8492 3.32254 12.3723 3.125 11.875 3.125H5C4.50272 3.125 4.02581 3.32254 3.67417 3.67417C3.32254 4.02581 3.125 4.50272 3.125 5V11.875C3.125 12.3723 3.32254 12.8492 3.67417 13.2008C4.02581 13.5525 4.50272 13.75 5 13.75H6.875M13.75 6.875H15C15.4973 6.875 15.9742 7.07254 16.3258 7.42417C16.6775 7.77581 16.875 8.25272 16.875 8.75V15C16.875 15.4973 16.6775 15.9742 16.3258 16.3258C15.9742 16.6775 15.4973 16.875 15 16.875H8.75C8.25272 16.875 7.77581 16.6775 7.42417 16.3258C7.07254 15.9742 6.875 15.4973 6.875 15V13.75M13.75 6.875H8.75C8.25272 6.875 7.77581 7.07254 7.42417 7.42417C7.07254 7.77581 6.875 8.25272 6.875 8.75V13.75"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconCopyOutline
|
||||
@@ -0,0 +1,27 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconDarkMode = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M18.1267 12.5017C17.136 12.9147 16.0732 13.1265 15 13.1251C10.5125 13.1251 6.875 9.48758 6.875 5.00008C6.875 3.89175 7.09667 2.83591 7.49833 1.87341C6.01789 2.49101 4.75331 3.53287 3.86386 4.86779C2.9744 6.20271 2.49986 7.77098 2.5 9.37508C2.5 13.8626 6.1375 17.5001 10.625 17.5001C12.2291 17.5002 13.7974 17.0257 15.1323 16.1362C16.4672 15.2468 17.5091 13.9822 18.1267 12.5017Z"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconDarkMode
|
||||
@@ -0,0 +1,27 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconLightMode = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10 2.5V4.375M15.3033 4.69667L13.9775 6.0225M17.5 10H15.625M15.3033 15.3033L13.9775 13.9775M10 15.625V17.5M6.0225 13.9775L4.69667 15.3033M4.375 10H2.5M6.0225 6.0225L4.69667 4.69667M13.125 10C13.125 10.8288 12.7958 11.6237 12.2097 12.2097C11.6237 12.7958 10.8288 13.125 10 13.125C9.1712 13.125 8.37634 12.7958 7.79029 12.2097C7.20424 11.6237 6.875 10.8288 6.875 10C6.875 9.1712 7.20424 8.37634 7.79029 7.79029C8.37634 7.20424 9.1712 6.875 10 6.875C10.8288 6.875 11.6237 7.20424 12.2097 7.79029C12.7958 8.37634 13.125 9.1712 13.125 10Z"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconLightMode
|
||||
@@ -0,0 +1,25 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconMedusa = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16.2447 3.92183L12.1688 1.57686C10.8352 0.807712 9.20112 0.807712 7.86753 1.57686L3.77285 3.92183C2.45804 4.69098 1.63159 6.11673 1.63159 7.63627V12.345C1.63159 13.8833 2.45804 15.2903 3.77285 16.0594L7.84875 18.4231C9.18234 19.1923 10.8165 19.1923 12.15 18.4231L16.2259 16.0594C17.5595 15.2903 18.3672 13.8833 18.3672 12.345V7.63627C18.4048 6.11673 17.5783 4.69098 16.2447 3.92183ZM10.0088 14.1834C7.69849 14.1834 5.82019 12.3075 5.82019 10C5.82019 7.69255 7.69849 5.81657 10.0088 5.81657C12.3191 5.81657 14.2162 7.69255 14.2162 10C14.2162 12.3075 12.3379 14.1834 10.0088 14.1834Z"
|
||||
fill="#030712"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconMedusa
|
||||
@@ -0,0 +1,27 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconMinusMini = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M14.375 10H5.62498"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconMinusMini
|
||||
@@ -0,0 +1,27 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconPlusMini = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10.5 5V15M15.5 10H5.5"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconPlusMini
|
||||
@@ -0,0 +1,47 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconReport = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10 17.5C14.1421 17.5 17.5 14.1421 17.5 10C17.5 5.85786 14.1421 2.5 10 2.5C5.85786 2.5 2.5 5.85786 2.5 10C2.5 14.1421 5.85786 17.5 10 17.5Z"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10 6.6665V9.99984"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10 13.3335H10.0088"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconReport
|
||||
@@ -0,0 +1,27 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconSidebar = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9 3.125H4.375C3.87772 3.125 3.40081 3.32254 3.04917 3.67417C2.69754 4.02581 2.5 4.50272 2.5 5V6.875V15C2.5 15.4973 2.69754 15.9742 3.04917 16.3258C3.40081 16.6775 3.87772 16.875 4.375 16.875H9M9 3.125H15.625C16.1223 3.125 16.5992 3.32254 16.9508 3.67417C17.3025 4.02581 17.5 4.50272 17.5 5V6.875V15C17.5 15.4973 17.3025 15.9742 16.9508 16.3258C16.5992 16.6775 16.1223 16.875 15.625 16.875H9M9 3.125V16.875M5 6.5H6.5M5 9.5H6.5"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconSidebar
|
||||
@@ -0,0 +1,24 @@
|
||||
import type IconProps from "../types"
|
||||
|
||||
const IconSpinner = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M14.8649 14.8649C15.3023 15.3023 15.3063 16.0188 14.8121 16.3909C14.0399 16.9723 13.1661 17.4109 12.2319 17.6823C10.9379 18.0583 9.56991 18.1013 8.25485 17.8073C6.93979 17.5134 5.72029 16.892 4.7095 16.0009C3.69872 15.1098 2.92941 13.9778 2.47295 12.7099C2.0165 11.4421 1.8877 10.0794 2.09849 8.74852C2.30929 7.4176 2.85286 6.16149 3.67876 5.09674C4.50466 4.032 5.58613 3.19312 6.82282 2.65796C7.71563 2.27161 8.66846 2.05258 9.6341 2.00837C10.252 1.98008 10.7057 2.53472 10.6475 3.15053C10.5893 3.76635 10.0393 4.20701 9.42382 4.26889C8.83606 4.32798 8.25864 4.47736 7.71243 4.71373C6.82201 5.09905 6.04336 5.70304 5.44871 6.46966C4.85406 7.23627 4.46269 8.14067 4.31091 9.09894C4.15914 10.0572 4.25188 11.0383 4.58053 11.9511C4.90917 12.864 5.46308 13.679 6.19084 14.3206C6.91861 14.9623 7.79665 15.4096 8.74349 15.6213C9.69034 15.8329 10.6753 15.802 11.607 15.5313C12.1785 15.3652 12.7186 15.1123 13.2092 14.7832C13.7229 14.4385 14.4275 14.4275 14.8649 14.8649Z"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconSpinner
|
||||
@@ -0,0 +1,27 @@
|
||||
import IconProps from "../types"
|
||||
|
||||
const IconXMark = ({ iconColorClassName, ...props }: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={props.width || 20}
|
||||
height={props.height || 20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5 15L15 5M5 5L15 15"
|
||||
className={
|
||||
iconColorClassName ||
|
||||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
|
||||
}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconXMark
|
||||
@@ -0,0 +1,6 @@
|
||||
type IconProps = {
|
||||
iconColorClassName?: string
|
||||
containerClassName?: string
|
||||
} & React.AllHTMLAttributes<SVGElement>
|
||||
|
||||
export default IconProps
|
||||
@@ -0,0 +1,45 @@
|
||||
import { CopyButtonProps } from "@/components/CopyButton"
|
||||
import clsx from "clsx"
|
||||
import dynamic from "next/dynamic"
|
||||
import SpinnerLoading from "../Loading/Spinner"
|
||||
|
||||
const CopyButton = dynamic<CopyButtonProps>(
|
||||
async () => import("../CopyButton"),
|
||||
{
|
||||
loading: () => <SpinnerLoading />,
|
||||
}
|
||||
) as React.FC<CopyButtonProps>
|
||||
|
||||
export type InlineCodeProps = React.ComponentProps<"code">
|
||||
|
||||
const InlineCode = (props: InlineCodeProps) => {
|
||||
const isInline = typeof props.children === "string"
|
||||
return (
|
||||
<>
|
||||
{!isInline && <code {...props} />}
|
||||
{isInline && (
|
||||
<CopyButton
|
||||
text={props.children as string}
|
||||
buttonClassName={clsx(
|
||||
"bg-transparent border-0 p-0 inline text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
|
||||
"active:[&>code]:bg-medusa-bg-subtle-pressed dark:active:[&>code]:bg-medusa-bg-subtle-pressed-dark",
|
||||
"focus:[&>code]:bg-medusa-bg-subtle-pressed dark:focus:[&>code]:bg-medusa-bg-subtle-pressed-dark",
|
||||
"hover:[&>code]:bg-medusa-bg-subtle-hover dark:hover:[&>code]:bg-medusa-bg-base-hover-dark"
|
||||
)}
|
||||
>
|
||||
<code
|
||||
{...props}
|
||||
className={clsx(
|
||||
"border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark border",
|
||||
"text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text-dark",
|
||||
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark font-monospace text-code-label rounded-sm py-0 px-[6px]",
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
</CopyButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InlineCode
|
||||
@@ -0,0 +1,31 @@
|
||||
import clsx from "clsx"
|
||||
type InputTextProps = {
|
||||
className?: string
|
||||
} & React.DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
>
|
||||
|
||||
const InputText = (props: InputTextProps) => {
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
className={clsx(
|
||||
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-secondary dark:shadow-button-secondary-dark",
|
||||
"border-medusa-border-loud-muted dark:border-medusa-border-loud-muted-dark rounded-sm border border-solid",
|
||||
"px-0.75 py-[9px]",
|
||||
"hover:bg-medusa-bg-field-hover dark:hover:bg-medusa-bg-field-hover-dark",
|
||||
"focus:border-medusa-border-interactive dark:focus:border-medusa-border-interactive-dark",
|
||||
"active:border-medusa-border-interactive dark:active:border-medusa-border-interactive-dark",
|
||||
"disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
|
||||
"disabled:border-medusa-border-base dark:disabled:border-medusa-border-base-dark",
|
||||
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
|
||||
"disabled:placeholder:text-medusa-fg-disabled dark:disabled:placeholder:text-medusa-fg-disabled-dark",
|
||||
"text-compact-medium font-base",
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputText
|
||||
@@ -0,0 +1,21 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
|
||||
type LabelProps = {
|
||||
className?: string
|
||||
} & React.HTMLAttributes<HTMLSpanElement>
|
||||
|
||||
const Label = ({ children, className }: LabelProps) => {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-compact-medium-plus",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default Label
|
||||
@@ -0,0 +1,11 @@
|
||||
import Loading from ".."
|
||||
|
||||
const ContentLoading = () => {
|
||||
return (
|
||||
<div className="w-api-ref-content">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContentLoading
|
||||
@@ -0,0 +1,55 @@
|
||||
import DividedLayout from "@/layouts/Divided"
|
||||
import Loading from ".."
|
||||
|
||||
type DividedLoadingProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const DividedLoading = ({ className }: DividedLoadingProps) => {
|
||||
return (
|
||||
<DividedLayout
|
||||
mainContent={
|
||||
<>
|
||||
<Loading count={1} className="mb-2 !w-1/3" />
|
||||
<Loading count={1} />
|
||||
<div className="flex gap-1">
|
||||
<Loading count={1} className="!w-1/3" />
|
||||
<Loading count={1} className="!w-2/3" />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Loading count={1} className="!w-1/3" />
|
||||
<Loading count={1} className="!w-2/3" />
|
||||
</div>
|
||||
<Loading count={1} className="mt-2 !w-1/3" />
|
||||
<Loading count={1} />
|
||||
<div className="mt-2 flex gap-1">
|
||||
<Loading count={1} className="!w-1/3" />
|
||||
<Loading count={1} className="!w-2/3" />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Loading count={1} className="!w-1/3" />
|
||||
<Loading count={1} className="!w-2/3" />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Loading count={1} className="!w-1/3" />
|
||||
<Loading count={1} className="!w-2/3" />
|
||||
</div>
|
||||
<Loading count={5} barClassName="mt-1" />
|
||||
</>
|
||||
}
|
||||
codeContent={
|
||||
<>
|
||||
<Loading count={1} />
|
||||
<Loading count={1} className="my-2" />
|
||||
<Loading count={1} barClassName="h-[200px] !rounded-sm" />
|
||||
<Loading count={1} className="my-2" />
|
||||
<Loading count={1} barClassName="h-3 !rounded-sm" />
|
||||
<Loading count={1} barClassName="h-[230px] !rounded-sm" />
|
||||
</>
|
||||
}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default DividedLoading
|
||||
@@ -0,0 +1,20 @@
|
||||
import IconSpinner from "@/components/Icons/Spinner"
|
||||
import type IconProps from "@/components/Icons/types"
|
||||
import clsx from "clsx"
|
||||
|
||||
type SpinnerLoadingProps = {
|
||||
iconProps?: IconProps
|
||||
}
|
||||
|
||||
const SpinnerLoading = ({ iconProps }: SpinnerLoadingProps) => {
|
||||
return (
|
||||
<span role="status">
|
||||
<IconSpinner
|
||||
{...iconProps}
|
||||
className={clsx("animate-spin", iconProps?.className)}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpinnerLoading
|
||||
@@ -0,0 +1,40 @@
|
||||
import clsx from "clsx"
|
||||
|
||||
type LoadingProps = {
|
||||
className?: string
|
||||
barClassName?: string
|
||||
count?: number
|
||||
}
|
||||
|
||||
const Loading = ({ className, count = 6, barClassName }: LoadingProps) => {
|
||||
const getLoadingBars = () => {
|
||||
const bars = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
bars.push(
|
||||
<span
|
||||
className={clsx(
|
||||
"bg-medusa-bg-subtle-pressed dark:bg-medusa-bg-subtle-pressed-dark h-1 w-full rounded-full",
|
||||
barClassName
|
||||
)}
|
||||
key={i}
|
||||
></span>
|
||||
)
|
||||
}
|
||||
|
||||
return bars
|
||||
}
|
||||
return (
|
||||
<span
|
||||
role="status"
|
||||
className={clsx(
|
||||
"my-1 flex w-full animate-pulse flex-col gap-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{getLoadingBars()}
|
||||
<span className="sr-only">Loading...</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
||||
@@ -0,0 +1,27 @@
|
||||
import CodeBlock from "@/components/CodeBlock"
|
||||
import InlineCode from "../../InlineCode"
|
||||
|
||||
type CodeWrapperProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
// due to how mdx handles code blocks
|
||||
// it is required that a code block specify a language
|
||||
// to be considered a block. Otherwise, it will be
|
||||
// considered as inline code
|
||||
const CodeWrapper = ({ className, children }: CodeWrapperProps) => {
|
||||
if (!children) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
|
||||
if (match) {
|
||||
return <CodeBlock source={children as string} lang={match[1]} />
|
||||
}
|
||||
|
||||
return <InlineCode>{children}</InlineCode>
|
||||
}
|
||||
|
||||
export default CodeWrapper
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
|
||||
import { InView } from "react-intersection-observer"
|
||||
import { useSidebar } from "../../../providers/sidebar"
|
||||
import checkElementInViewport from "../../../utils/check-element-in-viewport"
|
||||
import { useEffect } from "react"
|
||||
import getSectionId from "../../../utils/get-section-id"
|
||||
|
||||
type H2Props = {
|
||||
addToSidebar?: boolean
|
||||
} & React.HTMLAttributes<HTMLHeadingElement>
|
||||
|
||||
const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => {
|
||||
const { activePath, setActivePath, addItems } = useSidebar()
|
||||
|
||||
const handleViewChange = (
|
||||
inView: boolean,
|
||||
entry: IntersectionObserverEntry
|
||||
) => {
|
||||
if (!addToSidebar) {
|
||||
return
|
||||
}
|
||||
const heading = entry.target
|
||||
if (
|
||||
(inView ||
|
||||
checkElementInViewport(heading.parentElement || heading, 40)) &&
|
||||
window.scrollY !== 0 &&
|
||||
activePath !== heading.id
|
||||
) {
|
||||
// can't use next router as it doesn't support
|
||||
// changing url without scrolling
|
||||
history.pushState({}, "", `#${heading.id}`)
|
||||
setActivePath(heading.id)
|
||||
}
|
||||
}
|
||||
const id = getSectionId([children as string])
|
||||
|
||||
useEffect(() => {
|
||||
if (id === (activePath || location.hash.replace("#", ""))) {
|
||||
const elm = document.getElementById(id)
|
||||
elm?.scrollIntoView()
|
||||
}
|
||||
|
||||
addItems([
|
||||
{
|
||||
path: `${id}`,
|
||||
title: children as string,
|
||||
loaded: true,
|
||||
},
|
||||
])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<InView
|
||||
as="h2"
|
||||
threshold={0.4}
|
||||
skip={!addToSidebar}
|
||||
initialInView={false}
|
||||
{...props}
|
||||
onChange={handleViewChange}
|
||||
id={id}
|
||||
>
|
||||
{children}
|
||||
</InView>
|
||||
)
|
||||
}
|
||||
|
||||
export default H2
|
||||
@@ -0,0 +1,28 @@
|
||||
import clsx from "clsx"
|
||||
import NextLink from "next/link"
|
||||
import type { LinkProps as NextLinkProps } from "next/link"
|
||||
|
||||
export type LinkProps = {
|
||||
href?: string
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
} & Partial<NextLinkProps> &
|
||||
React.AllHTMLAttributes<HTMLAnchorElement>
|
||||
|
||||
const Link = ({ href, children, className, ...rest }: LinkProps) => {
|
||||
return (
|
||||
<NextLink
|
||||
href={href || ""}
|
||||
{...rest}
|
||||
className={clsx(
|
||||
"text-medusa-fg-interactive hover:text-medusa-fg-interactive-hover",
|
||||
"dark:text-medusa-fg-interactive-dark dark:hover:text-medusa-fg-interactive-hover-dark",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
|
||||
export default Link
|
||||
@@ -0,0 +1,66 @@
|
||||
import Loading from "@/components/Loading"
|
||||
import type { MDXContentClientProps } from "@/components/MDXContent/Client"
|
||||
import type { MDXContentServerProps } from "@/components/MDXContent/Server"
|
||||
import type { SecuritySchemeObject } from "@/types/openapi"
|
||||
import getSecuritySchemaTypeName from "@/utils/get-security-schema-type-name"
|
||||
import clsx from "clsx"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
const MDXContentClient = dynamic<MDXContentClientProps>(
|
||||
async () => import("../../../MDXContent/Client"),
|
||||
{
|
||||
loading: () => <Loading />,
|
||||
}
|
||||
) as React.FC<MDXContentClientProps>
|
||||
|
||||
const MDXContentServer = dynamic<MDXContentServerProps>(
|
||||
async () => import("../../../MDXContent/Server"),
|
||||
{
|
||||
loading: () => <Loading />,
|
||||
}
|
||||
) as React.FC<MDXContentServerProps>
|
||||
|
||||
export type SecurityDescriptionProps = {
|
||||
securitySchema: SecuritySchemeObject
|
||||
isServer?: boolean
|
||||
}
|
||||
|
||||
const SecurityDescription = ({
|
||||
securitySchema,
|
||||
isServer = true,
|
||||
}: SecurityDescriptionProps) => {
|
||||
return (
|
||||
<>
|
||||
<h2>{securitySchema["x-displayName"] as string}</h2>
|
||||
{isServer && <MDXContentServer content={securitySchema.description} />}
|
||||
{!isServer && <MDXContentClient content={securitySchema.description} />}
|
||||
<p>
|
||||
<strong>Security Scheme Type:</strong>{" "}
|
||||
{getSecuritySchemaTypeName(securitySchema)}
|
||||
</p>
|
||||
{(securitySchema.type === "http" || securitySchema.type === "apiKey") && (
|
||||
<p
|
||||
className={clsx(
|
||||
"bg-docs-bg-surface dark:bg-docs-bg-surface-dark",
|
||||
"p-1"
|
||||
)}
|
||||
>
|
||||
<strong>
|
||||
{securitySchema.type === "http"
|
||||
? "HTTP Authorization Scheme"
|
||||
: "Cookie parameter name"}
|
||||
:
|
||||
</strong>{" "}
|
||||
<code>
|
||||
{securitySchema.type === "http"
|
||||
? securitySchema.scheme
|
||||
: securitySchema.name}
|
||||
</code>
|
||||
</p>
|
||||
)}
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SecurityDescription
|
||||
@@ -0,0 +1,34 @@
|
||||
import dynamic from "next/dynamic"
|
||||
import type { OpenAPIV3 } from "openapi-types"
|
||||
import type { SecurityDescriptionProps } from "./Description"
|
||||
import { Fragment } from "react"
|
||||
|
||||
const SecurityDescription = dynamic<SecurityDescriptionProps>(
|
||||
async () => import("./Description")
|
||||
) as React.FC<SecurityDescriptionProps>
|
||||
|
||||
type SecurityProps = {
|
||||
specs?: OpenAPIV3.Document
|
||||
}
|
||||
|
||||
const Security = ({ specs }: SecurityProps) => {
|
||||
return (
|
||||
<div>
|
||||
{specs && (
|
||||
<>
|
||||
{Object.values(specs.components?.securitySchemes || {}).map(
|
||||
(securitySchema, index) => (
|
||||
<Fragment key={index}>
|
||||
{!("$ref" in securitySchema) && (
|
||||
<SecurityDescription securitySchema={securitySchema} />
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Security
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { MDXComponents } from "mdx/types"
|
||||
import Security from "./Security"
|
||||
import type { OpenAPIV3 } from "openapi-types"
|
||||
import Link from "./Link"
|
||||
import CodeWrapper from "./CodeWrapper"
|
||||
import H2 from "./H2"
|
||||
|
||||
export type ScopeType = {
|
||||
specs?: OpenAPIV3.Document
|
||||
addToSidebar?: boolean
|
||||
}
|
||||
|
||||
const getCustomComponents = (scope?: ScopeType): MDXComponents => {
|
||||
return {
|
||||
Security: () => <Security specs={scope?.specs} />,
|
||||
code: CodeWrapper,
|
||||
a: Link,
|
||||
h2: (props) => <H2 addToSidebar={scope?.addToSidebar} {...props} />,
|
||||
}
|
||||
}
|
||||
|
||||
export default getCustomComponents
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import getCustomComponents from "../../MDXComponents"
|
||||
import type { ScopeType } from "../../MDXComponents"
|
||||
import { MDXRemote } from "next-mdx-remote"
|
||||
import type { MDXRemoteProps, MDXRemoteSerializeResult } from "next-mdx-remote"
|
||||
import { serialize } from "next-mdx-remote/serialize"
|
||||
|
||||
export type MDXContentClientProps = {
|
||||
content: any
|
||||
className?: string
|
||||
} & Partial<MDXRemoteProps>
|
||||
|
||||
const MDXContentClient = ({
|
||||
content,
|
||||
className,
|
||||
...props
|
||||
}: MDXContentClientProps) => {
|
||||
const [parsedContent, setParsedContent] = useState<MDXRemoteSerializeResult>()
|
||||
|
||||
useEffect(() => {
|
||||
void serialize(content, {
|
||||
mdxOptions: {
|
||||
// A workaround for an error in next-mdx-remote
|
||||
// more details in this issue:
|
||||
// https://github.com/hashicorp/next-mdx-remote/issues/350
|
||||
development: process.env.NEXT_PUBLIC_ENV === "development",
|
||||
},
|
||||
scope: props.scope,
|
||||
}).then((output) => {
|
||||
setParsedContent(output)
|
||||
})
|
||||
}, [content, props.scope])
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{parsedContent !== undefined && (
|
||||
<MDXRemote
|
||||
{...parsedContent}
|
||||
components={getCustomComponents((props.scope as ScopeType) || {})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MDXContentClient
|
||||
@@ -0,0 +1,28 @@
|
||||
"use server"
|
||||
|
||||
import { MDXRemote } from "next-mdx-remote/rsc"
|
||||
import getCustomComponents from "../../MDXComponents"
|
||||
import type { ScopeType } from "../../MDXComponents"
|
||||
import type { MDXRemoteProps } from "next-mdx-remote"
|
||||
|
||||
export type MDXContentServerProps = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content: any
|
||||
} & Partial<MDXRemoteProps>
|
||||
|
||||
const MDXContentServer = ({ content, ...props }: MDXContentServerProps) => {
|
||||
return (
|
||||
<>
|
||||
<MDXRemote
|
||||
source={content}
|
||||
components={getCustomComponents((props.scope as ScopeType) || {})}
|
||||
options={{
|
||||
scope: props.scope,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MDXContentServer
|
||||
@@ -0,0 +1,28 @@
|
||||
import clsx from "clsx"
|
||||
import capitalize from "@/utils/capitalize"
|
||||
|
||||
export type MethodLabelProps = {
|
||||
method: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const MethodLabel = ({ method, className }: MethodLabelProps) => {
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"text-compact-x-small-plus rounded-sm border py-0 px-[6px]",
|
||||
method === "get" &&
|
||||
"bg-medusa-tag-green-bg dark:bg-medusa-tag-green-bg-dark text-medusa-tag-green-text dark:text-medusa-tag-green-text-dark border-medusa-tag-green-border dark:border-medusa-tag-green-border-dark",
|
||||
method === "post" &&
|
||||
"bg-medusa-tag-blue-bg dark:bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text dark:text-medusa-tag-blue-text-dark border-medusa-tag-blue-border dark:border-medusa-tag-blue-border-dark",
|
||||
method === "delete" &&
|
||||
"bg-medusa-tag-red-bg dark:bg-medusa-tag-red-bg-dark text-medusa-tag-red-text dark:text-medusa-tag-red-text-dark border-medusa-tag-red-border dark:border-medusa-tag-red-border-dark",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{method === "delete" ? "Del" : capitalize(method)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default MethodLabel
|
||||
@@ -0,0 +1,26 @@
|
||||
import clsx from "clsx"
|
||||
import Button, { ButtonProps } from "../../Button"
|
||||
|
||||
type ModalFooterProps = {
|
||||
actions: ButtonProps[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
const ModalFooter = ({ actions, className }: ModalFooterProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"py-1.5 pl-0 pr-2",
|
||||
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-t border-solid",
|
||||
"flex justify-end gap-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{actions.map((action, index) => (
|
||||
<Button {...action} key={index} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalFooter
|
||||
@@ -0,0 +1,33 @@
|
||||
import clsx from "clsx"
|
||||
import { useModal } from "../../../providers/modal"
|
||||
import IconXMark from "../../Icons/XMark"
|
||||
|
||||
type ModalHeaderProps = {
|
||||
title?: string
|
||||
}
|
||||
|
||||
const ModalHeader = ({ title }: ModalHeaderProps) => {
|
||||
const { closeModal } = useModal()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-b border-solid py-1.5 px-2",
|
||||
"flex items-center justify-between"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-h2"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<button className="btn-clear cursor-pointer" onClick={() => closeModal()}>
|
||||
<IconXMark />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalHeader
|
||||
@@ -0,0 +1,70 @@
|
||||
import clsx from "clsx"
|
||||
import React, { useRef } from "react"
|
||||
import { ButtonProps } from "../Button"
|
||||
import { useModal } from "../../providers/modal"
|
||||
import ModalHeader from "./Header"
|
||||
import ModalFooter from "./Footer"
|
||||
|
||||
export type ModalProps = {
|
||||
className?: string
|
||||
title?: string
|
||||
actions?: ButtonProps[]
|
||||
contentClassName?: string
|
||||
} & React.DetailedHTMLProps<
|
||||
React.DialogHTMLAttributes<HTMLDialogElement>,
|
||||
HTMLDialogElement
|
||||
>
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
className,
|
||||
title,
|
||||
actions,
|
||||
children,
|
||||
contentClassName,
|
||||
...props
|
||||
}) => {
|
||||
const { closeModal } = useModal()
|
||||
const dialogRef = useRef<HTMLDialogElement>(null)
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDialogElement, MouseEvent>) => {
|
||||
// close modal when the user clicks outside the content
|
||||
if (e.target === dialogRef.current) {
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<dialog
|
||||
{...props}
|
||||
className={clsx(
|
||||
"fixed top-0 left-0 flex h-screen w-screen items-center justify-center",
|
||||
"z-[500] bg-transparent",
|
||||
className
|
||||
)}
|
||||
onClick={handleClick}
|
||||
ref={dialogRef}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark rounded-sm",
|
||||
"border-medusa-border-base dark:border-medusa-border-base-dark border border-solid",
|
||||
"shadow-modal dark:shadow-modal-dark",
|
||||
"w-[90%] md:w-[75%] lg:w-[560px]"
|
||||
)}
|
||||
>
|
||||
<ModalHeader title={title} />
|
||||
<div
|
||||
className={clsx(
|
||||
"overflow-auto py-1.5 px-2 lg:min-h-[400px]",
|
||||
contentClassName
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{actions && actions?.length > 0 && <ModalFooter actions={actions} />}
|
||||
</div>
|
||||
</dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import { useColorMode } from "@/providers/color-mode"
|
||||
import NavbarIconButton, { NavbarIconButtonProps } from "../IconButton"
|
||||
import type IconProps from "@/components/Icons/types"
|
||||
import dynamic from "next/dynamic"
|
||||
|
||||
const IconLightMode = dynamic<IconProps>(
|
||||
async () => import("../../Icons/LightMode")
|
||||
) as React.FC<IconProps>
|
||||
|
||||
const IconDarkMode = dynamic<IconProps>(
|
||||
async () => import("../../Icons/DarkMode")
|
||||
) as React.FC<IconProps>
|
||||
|
||||
type NavbarColorModeToggleProps = {
|
||||
buttonProps?: NavbarIconButtonProps
|
||||
}
|
||||
|
||||
const NavbarColorModeToggle = ({ buttonProps }: NavbarColorModeToggleProps) => {
|
||||
const { colorMode, toggleColorMode } = useColorMode()
|
||||
|
||||
return (
|
||||
<NavbarIconButton {...buttonProps} onClick={() => toggleColorMode()}>
|
||||
{colorMode === "light" && (
|
||||
<IconLightMode iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
|
||||
)}
|
||||
{colorMode === "dark" && (
|
||||
<IconDarkMode iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
|
||||
)}
|
||||
</NavbarIconButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavbarColorModeToggle
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useModal } from "../../../providers/modal"
|
||||
import Button from "../../Button"
|
||||
import DetailedFeedback from "../../DetailedFeedback"
|
||||
|
||||
const FeedbackModal = () => {
|
||||
const { setModalProps } = useModal()
|
||||
|
||||
const openModal = () => {
|
||||
setModalProps({
|
||||
title: "Send your Feedback",
|
||||
children: <DetailedFeedback />,
|
||||
contentClassName: "lg:!min-h-auto !p-0",
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={openModal} variant="secondary">
|
||||
Feedback
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeedbackModal
|
||||
@@ -0,0 +1,24 @@
|
||||
import clsx from "clsx"
|
||||
|
||||
export type NavbarIconButtonProps = React.HTMLAttributes<HTMLButtonElement>
|
||||
|
||||
const NavbarIconButton = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: NavbarIconButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
"btn-secondary btn-secondary-icon",
|
||||
"[&>svg]:h-[22px] [&>svg]:w-[22px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavbarIconButton
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client"
|
||||
|
||||
import clsx from "clsx"
|
||||
import Link from "next/link"
|
||||
import type { LinkProps } from "next/link"
|
||||
import { useNavbar } from "@/providers/navbar"
|
||||
import { Area } from "@/types/openapi"
|
||||
|
||||
type NavbarLinkProps = {
|
||||
href: string
|
||||
label: string
|
||||
className?: string
|
||||
activeValue?: Area
|
||||
} & LinkProps
|
||||
|
||||
const NavbarLink = ({
|
||||
href,
|
||||
label,
|
||||
className,
|
||||
activeValue,
|
||||
}: NavbarLinkProps) => {
|
||||
const { activeItem } = useNavbar()
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={clsx(
|
||||
activeItem === activeValue &&
|
||||
"text-medusa-fg-base dark:text-medusa-fg-base-dark",
|
||||
activeItem !== activeValue &&
|
||||
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
|
||||
"text-compact-small-plus inline-block",
|
||||
"hover:text-medusa-fg-base dark:hover:text-medusa-fg-base-dark",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavbarLink
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { useColorMode } from "@/providers/color-mode"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
const NavbarLogo = () => {
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
return (
|
||||
<Link href={`/`} className="flex-1">
|
||||
<Image
|
||||
src={
|
||||
colorMode === "light"
|
||||
? "/images/logo-icon.png"
|
||||
: "/images/logo-icon-dark.png"
|
||||
}
|
||||
alt="Medusa Logo"
|
||||
height={20}
|
||||
width={20}
|
||||
className="align-middle"
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavbarLogo
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client"
|
||||
|
||||
import NavbarIconButton, { NavbarIconButtonProps } from "../IconButton"
|
||||
import { useSidebar } from "@/providers/sidebar"
|
||||
import IconSidebar from "../../Icons/Sidebar"
|
||||
import clsx from "clsx"
|
||||
import IconXMark from "../../Icons/XMark"
|
||||
|
||||
type NavbarMenuButtonProps = {
|
||||
buttonProps?: NavbarIconButtonProps
|
||||
}
|
||||
|
||||
const NavbarMenuButton = ({ buttonProps }: NavbarMenuButtonProps) => {
|
||||
const { items, setMobileSidebarOpen, mobileSidebarOpen } = useSidebar()
|
||||
|
||||
return (
|
||||
<NavbarIconButton
|
||||
{...buttonProps}
|
||||
className={clsx("mr-1 lg:!hidden", buttonProps?.className)}
|
||||
onClick={() => {
|
||||
if (items.top.length !== 0 && items.bottom.length !== 0) {
|
||||
setMobileSidebarOpen((prevValue) => !prevValue)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!mobileSidebarOpen && (
|
||||
<IconSidebar iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
|
||||
)}
|
||||
{mobileSidebarOpen && (
|
||||
<IconXMark iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
|
||||
)}
|
||||
</NavbarIconButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavbarMenuButton
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { useColorMode } from "@/providers/color-mode"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
const NavbarMobileLogo = () => {
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
return (
|
||||
<Link href={`/`} className="flex-1 lg:hidden">
|
||||
<Image
|
||||
src={
|
||||
colorMode === "light"
|
||||
? "/images/logo-mobile.png"
|
||||
: "/images/logo-mobile-dark.png"
|
||||
}
|
||||
alt="Medusa Logo"
|
||||
height={20}
|
||||
width={82}
|
||||
className="mx-auto align-middle"
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavbarMobileLogo
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import NavbarMenuButton from "../MenuButton"
|
||||
import NavbarMobileLogo from "../MobileLogo"
|
||||
import SearchBar from "../../SearchBar"
|
||||
import NavbarColorModeToggle from "../ColorModeToggle"
|
||||
|
||||
const MobileMenu = () => {
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
if (window.innerWidth < 1025 && !isMobile) {
|
||||
setIsMobile(true)
|
||||
} else if (window.innerWidth >= 1025 && isMobile) {
|
||||
setIsMobile(false)
|
||||
}
|
||||
}, [isMobile])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize)
|
||||
}
|
||||
}, [handleResize])
|
||||
|
||||
useEffect(() => {
|
||||
handleResize()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between lg:hidden">
|
||||
{isMobile && (
|
||||
<>
|
||||
<NavbarMenuButton
|
||||
buttonProps={{
|
||||
className:
|
||||
"!border-none !bg-transparent !bg-no-image !shadow-none",
|
||||
}}
|
||||
/>
|
||||
<NavbarMobileLogo />
|
||||
<div className="flex">
|
||||
<SearchBar />
|
||||
<NavbarColorModeToggle
|
||||
buttonProps={{
|
||||
className:
|
||||
"!border-none !bg-transparent !bg-no-image !shadow-none ml-1",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileMenu
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
|
||||
import IconSidebar from "@/components/Icons/Sidebar"
|
||||
import Tooltip from "@/components/Tooltip"
|
||||
import NavbarIconButton from "../IconButton"
|
||||
import { useSidebar } from "../../../providers/sidebar"
|
||||
import clsx from "clsx"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
const NavbarSidebarButton = () => {
|
||||
const { desktopSidebarOpen, setDesktopSidebarOpen } = useSidebar()
|
||||
const [isApple, setIsApple] = useState(false)
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setDesktopSidebarOpen((prevValue) => !prevValue)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsApple(navigator.userAgent.toLowerCase().indexOf("mac") !== 0)
|
||||
function isEditingContent(event: KeyboardEvent) {
|
||||
const element = event.target as HTMLElement
|
||||
const tagName = element.tagName
|
||||
return (
|
||||
element.isContentEditable ||
|
||||
tagName === "INPUT" ||
|
||||
tagName === "SELECT" ||
|
||||
tagName === "TEXTAREA"
|
||||
)
|
||||
}
|
||||
|
||||
function sidebarShortcut(e: KeyboardEvent) {
|
||||
if (
|
||||
(e.metaKey || e.ctrlKey) &&
|
||||
e.key.toLowerCase() === "i" &&
|
||||
!isEditingContent(e)
|
||||
) {
|
||||
e.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", sidebarShortcut)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", sidebarShortcut)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getPlatformKey = () =>
|
||||
`
|
||||
<kbd class="${clsx(
|
||||
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark",
|
||||
"border border-solid rounded-sm border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
|
||||
"text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text font-base text-compact-x-small-plus",
|
||||
"inline-flex !p-0 justify-center items-center shadow-none ml-0.5",
|
||||
isApple && "w-[22px] h-[22px]",
|
||||
!isApple && "w-1.5 h-1.5"
|
||||
)}">${isApple ? "⌘" : "Ctrl"}</kbd>
|
||||
`
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
html={
|
||||
desktopSidebarOpen
|
||||
? `<span class="text-compact-x-small-plus">Close sidebar ${getPlatformKey()}
|
||||
<kbd class="${clsx(
|
||||
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark",
|
||||
"border border-solid rounded-sm border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
|
||||
"text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text font-base text-compact-x-small-plus",
|
||||
"inline-flex w-[22px] h-[22px] !p-0 justify-center items-center shadow-none"
|
||||
)}">I</kbd></span>`
|
||||
: `<span class="text-compact-x-small-plus">Lock sidebar open ${getPlatformKey()}
|
||||
<kbd class="${clsx(
|
||||
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark",
|
||||
"border border-solid rounded-sm border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
|
||||
"text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text font-base text-compact-x-small-plus",
|
||||
"inline-flex w-[22px] h-[22px] !p-0 justify-center items-center shadow-none"
|
||||
)}">I</kbd></span>`
|
||||
}
|
||||
>
|
||||
<NavbarIconButton onClick={toggleSidebar}>
|
||||
<IconSidebar iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
|
||||
</NavbarIconButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavbarSidebarButton
|
||||
@@ -0,0 +1,58 @@
|
||||
import clsx from "clsx"
|
||||
import NavbarLink from "./Link"
|
||||
import NavbarColorModeToggle from "./ColorModeToggle"
|
||||
import NavbarLogo from "./Logo"
|
||||
import SearchBar from "../SearchBar"
|
||||
import NavbarMenuButton from "./MenuButton"
|
||||
import getLinkWithBasePath from "../../utils/get-link-with-base-path"
|
||||
import FeedbackModal from "./FeedbackModal"
|
||||
import NavbarMobileLogo from "./MobileLogo"
|
||||
import MobileMenu from "./MobileMenu"
|
||||
|
||||
const Navbar = () => {
|
||||
return (
|
||||
<nav
|
||||
className={clsx(
|
||||
"h-navbar sticky top-0 w-full justify-between",
|
||||
"bg-docs-bg dark:bg-docs-bg-dark border-medusa-border-base dark:border-medusa-border-base-dark z-[400] border-b"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"h-navbar max-w-xxl py-0.75 sticky top-0 mx-auto flex w-full justify-between px-1 lg:px-3"
|
||||
)}
|
||||
>
|
||||
<div className="hidden w-full items-center gap-0.5 lg:flex lg:w-auto lg:gap-1.5">
|
||||
<NavbarLogo />
|
||||
<div className="hidden items-center gap-1.5 lg:flex">
|
||||
<NavbarLink href="https://docs.medusajs.com/" label="Docs" />
|
||||
<NavbarLink
|
||||
href="https://docs.medusajs.com/user-guide"
|
||||
label="User Guide"
|
||||
/>
|
||||
<NavbarLink
|
||||
href={getLinkWithBasePath("/store")}
|
||||
label="Store API"
|
||||
activeValue="store"
|
||||
/>
|
||||
<NavbarLink
|
||||
href={getLinkWithBasePath("/admin")}
|
||||
label="Admin API"
|
||||
activeValue="admin"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden min-w-0 flex-1 items-center justify-end gap-0.5 lg:flex">
|
||||
<div className="w-[240px] [&>*]:flex-1">
|
||||
<SearchBar />
|
||||
</div>
|
||||
<NavbarColorModeToggle />
|
||||
<FeedbackModal />
|
||||
</div>
|
||||
<MobileMenu />
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import { DocSearch } from "@docsearch/react"
|
||||
|
||||
import "@docsearch/css"
|
||||
|
||||
const SearchBar = () => {
|
||||
return (
|
||||
<DocSearch
|
||||
appId={process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "temp"}
|
||||
indexName={process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME || "temp"}
|
||||
apiKey={process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "temp"}
|
||||
searchParameters={{
|
||||
tagFilters: ["api"],
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchBar
|
||||
@@ -0,0 +1,21 @@
|
||||
import clsx from "clsx"
|
||||
import SectionDivider from "../Divider"
|
||||
import { forwardRef } from "react"
|
||||
|
||||
type SectionContainerProps = {
|
||||
children: React.ReactNode
|
||||
noTopPadding?: boolean
|
||||
}
|
||||
|
||||
const SectionContainer = forwardRef<HTMLDivElement, SectionContainerProps>(
|
||||
function SectionContainer({ children, noTopPadding = false }, ref) {
|
||||
return (
|
||||
<div className={clsx("relative pb-7", !noTopPadding && "pt-7")} ref={ref}>
|
||||
{children}
|
||||
<SectionDivider className="-left-1.5 lg:!-left-4" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default SectionContainer
|
||||
@@ -0,0 +1,18 @@
|
||||
import clsx from "clsx"
|
||||
|
||||
type SectionDividerProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SectionDivider = ({ className }: SectionDividerProps) => {
|
||||
return (
|
||||
<hr
|
||||
className={clsx(
|
||||
"absolute bottom-0 -left-1.5 z-0 m-0 w-screen lg:left-0",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SectionDivider
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import clsx from "clsx"
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
export type SectionProps = {
|
||||
addToSidebar?: boolean
|
||||
} & React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
const Section = ({ children, className }: SectionProps) => {
|
||||
const sectionRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if ("scrollRestoration" in history) {
|
||||
// disable scroll on refresh
|
||||
history.scrollRestoration = "manual"
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={sectionRef}
|
||||
className={clsx("[&_ul]:list-disc [&_ul]:px-1", "[&_h2]:pt-7", className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Section
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { SidebarItemType } from "@/providers/sidebar"
|
||||
import { useSidebar } from "@/providers/sidebar"
|
||||
import clsx from "clsx"
|
||||
import dynamic from "next/dynamic"
|
||||
import Link from "next/link"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import type { MethodLabelProps } from "../../MethodLabel"
|
||||
import checkSidebarItemVisibility from "@/utils/check-sidebar-item-visibility"
|
||||
import Loading from "../../Loading"
|
||||
|
||||
const MethodLabel = dynamic<MethodLabelProps>(
|
||||
async () => import("../../MethodLabel")
|
||||
) as React.FC<MethodLabelProps>
|
||||
|
||||
export type SidebarItemProps = {
|
||||
item: SidebarItemType
|
||||
nested?: boolean
|
||||
} & React.AllHTMLAttributes<HTMLLIElement>
|
||||
|
||||
const SidebarItem = ({ item, nested = false, className }: SidebarItemProps) => {
|
||||
const [showLoading, setShowLoading] = useState(false)
|
||||
const { isItemActive, setMobileSidebarOpen: setSidebarOpen } = useSidebar()
|
||||
const active = useMemo(() => {
|
||||
return isItemActive(item, nested)
|
||||
}, [isItemActive, item, nested])
|
||||
const collapsed = !isItemActive(item, true)
|
||||
const ref = useRef<HTMLLIElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (active && ref.current && window.innerWidth >= 1025) {
|
||||
if (
|
||||
!checkSidebarItemVisibility(ref.current, {
|
||||
topMargin: 57,
|
||||
})
|
||||
) {
|
||||
// scroll to element
|
||||
ref.current.scrollIntoView({
|
||||
block: "center",
|
||||
})
|
||||
}
|
||||
}
|
||||
if (active) {
|
||||
setShowLoading(true)
|
||||
}
|
||||
}, [active])
|
||||
|
||||
return (
|
||||
<li
|
||||
className={clsx(
|
||||
item.hasChildren && !collapsed && "my-1.5",
|
||||
!item.hasChildren && !nested && active && "mt-1.5",
|
||||
((item.hasChildren && !collapsed) ||
|
||||
(!item.hasChildren && !nested && active)) &&
|
||||
"-translate-y-1 transition-transform",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<Link
|
||||
href={item.isPathHref ? item.path : `#${item.path}`}
|
||||
className={clsx(
|
||||
"flex items-center justify-between gap-0.5 rounded-sm border px-0.5 py-[6px] hover:no-underline",
|
||||
!item.hasChildren &&
|
||||
"text-compact-small-plus text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
|
||||
item.hasChildren &&
|
||||
"text-compact-x-small-plus text-medusa-fg-muted dark:text-medusa-fg-muted-dark uppercase",
|
||||
active &&
|
||||
"!text-medusa-fg-base dark:!text-medusa-fg-base-dark bg-medusa-bg-base-pressed dark:bg-medusa-bg-base-pressed-dark",
|
||||
active &&
|
||||
"border-medusa-border-base dark:border-medusa-border-base-dark",
|
||||
!active &&
|
||||
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark border-transparent"
|
||||
)}
|
||||
scroll={true}
|
||||
onClick={() => {
|
||||
if (window.innerWidth < 1025) {
|
||||
setSidebarOpen(false)
|
||||
}
|
||||
}}
|
||||
replace
|
||||
shallow
|
||||
>
|
||||
<span>{item.title}</span>
|
||||
{item.method && <MethodLabel method={item.method} className="h-fit" />}
|
||||
</Link>
|
||||
{item.hasChildren && (
|
||||
<ul
|
||||
className={clsx("ease-ease overflow-hidden", collapsed && "m-0 h-0")}
|
||||
>
|
||||
{showLoading && !item.loaded && (
|
||||
<Loading
|
||||
count={3}
|
||||
className="!mb-0 !px-0.5"
|
||||
barClassName="h-[20px]"
|
||||
/>
|
||||
)}
|
||||
{item.children?.map((childItem, index) => (
|
||||
<SidebarItem item={childItem} key={index} nested={true} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default SidebarItem
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import { useSidebar } from "@/providers/sidebar"
|
||||
import clsx from "clsx"
|
||||
import dynamic from "next/dynamic"
|
||||
import { SidebarItemProps } from "./Item"
|
||||
import Loading from "../Loading"
|
||||
|
||||
const SidebarItem = dynamic<SidebarItemProps>(async () => import("./Item"), {
|
||||
loading: () => <Loading count={1} />,
|
||||
}) as React.FC<SidebarItemProps>
|
||||
|
||||
type SidebarProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Sidebar = ({ className = "" }: SidebarProps) => {
|
||||
const { items, mobileSidebarOpen, desktopSidebarOpen } = useSidebar()
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={clsx(
|
||||
"clip bg-docs-bg dark:bg-docs-bg-dark w-api-ref-sidebar block",
|
||||
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-r border-solid",
|
||||
"fixed -left-full top-[57px] h-screen transition-[left] lg:relative lg:left-0 lg:top-auto lg:h-auto",
|
||||
"lg:w-sidebar z-[100] w-full lg:z-0",
|
||||
mobileSidebarOpen && "!left-0",
|
||||
!desktopSidebarOpen && "!absolute !-left-full",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
animationFillMode: "forwards",
|
||||
}}
|
||||
>
|
||||
<ul
|
||||
className={clsx(
|
||||
"sticky top-[57px] h-screen max-h-screen w-full list-none overflow-auto p-0",
|
||||
"px-1.5 pb-[57px] pt-1.5"
|
||||
)}
|
||||
id="sidebar"
|
||||
>
|
||||
<div className="mb-1.5 lg:hidden">
|
||||
{!items.mobile.length && <Loading className="px-0" />}
|
||||
{items.mobile.map((item, index) => (
|
||||
<SidebarItem item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-1.5">
|
||||
{!items.top.length && <Loading className="px-0" />}
|
||||
{items.top.map((item, index) => (
|
||||
<SidebarItem item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-1.5">
|
||||
{!items.bottom.length && <Loading className="px-0" />}
|
||||
{items.bottom.map((item, index) => (
|
||||
<SidebarItem item={item} key={index} />
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
@@ -0,0 +1,23 @@
|
||||
type SpaceProps = {
|
||||
top?: number
|
||||
bottom?: number
|
||||
left?: number
|
||||
right?: number
|
||||
}
|
||||
|
||||
const Space = ({ top = 0, bottom = 0, left = 0, right = 0 }: SpaceProps) => {
|
||||
return (
|
||||
<div
|
||||
className="w-full"
|
||||
style={{
|
||||
height: `1px`,
|
||||
marginTop: `${top ? top - 1 : top}px`,
|
||||
marginBottom: `${bottom ? bottom - 1 : bottom}px`,
|
||||
marginLeft: `${left}px`,
|
||||
marginRight: `${right}px`,
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Space
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Code } from "@/types/openapi"
|
||||
import CodeTabs from "@/components/CodeTabs"
|
||||
import slugify from "slugify"
|
||||
|
||||
export type TagOperationCodeSectionRequestSamplesProps = {
|
||||
codeSamples: Code[]
|
||||
}
|
||||
|
||||
const TagOperationCodeSectionRequestSamples = ({
|
||||
codeSamples,
|
||||
}: TagOperationCodeSectionRequestSamplesProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h3>Request samples</h3>
|
||||
<CodeTabs
|
||||
tabs={codeSamples.map((codeSample) => ({
|
||||
label: codeSample.label,
|
||||
value: slugify(codeSample.label),
|
||||
code: {
|
||||
...codeSample,
|
||||
collapsed: true,
|
||||
className: "!mb-0",
|
||||
},
|
||||
}))}
|
||||
className="mt-2 !mb-0"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagOperationCodeSectionRequestSamples
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { CodeBlockProps } from "@/components/CodeBlock"
|
||||
import type { ExampleObject, ResponseObject } from "@/types/openapi"
|
||||
import type { JSONSchema7 } from "json-schema"
|
||||
import stringify from "json-stringify-pretty-compact"
|
||||
import dynamic from "next/dynamic"
|
||||
import { sample } from "openapi-sampler"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
|
||||
const CodeBlock = dynamic<CodeBlockProps>(
|
||||
async () => import("../../../../../CodeBlock")
|
||||
) as React.FC<CodeBlockProps>
|
||||
|
||||
export type TagsOperationCodeSectionResponsesSampleProps = {
|
||||
response: ResponseObject
|
||||
} & React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
const TagsOperationCodeSectionResponsesSample = ({
|
||||
response,
|
||||
className,
|
||||
}: TagsOperationCodeSectionResponsesSampleProps) => {
|
||||
const [examples, setExamples] = useState<ExampleObject[]>([])
|
||||
const [selectedExample, setSelectedExample] = useState<
|
||||
ExampleObject | undefined
|
||||
>()
|
||||
|
||||
const initExamples = useCallback(() => {
|
||||
if (!response.content) {
|
||||
return []
|
||||
}
|
||||
const contentSchema = Object.values(response.content)[0]
|
||||
const tempExamples = []
|
||||
if (contentSchema.examples) {
|
||||
Object.entries(contentSchema.examples).forEach(([value, example]) => {
|
||||
if ("$ref" in example) {
|
||||
return []
|
||||
}
|
||||
|
||||
tempExamples.push({
|
||||
title: example.summary || "",
|
||||
value,
|
||||
content: stringify(example.value, {
|
||||
maxLength: 50,
|
||||
}),
|
||||
})
|
||||
})
|
||||
} else if (contentSchema.example) {
|
||||
tempExamples.push({
|
||||
title: "",
|
||||
value: "",
|
||||
content: stringify(contentSchema.example, {
|
||||
maxLength: 50,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
const contentSample = stringify(
|
||||
sample(
|
||||
{
|
||||
...contentSchema.schema,
|
||||
} as JSONSchema7,
|
||||
{
|
||||
skipNonRequired: true,
|
||||
}
|
||||
),
|
||||
{
|
||||
maxLength: 50,
|
||||
}
|
||||
)
|
||||
|
||||
tempExamples.push({
|
||||
title: "",
|
||||
value: "",
|
||||
content: contentSample,
|
||||
})
|
||||
}
|
||||
|
||||
return tempExamples
|
||||
}, [response.content])
|
||||
|
||||
useEffect(() => {
|
||||
const tempExamples = initExamples()
|
||||
setExamples(tempExamples)
|
||||
setSelectedExample(tempExamples[0])
|
||||
}, [initExamples])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={className}>
|
||||
{response.content && (
|
||||
<span>Content type: {Object.keys(response.content)[0]}</span>
|
||||
)}
|
||||
<>
|
||||
{examples.length > 1 && (
|
||||
<select
|
||||
onChange={(event) =>
|
||||
setSelectedExample(
|
||||
examples.find((ex) => ex.value === event.target.value)
|
||||
)
|
||||
}
|
||||
className="border-medusa-border-base dark:border-medusa-border-base-dark my-1 w-full rounded-sm border p-0.5"
|
||||
>
|
||||
{examples.map((example, index) => (
|
||||
<option value={example.value} key={index}>
|
||||
{example.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{selectedExample && (
|
||||
<CodeBlock
|
||||
source={selectedExample.content}
|
||||
lang={getLanguageFromMedia(Object.keys(response.content)[0])}
|
||||
collapsed={true}
|
||||
className="mt-2 mb-0"
|
||||
/>
|
||||
)}
|
||||
{!selectedExample && <>Empty Response</>}
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagsOperationCodeSectionResponsesSample
|
||||
|
||||
const getLanguageFromMedia = (media: string) => {
|
||||
return media.substring(media.indexOf("/"))
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Operation } from "@/types/openapi"
|
||||
import dynamic from "next/dynamic"
|
||||
import type { TagsOperationCodeSectionResponsesSampleProps } from "./Sample"
|
||||
import Badge from "../../../../Badge"
|
||||
|
||||
const TagsOperationCodeSectionResponsesSample =
|
||||
dynamic<TagsOperationCodeSectionResponsesSampleProps>(
|
||||
async () => import("./Sample")
|
||||
) as React.FC<TagsOperationCodeSectionResponsesSampleProps>
|
||||
|
||||
type TagsOperationCodeSectionResponsesProps = {
|
||||
operation: Operation
|
||||
}
|
||||
|
||||
const TagsOperationCodeSectionResponses = ({
|
||||
operation,
|
||||
}: TagsOperationCodeSectionResponsesProps) => {
|
||||
const responseCodes = Object.keys(operation.responses)
|
||||
const responseCode = responseCodes.find((rc) => rc === "200" || rc === "201")
|
||||
const response = responseCode ? operation.responses[responseCode] : null
|
||||
|
||||
if (!response) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-0.5 flex items-center gap-0.5">
|
||||
<h3 className="mb-0">Response </h3>
|
||||
<Badge variant="green">{responseCode}</Badge>
|
||||
</div>
|
||||
|
||||
<TagsOperationCodeSectionResponsesSample response={response} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagsOperationCodeSectionResponses
|
||||
@@ -0,0 +1,53 @@
|
||||
import MethodLabel from "@/components/MethodLabel"
|
||||
import type { Operation } from "@/types/openapi"
|
||||
import TagsOperationCodeSectionResponses from "./Responses"
|
||||
import type { TagOperationCodeSectionRequestSamplesProps } from "./RequestSamples"
|
||||
import dynamic from "next/dynamic"
|
||||
import clsx from "clsx"
|
||||
import CopyButton from "../../../CopyButton"
|
||||
import IconCopyOutline from "../../../Icons/CopyOutline"
|
||||
|
||||
const TagOperationCodeSectionRequestSamples =
|
||||
dynamic<TagOperationCodeSectionRequestSamplesProps>(
|
||||
async () => import("./RequestSamples")
|
||||
) as React.FC<TagOperationCodeSectionRequestSamplesProps>
|
||||
|
||||
export type TagOperationCodeSectionProps = {
|
||||
operation: Operation
|
||||
method: string
|
||||
endpointPath: string
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const TagOperationCodeSection = ({
|
||||
operation,
|
||||
method,
|
||||
endpointPath,
|
||||
className,
|
||||
}: TagOperationCodeSectionProps) => {
|
||||
return (
|
||||
<div className={clsx("mt-2 flex flex-col gap-2", className)}>
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-medusa-bg-subtle dark:bg-medusa-bg-subtle-dark border-medusa-border-base dark:border-medusa-border-base-dark px-0.75 rounded border py-0.5",
|
||||
"text-code-body flex w-full justify-between gap-1"
|
||||
)}
|
||||
>
|
||||
<div className={clsx("flex w-[calc(100%-36px)] gap-1")}>
|
||||
<MethodLabel method={method} className="h-fit" />
|
||||
<code className="break-words break-all">{endpointPath}</code>
|
||||
</div>
|
||||
<CopyButton text={endpointPath} tooltipClassName="font-base">
|
||||
<IconCopyOutline iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
|
||||
</CopyButton>
|
||||
</div>
|
||||
{operation["x-codeSamples"] && (
|
||||
<TagOperationCodeSectionRequestSamples
|
||||
codeSamples={operation["x-codeSamples"]}
|
||||
/>
|
||||
)}
|
||||
<TagsOperationCodeSectionResponses operation={operation} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagOperationCodeSection
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { Parameter, SchemaObject } from "@/types/openapi"
|
||||
import TagOperationParameters from "../../Parameters"
|
||||
|
||||
export type TagsOperationDescriptionSectionParametersProps = {
|
||||
parameters: Parameter[]
|
||||
}
|
||||
|
||||
const TagsOperationDescriptionSectionParameters = ({
|
||||
parameters,
|
||||
}: TagsOperationDescriptionSectionParametersProps) => {
|
||||
const pathParameters: SchemaObject = {
|
||||
type: "object",
|
||||
required: [],
|
||||
properties: {},
|
||||
}
|
||||
const queryParameters: SchemaObject = {
|
||||
type: "object",
|
||||
required: [],
|
||||
properties: {},
|
||||
}
|
||||
|
||||
parameters.forEach((parameter) => {
|
||||
const parameterObject = {
|
||||
...parameter.schema,
|
||||
parameterName: parameter.name,
|
||||
description: parameter.description,
|
||||
example: parameter.example,
|
||||
examples: parameter.examples,
|
||||
}
|
||||
if (parameter.in === "path") {
|
||||
if (parameter.required) {
|
||||
pathParameters.required?.push(parameter.name)
|
||||
}
|
||||
pathParameters.properties[parameter.name] = parameterObject
|
||||
} else if (parameter.in === "query") {
|
||||
if (parameter.required) {
|
||||
queryParameters.required?.push(parameter.name)
|
||||
}
|
||||
queryParameters.properties[parameter.name] = parameterObject
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.values(pathParameters.properties).length > 0 && (
|
||||
<>
|
||||
<h3 className="border-medusa-border-base dark:border-medusa-border-base-dark border-b py-1.5">
|
||||
Path Parameters
|
||||
</h3>
|
||||
<TagOperationParameters
|
||||
schemaObject={pathParameters}
|
||||
topLevel={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{Object.values(queryParameters.properties).length > 0 && (
|
||||
<>
|
||||
<h3 className="border-medusa-border-base dark:border-medusa-border-base-dark border-b py-1.5">
|
||||
Query Parameters
|
||||
</h3>
|
||||
<TagOperationParameters
|
||||
schemaObject={queryParameters}
|
||||
topLevel={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagsOperationDescriptionSectionParameters
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { RequestObject } from "@/types/openapi"
|
||||
import DetailsSummary from "../../../../Details/Summary"
|
||||
import TagOperationParameters from "../../Parameters"
|
||||
|
||||
export type TagsOperationDescriptionSectionRequestProps = {
|
||||
requestBody: RequestObject
|
||||
}
|
||||
|
||||
const TagsOperationDescriptionSectionRequest = ({
|
||||
requestBody,
|
||||
}: TagsOperationDescriptionSectionRequestProps) => {
|
||||
return (
|
||||
<>
|
||||
<DetailsSummary
|
||||
title="Request Body"
|
||||
subtitle={Object.keys(requestBody.content)[0]}
|
||||
expandable={false}
|
||||
className="border-t-0"
|
||||
titleClassName="text-h3"
|
||||
/>
|
||||
<TagOperationParameters
|
||||
schemaObject={
|
||||
requestBody.content[Object.keys(requestBody.content)[0]].schema
|
||||
}
|
||||
topLevel={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagsOperationDescriptionSectionRequest
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { ResponsesObject } from "@/types/openapi"
|
||||
import clsx from "clsx"
|
||||
import Details from "@/components/Details"
|
||||
import DetailsSummary from "@/components/Details/Summary"
|
||||
import Badge from "@/components/Badge"
|
||||
import TagOperationParameters from "../../Parameters"
|
||||
import { Fragment } from "react"
|
||||
|
||||
export type TagsOperationDescriptionSectionResponsesProps = {
|
||||
responses: ResponsesObject
|
||||
}
|
||||
|
||||
const TagsOperationDescriptionSectionResponses = ({
|
||||
responses,
|
||||
}: TagsOperationDescriptionSectionResponsesProps) => {
|
||||
return (
|
||||
<>
|
||||
<h3 className="my-1.5">Responses</h3>
|
||||
<div
|
||||
className={clsx("[&>details:not(:first-of-type)>summary]:border-t-0")}
|
||||
>
|
||||
{Object.entries(responses).map(([code, response], index) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{response.content && (
|
||||
<>
|
||||
{(code === "200" || code === "201") && (
|
||||
<>
|
||||
<DetailsSummary
|
||||
title={`${code} ${response.description}`}
|
||||
subtitle={Object.keys(response.content)[0]}
|
||||
badge={<Badge variant="green">Success</Badge>}
|
||||
expandable={false}
|
||||
className={clsx(
|
||||
index !== 0 && "border-t-0",
|
||||
index === 0 && "border-b-0"
|
||||
)}
|
||||
/>
|
||||
<TagOperationParameters
|
||||
schemaObject={
|
||||
response.content[Object.keys(response.content)[0]]
|
||||
.schema
|
||||
}
|
||||
topLevel={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{code !== "200" && code !== "201" && (
|
||||
<Details
|
||||
summaryElm={
|
||||
<DetailsSummary
|
||||
title={`${code} ${response.description}`}
|
||||
subtitle={Object.keys(response.content)[0]}
|
||||
badge={<Badge variant="red">Error</Badge>}
|
||||
open={index === 0}
|
||||
/>
|
||||
}
|
||||
openInitial={index === 0}
|
||||
className={clsx(index > 1 && "border-t-0")}
|
||||
>
|
||||
<TagOperationParameters
|
||||
schemaObject={
|
||||
response.content[Object.keys(response.content)[0]]
|
||||
.schema
|
||||
}
|
||||
topLevel={true}
|
||||
/>
|
||||
</Details>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!response.content && (
|
||||
<DetailsSummary
|
||||
title={`${code} ${response.description}`}
|
||||
subtitle={"Empty response"}
|
||||
badge={
|
||||
<Badge
|
||||
variant={
|
||||
code === "200" || code === "201" ? "green" : "red"
|
||||
}
|
||||
>
|
||||
{code === "200" || code === "201" ? "Success" : "Error"}
|
||||
</Badge>
|
||||
}
|
||||
expandable={false}
|
||||
className={clsx(
|
||||
index !== 0 && "border-t-0",
|
||||
index === 0 &&
|
||||
Object.entries(responses).length > 1 &&
|
||||
"border-b-0"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagsOperationDescriptionSectionResponses
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useBaseSpecs } from "@/providers/base-specs"
|
||||
import type { OpenAPIV3 } from "openapi-types"
|
||||
import Card from "../../../../Card"
|
||||
|
||||
export type TagsOperationDescriptionSectionSecurityProps = {
|
||||
security: OpenAPIV3.SecurityRequirementObject[]
|
||||
}
|
||||
|
||||
const TagsOperationDescriptionSectionSecurity = ({
|
||||
security,
|
||||
}: TagsOperationDescriptionSectionSecurityProps) => {
|
||||
const { getSecuritySchema } = useBaseSpecs()
|
||||
|
||||
const getDescription = () => {
|
||||
let str = ""
|
||||
security.forEach((item) => {
|
||||
if (str.length) {
|
||||
str += " or "
|
||||
}
|
||||
str += getSecuritySchema(Object.keys(item)[0])?.["x-displayName"]
|
||||
})
|
||||
return str
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-2">
|
||||
<Card
|
||||
title="Authorization"
|
||||
text={getDescription()}
|
||||
href="#authentication"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagsOperationDescriptionSectionSecurity
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { Operation } from "@/types/openapi"
|
||||
import type { TagsOperationDescriptionSectionSecurityProps } from "./Security"
|
||||
import type { TagsOperationDescriptionSectionRequestProps } from "./RequestBody"
|
||||
import type { TagsOperationDescriptionSectionResponsesProps } from "./Responses"
|
||||
import dynamic from "next/dynamic"
|
||||
import TagsOperationDescriptionSectionParameters from "./Parameters"
|
||||
import MDXContentClient from "@/components/MDXContent/Client"
|
||||
import type { BadgeProps } from "../../../Badge"
|
||||
import type { TagsOperationFeatureFlagNoticeProps } from "../FeatureFlagNotice"
|
||||
import type { LinkProps } from "../../../MDXComponents/Link"
|
||||
import Feedback from "../../../Feedback"
|
||||
import { useArea } from "../../../../providers/area"
|
||||
|
||||
const TagsOperationDescriptionSectionSecurity =
|
||||
dynamic<TagsOperationDescriptionSectionSecurityProps>(
|
||||
async () => import("./Security")
|
||||
) as React.FC<TagsOperationDescriptionSectionSecurityProps>
|
||||
|
||||
const TagsOperationDescriptionSectionRequest =
|
||||
dynamic<TagsOperationDescriptionSectionRequestProps>(
|
||||
async () => import("./RequestBody")
|
||||
) as React.FC<TagsOperationDescriptionSectionRequestProps>
|
||||
|
||||
const TagsOperationDescriptionSectionResponses =
|
||||
dynamic<TagsOperationDescriptionSectionResponsesProps>(
|
||||
async () => import("./Responses")
|
||||
) as React.FC<TagsOperationDescriptionSectionResponsesProps>
|
||||
|
||||
const Link = dynamic<LinkProps>(
|
||||
async () => import("../../../MDXComponents/Link")
|
||||
) as React.FC<LinkProps>
|
||||
|
||||
const Badge = dynamic<BadgeProps>(
|
||||
async () => import("../../../Badge")
|
||||
) as React.FC<BadgeProps>
|
||||
|
||||
const TagsOperationFeatureFlagNotice =
|
||||
dynamic<TagsOperationFeatureFlagNoticeProps>(
|
||||
async () => import("../FeatureFlagNotice")
|
||||
) as React.FC<TagsOperationFeatureFlagNoticeProps>
|
||||
|
||||
type TagsOperationDescriptionSectionProps = {
|
||||
operation: Operation
|
||||
}
|
||||
const TagsOperationDescriptionSection = ({
|
||||
operation,
|
||||
}: TagsOperationDescriptionSectionProps) => {
|
||||
const { area } = useArea()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>
|
||||
{operation.summary}
|
||||
{operation.deprecated && (
|
||||
<Badge variant="orange" className="ml-0.5">
|
||||
deprecated
|
||||
</Badge>
|
||||
)}
|
||||
{operation["x-featureFlag"] && (
|
||||
<TagsOperationFeatureFlagNotice
|
||||
featureFlag={operation["x-featureFlag"]}
|
||||
tooltipTextClassName="font-normal text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark"
|
||||
badgeClassName="ml-0.5"
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
<div className="my-1">
|
||||
<MDXContentClient content={operation.description} />
|
||||
</div>
|
||||
<Feedback
|
||||
event="survey_api-ref"
|
||||
extraData={{
|
||||
area,
|
||||
section: operation.summary,
|
||||
}}
|
||||
sectionTitle={operation.summary}
|
||||
className="!my-2"
|
||||
vertical={true}
|
||||
question="Did this endpoint run successfully?"
|
||||
/>
|
||||
{operation.externalDocs && (
|
||||
<>
|
||||
Related guide:{" "}
|
||||
<Link href={operation.externalDocs.url} target="_blank">
|
||||
{operation.externalDocs.description || "Read More"}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{operation.security && (
|
||||
<TagsOperationDescriptionSectionSecurity
|
||||
security={operation.security}
|
||||
/>
|
||||
)}
|
||||
{operation.parameters && (
|
||||
<TagsOperationDescriptionSectionParameters
|
||||
parameters={operation.parameters}
|
||||
/>
|
||||
)}
|
||||
{operation.requestBody && (
|
||||
<TagsOperationDescriptionSectionRequest
|
||||
requestBody={operation.requestBody}
|
||||
/>
|
||||
)}
|
||||
<TagsOperationDescriptionSectionResponses
|
||||
responses={operation.responses}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagsOperationDescriptionSection
|
||||
@@ -0,0 +1,41 @@
|
||||
import Badge from "../../../Badge"
|
||||
import Link from "../../../MDXComponents/Link"
|
||||
import Tooltip from "../../../Tooltip"
|
||||
|
||||
export type TagsOperationFeatureFlagNoticeProps = {
|
||||
featureFlag: string
|
||||
type?: "endpoint" | "parameter"
|
||||
tooltipTextClassName?: string
|
||||
badgeClassName?: string
|
||||
}
|
||||
|
||||
const TagsOperationFeatureFlagNotice = ({
|
||||
featureFlag,
|
||||
type = "endpoint",
|
||||
tooltipTextClassName,
|
||||
badgeClassName,
|
||||
}: TagsOperationFeatureFlagNoticeProps) => {
|
||||
return (
|
||||
<Tooltip
|
||||
tooltipChildren={
|
||||
<span className={tooltipTextClassName}>
|
||||
To use this {type}, make sure to
|
||||
<br />
|
||||
<Link
|
||||
href="https://docs.medusajs.com/development/feature-flags/toggle"
|
||||
target="__blank"
|
||||
>
|
||||
enable its feature flag: <code>{featureFlag}</code>
|
||||
</Link>
|
||||
</span>
|
||||
}
|
||||
clickable
|
||||
>
|
||||
<Badge variant="green" className={badgeClassName}>
|
||||
feature flag
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagsOperationFeatureFlagNotice
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { InlineCodeProps } from "@/components/InlineCode"
|
||||
import MDXContentClient from "@/components/MDXContent/Client"
|
||||
import type { SchemaObject } from "@/types/openapi"
|
||||
import clsx from "clsx"
|
||||
import dynamic from "next/dynamic"
|
||||
import type { LinkProps } from "../../../../MDXComponents/Link"
|
||||
import capitalize from "../../../../../utils/capitalize"
|
||||
import { Fragment } from "react"
|
||||
|
||||
const InlineCode = dynamic<InlineCodeProps>(
|
||||
async () => import("../../../../InlineCode")
|
||||
) as React.FC<InlineCodeProps>
|
||||
|
||||
const Link = dynamic<LinkProps>(
|
||||
async () => import("../../../../MDXComponents/Link")
|
||||
) as React.FC<LinkProps>
|
||||
|
||||
type TagOperationParametersDescriptionProps = {
|
||||
schema: SchemaObject
|
||||
}
|
||||
|
||||
const TagOperationParametersDescription = ({
|
||||
schema,
|
||||
}: TagOperationParametersDescriptionProps) => {
|
||||
let typeDescription: React.ReactNode = <></>
|
||||
switch (true) {
|
||||
case schema.type === "object":
|
||||
typeDescription = (
|
||||
<>
|
||||
{schema.type} {schema.title ? `(${schema.title})` : ""}
|
||||
{schema.nullable ? ` or null` : ""}
|
||||
</>
|
||||
)
|
||||
break
|
||||
case schema.type === "array":
|
||||
typeDescription = (
|
||||
<>
|
||||
{schema.type === "array" && formatArrayDescription(schema.items)}
|
||||
{schema.nullable ? ` or null` : ""}
|
||||
</>
|
||||
)
|
||||
break
|
||||
case schema.anyOf !== undefined:
|
||||
case schema.allOf !== undefined:
|
||||
typeDescription = (
|
||||
<>
|
||||
{formatUnionDescription(schema.allOf)}
|
||||
{schema.nullable ? ` or null` : ""}
|
||||
</>
|
||||
)
|
||||
break
|
||||
case schema.oneOf !== undefined:
|
||||
typeDescription = (
|
||||
<>
|
||||
{schema.oneOf?.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
{index !== 0 && <> or </>}
|
||||
{item.type !== "array" && <>{item.title || item.type}</>}
|
||||
{item.type === "array" && (
|
||||
<>array{item.items.type ? ` of ${item.items.type}s` : ""}</>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
{schema.nullable ? ` or null` : ""}
|
||||
</>
|
||||
)
|
||||
break
|
||||
default:
|
||||
typeDescription = (
|
||||
<>
|
||||
{schema.type}
|
||||
{schema.nullable ? ` or null` : ""}
|
||||
{schema.format ? ` <${schema.format}>` : ""}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={clsx("w-2/3 break-words pb-0.5")}>
|
||||
{typeDescription}
|
||||
{schema.default !== undefined && (
|
||||
<>
|
||||
<br />
|
||||
<span>
|
||||
Default:{" "}
|
||||
<InlineCode className="break-words">
|
||||
{JSON.stringify(schema.default)}
|
||||
</InlineCode>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{schema.enum && (
|
||||
<>
|
||||
<br />
|
||||
<span>
|
||||
Enum:{" "}
|
||||
{schema.enum.map((value, index) => (
|
||||
<Fragment key={index}>
|
||||
{index !== 0 && <>, </>}
|
||||
<InlineCode key={index}>{JSON.stringify(value)}</InlineCode>
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{schema.example !== undefined && (
|
||||
<>
|
||||
<br />
|
||||
<span>
|
||||
Example:{" "}
|
||||
<InlineCode className="break-words">
|
||||
{JSON.stringify(schema.example)}
|
||||
</InlineCode>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{schema.description && (
|
||||
<>
|
||||
<br />
|
||||
<MDXContentClient
|
||||
content={capitalize(schema.description)}
|
||||
className={clsx("!mb-0 [&>*]:!mb-0")}
|
||||
scope={{
|
||||
addToSidebar: false,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{schema.externalDocs && (
|
||||
<>
|
||||
Related guide:{" "}
|
||||
<Link href={schema.externalDocs.url} target="_blank">
|
||||
{schema.externalDocs.description || "Read More"}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagOperationParametersDescription
|
||||
|
||||
function formatArrayDescription(schema?: SchemaObject) {
|
||||
if (!schema) {
|
||||
return "Array"
|
||||
}
|
||||
|
||||
const type =
|
||||
schema.type === "object"
|
||||
? `objects ${schema.title ? `(${schema.title})` : ""}`
|
||||
: `${schema.type || "object"}s`
|
||||
|
||||
return `Array of ${type}`
|
||||
}
|
||||
|
||||
function formatUnionDescription(arr?: SchemaObject[]) {
|
||||
const types = [...new Set(arr?.map((type) => type.type || "object"))]
|
||||
return <>{types.join(" or ")}</>
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { SchemaObject } from "@/types/openapi"
|
||||
import dynamic from "next/dynamic"
|
||||
import type { BadgeProps } from "../../../../Badge"
|
||||
import type { TooltipProps } from "../../../../Tooltip"
|
||||
import type { TagsOperationFeatureFlagNoticeProps } from "../../FeatureFlagNotice"
|
||||
import { LinkProps } from "../../../../MDXComponents/Link"
|
||||
|
||||
const Badge = dynamic<BadgeProps>(
|
||||
async () => import("../../../../Badge")
|
||||
) as React.FC<BadgeProps>
|
||||
|
||||
const Tooltip = dynamic<TooltipProps>(
|
||||
async () => import("../../../../Tooltip")
|
||||
) as React.FC<TooltipProps>
|
||||
|
||||
const TagsOperationFeatureFlagNotice =
|
||||
dynamic<TagsOperationFeatureFlagNoticeProps>(
|
||||
async () => import("../../FeatureFlagNotice")
|
||||
) as React.FC<TagsOperationFeatureFlagNoticeProps>
|
||||
|
||||
const Link = dynamic<LinkProps>(
|
||||
async () => import("../../../../MDXComponents/Link")
|
||||
) as React.FC<LinkProps>
|
||||
|
||||
export type TagOperationParametersNameProps = {
|
||||
name: string
|
||||
isRequired?: boolean
|
||||
schema: SchemaObject
|
||||
}
|
||||
|
||||
const TagOperationParametersName = ({
|
||||
name,
|
||||
isRequired,
|
||||
schema,
|
||||
}: TagOperationParametersNameProps) => {
|
||||
return (
|
||||
<span className="w-1/3 break-words pr-0.5">
|
||||
<span className="font-monospace">{name}</span>
|
||||
{schema.deprecated && (
|
||||
<Badge variant="orange" className="ml-1">
|
||||
deprecated
|
||||
</Badge>
|
||||
)}
|
||||
{schema["x-expandable"] && (
|
||||
<>
|
||||
<br />
|
||||
<Tooltip
|
||||
tooltipChildren={
|
||||
<>
|
||||
If this request accepts an <code>expand</code> parameter,
|
||||
<br /> this field can be{" "}
|
||||
<Link href="#expanding-fields">expanded</Link> into an object.
|
||||
</>
|
||||
}
|
||||
clickable
|
||||
>
|
||||
<Badge variant="blue">expandable</Badge>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
{schema["x-featureFlag"] && (
|
||||
<>
|
||||
<br />
|
||||
<TagsOperationFeatureFlagNotice
|
||||
featureFlag={schema["x-featureFlag"]}
|
||||
type="parameter"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isRequired && (
|
||||
<>
|
||||
<br />
|
||||
<span className="text-medusa-tag-red-text text-compact-x-small">
|
||||
required
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagOperationParametersName
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user