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:
Shahed Nasser
2023-08-15 18:07:54 +03:00
committed by GitHub
parent 16249ec280
commit 914d773d3a
3270 changed files with 22075 additions and 192064 deletions
@@ -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
@@ -0,0 +1,24 @@
import clsx from "clsx"
export type TagsOperationParametersNestedProps =
React.HTMLAttributes<HTMLDivElement>
const TagsOperationParametersNested = ({
children,
...props
}: TagsOperationParametersNestedProps) => {
return (
<div
{...props}
className={clsx(
props.className,
"bg-docs-bg-surface dark:bg-docs-bg-surface-dark px-1 pt-1",
"border-medusa-border-base dark:border-medusa-border-base-dark my-1 rounded-sm border"
)}
>
{children}
</div>
)
}
export default TagsOperationParametersNested
@@ -0,0 +1,44 @@
import type { SchemaObject } from "@/types/openapi"
import clsx from "clsx"
import type { TagOperationParametersProps } from ".."
import dynamic from "next/dynamic"
import Loading from "@/components/Loading"
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import(".."),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersProps>
type TagsOperationParametersSectionProps = {
header?: string
contentType?: string
schema: SchemaObject
}
const TagsOperationParametersSection = ({
header,
contentType,
schema,
}: TagsOperationParametersSectionProps) => {
return (
<>
{header && (
<h3
className={clsx(!contentType && "my-2", contentType && "mt-2 mb-0")}
>
{header}
</h3>
)}
{contentType && (
<span className={clsx("mb-2 inline-block")}>
Content type: {contentType}
</span>
)}
<TagOperationParameters schemaObject={schema} topLevel={true} />
</>
)
}
export default TagsOperationParametersSection
@@ -0,0 +1,78 @@
import type { SchemaObject } from "@/types/openapi"
import dynamic from "next/dynamic"
import type { TagOperationParametersDefaultProps } from "../Default"
import type { TagOperationParametersProps } from "../.."
import Details from "@/components/Details"
import TagsOperationParametersNested from "../../Nested"
import Loading from "@/components/Loading"
const TagOperationParametersDefault =
dynamic<TagOperationParametersDefaultProps>(
async () => import("../Default"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersDefaultProps>
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import("../.."),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersProps>
export type TagOperationParametersArrayProps = {
name: string
schema: SchemaObject
isRequired?: boolean
}
const TagOperationParametersArray = ({
name,
schema,
isRequired,
}: TagOperationParametersArrayProps) => {
if (schema.type !== "array") {
return <></>
}
if (
!schema.items ||
(schema.items.type !== "object" &&
schema.items.type !== "array" &&
schema.items.type !== undefined) ||
(schema.items.type === "object" &&
!schema.items.properties &&
!schema.items.allOf &&
!schema.items.anyOf &&
!schema.items.oneOf)
) {
return (
<TagOperationParametersDefault
name={name}
schema={schema}
isRequired={isRequired}
/>
)
}
return (
<Details
summaryContent={
<TagOperationParametersDefault
name={name}
schema={schema}
isRequired={isRequired}
expandable={true}
/>
}
className="!border-y-0"
>
<TagsOperationParametersNested>
<TagOperationParameters schemaObject={schema.items} topLevel={true} />
</TagsOperationParametersNested>
</Details>
)
}
export default TagOperationParametersArray
@@ -0,0 +1,42 @@
import type { SchemaObject } from "@/types/openapi"
import TagOperationParametersDescription from "../../Description"
import clsx from "clsx"
import TagOperationParametersName from "../../Name"
export type TagOperationParametersDefaultProps = {
name?: string
schema: SchemaObject
isRequired?: boolean
className?: string
expandable?: boolean
}
const TagOperationParametersDefault = ({
name,
schema,
isRequired,
className,
expandable = false,
}: TagOperationParametersDefaultProps) => {
return (
<div
className={clsx(
"my-0.5 inline-flex justify-between",
expandable && "w-[calc(100%-16px)]",
!expandable && "w-full pl-1",
className
)}
>
{name && (
<TagOperationParametersName
name={name}
isRequired={isRequired}
schema={schema}
/>
)}
<TagOperationParametersDescription schema={schema} />
</div>
)
}
export default TagOperationParametersDefault
@@ -0,0 +1,131 @@
import type { SchemaObject } from "@/types/openapi"
import TagOperationParametersDefault from "../Default"
import dynamic from "next/dynamic"
import type { TagOperationParametersProps } from "../.."
import type { TagsOperationParametersNestedProps } from "../../Nested"
import type { DetailsProps } from "@/components/Details"
import checkRequired from "@/utils/check-required"
import Loading from "@/components/Loading"
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import("../.."),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersProps>
const TagsOperationParametersNested =
dynamic<TagsOperationParametersNestedProps>(
async () => import("../../Nested"),
{
loading: () => <Loading />,
}
) as React.FC<TagsOperationParametersNestedProps>
const Details = dynamic<DetailsProps>(
async () => import("../../../../../Details"),
{
loading: () => <Loading />,
}
) as React.FC<DetailsProps>
export type TagOperationParametersObjectProps = {
name?: string
schema: SchemaObject
isRequired?: boolean
topLevel?: boolean
}
const TagOperationParametersObject = ({
name,
schema,
isRequired,
topLevel = false,
}: TagOperationParametersObjectProps) => {
if (
(schema.type !== "object" && schema.type !== undefined) ||
(!schema.properties && !name)
) {
return <></>
}
const getPropertyDescriptionElm = (expandable = false) => {
return (
<TagOperationParametersDefault
name={name}
schema={schema}
isRequired={isRequired}
expandable={expandable}
/>
)
}
const getPropertyParameterElms = (isNested = false) => {
// sort properties to show required fields first
const sortedProperties = Object.keys(schema.properties).sort(
(property1, property2) => {
schema.properties[property1].isRequired = checkRequired(
schema,
property1
)
schema.properties[property2].isRequired = checkRequired(
schema,
property2
)
return schema.properties[property1].isRequired &&
schema.properties[property2].isRequired
? 0
: schema.properties[property1].isRequired
? -1
: 1
}
)
const content = (
<>
{sortedProperties.map((property, index) => (
<TagOperationParameters
schemaObject={{
...schema.properties[property],
parameterName: property,
}}
key={index}
isRequired={
schema.properties[property].isRequired ||
checkRequired(schema, property)
}
/>
))}
</>
)
return (
<>
{isNested && (
<TagsOperationParametersNested>
{content}
</TagsOperationParametersNested>
)}
{!isNested && <div>{content}</div>}
</>
)
}
if (!schema.properties) {
return getPropertyDescriptionElm()
}
if (topLevel) {
return getPropertyParameterElms()
}
return (
<Details
summaryContent={getPropertyDescriptionElm(true)}
className="!border-y-0"
>
{getPropertyParameterElms(true)}
</Details>
)
}
export default TagOperationParametersObject
@@ -0,0 +1,127 @@
import type { SchemaObject } from "@/types/openapi"
import clsx from "clsx"
import dynamic from "next/dynamic"
import { useState } from "react"
import Details from "@/components/Details"
import type { TagOperationParametersDefaultProps } from "../Default"
import type { TagsOperationParametersNestedProps } from "../../Nested"
import type { TagOperationParametersProps } from "../.."
import Loading from "@/components/Loading"
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import("../.."),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersProps>
const TagOperationParametersDefault =
dynamic<TagOperationParametersDefaultProps>(
async () => import("../Default"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersDefaultProps>
const TagsOperationParametersNested =
dynamic<TagsOperationParametersNestedProps>(
async () => import("../../Nested"),
{
loading: () => <Loading />,
}
) as React.FC<TagsOperationParametersNestedProps>
export type TagOperationParamatersOneOfProps = {
schema: SchemaObject
isRequired?: boolean
isNested?: boolean
}
const TagOperationParamatersOneOf = ({
schema,
isRequired = false,
isNested = false,
}: TagOperationParamatersOneOfProps) => {
const [activeTab, setActiveTab] = useState<number>(0)
const getName = (item: SchemaObject): string => {
if (item.title) {
return item.title
}
if (item.anyOf || item.allOf) {
// return the name of any of the items
const name = item.anyOf
? item.anyOf.find((i) => i.title !== undefined)?.title
: item.allOf?.find((i) => i.title !== undefined)?.title
if (name) {
return name
}
}
return item.type || ""
}
const getContent = () => {
return (
<>
<div className={clsx("flex items-center gap-1 pl-1")}>
<span className="inline-block">One of</span>
<ul className="mb-0 flex list-none gap-1">
{schema.oneOf?.map((item, index) => (
<li
key={index}
className={clsx(
"rounded-xs cursor-pointer p-0.5",
"border border-solid",
activeTab === index &&
"bg-medusa-bg-subtle dark:bg-medusa-bg-subtle-dark border-medusa-border-strong dark:border-medusa-border-strong-dark",
activeTab !== index &&
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark border-medusa-border-base dark:border-medusa-border-base-dark"
)}
onClick={() => setActiveTab(index)}
>
{getName(item)}
</li>
))}
</ul>
</div>
{schema.oneOf && (
<>
<TagOperationParameters
schemaObject={schema.oneOf[activeTab]}
topLevel={true}
/>
</>
)}
</>
)
}
return (
<>
{isNested && (
<Details
summaryContent={
<TagOperationParametersDefault
schema={schema}
name={schema.parameterName || schema.title || ""}
isRequired={isRequired}
expandable={true}
/>
}
className="!border-y-0"
>
<TagsOperationParametersNested>
{getContent()}
</TagsOperationParametersNested>
</Details>
)}
{!isNested && getContent()}
</>
)
}
export default TagOperationParamatersOneOf
@@ -0,0 +1,65 @@
import type { SchemaObject } from "@/types/openapi"
import dynamic from "next/dynamic"
import type { TagOperationParametersDefaultProps } from "../Default"
import { TagOperationParametersObjectProps } from "../Object"
import Loading from "@/components/Loading"
const TagOperationParametersObject = dynamic<TagOperationParametersObjectProps>(
async () => import("../Object"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersObjectProps>
const TagOperationParametersDefault =
dynamic<TagOperationParametersDefaultProps>(
async () => import("../Default"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersDefaultProps>
export type TagOperationParametersUnionProps = {
name: string
schema: SchemaObject
isRequired?: boolean
topLevel?: boolean
}
const TagOperationParametersUnion = ({
name,
schema,
isRequired,
topLevel,
}: TagOperationParametersUnionProps) => {
const objectSchema = schema.anyOf
? schema.anyOf.find((item) => item.type === "object" && item.properties)
: schema.allOf?.find((item) => item.type === "object" && item.properties)
if (!objectSchema) {
return (
<TagOperationParametersDefault
schema={schema}
name={name}
isRequired={isRequired}
/>
)
}
if (!objectSchema.description) {
objectSchema.description = schema.anyOf
? schema.anyOf.find((item) => item.description !== undefined)?.description
: schema.allOf?.find((item) => item.description !== undefined)
?.description
}
return (
<TagOperationParametersObject
schema={objectSchema}
name={name}
topLevel={topLevel}
/>
)
}
export default TagOperationParametersUnion
@@ -0,0 +1,116 @@
import type { SchemaObject } from "@/types/openapi"
import dynamic from "next/dynamic"
import type { TagOperationParametersObjectProps } from "./Types/Object"
import type { TagOperationParametersDefaultProps } from "./Types/Default"
import type { TagOperationParametersArrayProps } from "./Types/Array"
import type { TagOperationParametersUnionProps } from "./Types/Union"
import type { TagOperationParamatersOneOfProps } from "./Types/OneOf"
import checkRequired from "@/utils/check-required"
import Loading from "@/components/Loading"
const TagOperationParametersObject = dynamic<TagOperationParametersObjectProps>(
async () => import("./Types/Object"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersObjectProps>
const TagOperationParametersDefault =
dynamic<TagOperationParametersDefaultProps>(
async () => import("./Types/Default"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersDefaultProps>
const TagOperationParametersArray = dynamic<TagOperationParametersArrayProps>(
async () => import("./Types/Array"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersArrayProps>
const TagOperationParametersUnion = dynamic<TagOperationParametersUnionProps>(
async () => import("./Types/Union"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersUnionProps>
const TagOperationParamatersOneOf = dynamic<TagOperationParamatersOneOfProps>(
async () => import("./Types/OneOf"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParamatersOneOfProps>
export type TagOperationParametersProps = {
schemaObject: SchemaObject
topLevel?: boolean
className?: string
isRequired?: boolean
}
const TagOperationParameters = ({
schemaObject,
className,
topLevel = false,
isRequired: originalIsRequired = false,
}: TagOperationParametersProps) => {
const isRequired =
originalIsRequired || checkRequired(schemaObject, schemaObject.title)
const propertyName = schemaObject.parameterName || schemaObject.title || ""
const getElement = () => {
if (schemaObject.anyOf || schemaObject.allOf) {
return (
<TagOperationParametersUnion
schema={schemaObject}
name={propertyName}
isRequired={isRequired}
topLevel={topLevel}
/>
)
}
if (schemaObject.oneOf) {
return (
<TagOperationParamatersOneOf
schema={schemaObject}
isNested={!topLevel}
/>
)
}
if (schemaObject.type === "array") {
return (
<TagOperationParametersArray
name={propertyName}
schema={schemaObject}
isRequired={isRequired}
/>
)
}
if (schemaObject.type === "object" || !schemaObject.type) {
return (
<TagOperationParametersObject
name={propertyName}
schema={schemaObject}
topLevel={topLevel}
isRequired={isRequired}
/>
)
}
return (
<TagOperationParametersDefault
schema={schemaObject}
name={propertyName}
isRequired={isRequired}
/>
)
return <></>
}
return <div className={className}>{getElement()}</div>
}
export default TagOperationParameters
@@ -0,0 +1,126 @@
"use client"
import type { Operation } from "@/types/openapi"
import clsx from "clsx"
import type { OpenAPIV3 } from "openapi-types"
import getSectionId from "@/utils/get-section-id"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import dynamic from "next/dynamic"
import { useInView } from "react-intersection-observer"
import { useSidebar } from "@/providers/sidebar"
import type { TagOperationCodeSectionProps } from "./CodeSection"
import TagsOperationDescriptionSection from "./DescriptionSection"
import DividedLayout from "@/layouts/Divided"
import { useLoading } from "@/providers/loading"
import SectionDivider from "../../Section/Divider"
const TagOperationCodeSection = dynamic<TagOperationCodeSectionProps>(
async () => import("./CodeSection")
) as React.FC<TagOperationCodeSectionProps>
export type TagOperationProps = {
operation: Operation
method?: string
tag: OpenAPIV3.TagObject
endpointPath: string
className?: string
}
const TagOperation = ({
operation,
method,
endpointPath,
className,
}: TagOperationProps) => {
const { setActivePath } = useSidebar()
const [show, setShow] = useState(false)
const path = useMemo(
() => getSectionId([...(operation.tags || []), operation.operationId]),
[operation]
)
const nodeRef = useRef<Element | null>(null)
const { loading, removeLoading } = useLoading()
const { ref } = useInView({
threshold: 0.3,
rootMargin: `112px 0px 112px 0px`,
onChange: (changedInView) => {
if (changedInView) {
if (!show) {
if (loading) {
removeLoading()
}
setShow(true)
}
// can't use next router as it doesn't support
// changing url without scrolling
history.replaceState({}, "", `#${path}`)
setActivePath(path)
}
},
})
// Use `useCallback` so we don't recreate the function on each render
const setRefs = useCallback(
(node: Element | null) => {
// Ref's from useRef needs to have the node assigned to `current`
nodeRef.current = node
// Callback refs, like the one from `useInView`, is a function that takes the node as an argument
ref(node)
},
[ref]
)
useEffect(() => {
const enableShow = () => {
setShow(true)
}
if (nodeRef && nodeRef.current) {
removeLoading()
const currentHash = location.hash.replace("#", "")
if (currentHash === path) {
setTimeout(() => {
nodeRef.current?.scrollIntoView()
enableShow()
}, 100)
} else if (currentHash.split("_")[0] === path.split("_")[0]) {
enableShow()
}
}
}, [nodeRef, path])
return (
<div
className={clsx("relative min-h-screen w-full pb-7", className)}
id={path}
ref={setRefs}
>
<div
className={clsx(
"flex w-full justify-between gap-1 opacity-0",
!show && "invisible",
show && "animate-fadeIn"
)}
style={{
animationFillMode: "forwards",
}}
>
<DividedLayout
mainContent={
<TagsOperationDescriptionSection operation={operation} />
}
codeContent={
<TagOperationCodeSection
method={method || ""}
operation={operation}
endpointPath={endpointPath}
/>
}
/>
</div>
<SectionDivider />
</div>
)
}
export default TagOperation
@@ -0,0 +1,102 @@
"use client"
import getSectionId from "@/utils/get-section-id"
import fetcher from "@/utils/swr-fetcher"
import type { OpenAPIV3 } from "openapi-types"
import useSWR from "swr"
import type { Operation, PathsObject } from "@/types/openapi"
import type { SidebarItemType } from "@/providers/sidebar"
import { SidebarItemSections, useSidebar } from "@/providers/sidebar"
import { Fragment, useEffect, useMemo } from "react"
import dynamic from "next/dynamic"
import type { TagOperationProps } from "../Operation"
import { useArea } from "@/providers/area"
import getLinkWithBasePath from "@/utils/get-link-with-base-path"
import clsx from "clsx"
import { useBaseSpecs } from "@/providers/base-specs"
import getTagChildSidebarItems from "@/utils/get-tag-child-sidebar-items"
import { useLoading } from "@/providers/loading"
import DividedLoading from "@/components/Loading/Divided"
const TagOperation = dynamic<TagOperationProps>(
async () => import("../Operation")
) as React.FC<TagOperationProps>
export type TagPathsProps = {
tag: OpenAPIV3.TagObject
} & React.HTMLAttributes<HTMLDivElement>
const TagPaths = ({ tag, className }: TagPathsProps) => {
const tagSlugName = useMemo(() => getSectionId([tag.name]), [tag])
const { area } = useArea()
const { items, addItems, findItemInSection } = useSidebar()
const { baseSpecs } = useBaseSpecs()
const { loading } = useLoading()
// if paths are already loaded since through
// the expanded field, they're loaded directly
// otherwise, they're loaded using the API endpoint
let paths: PathsObject =
baseSpecs?.expandedTags &&
Object.hasOwn(baseSpecs.expandedTags, tagSlugName)
? baseSpecs.expandedTags[tagSlugName]
: {}
const { data } = useSWR<{
paths: PathsObject
}>(
!Object.keys(paths).length
? getLinkWithBasePath(`/tag?tagName=${tagSlugName}&area=${area}`)
: null,
fetcher,
{
errorRetryInterval: 2000,
}
)
paths = data?.paths || paths
useEffect(() => {
if (paths) {
const parentItem = findItemInSection(
items[SidebarItemSections.BOTTOM],
tagSlugName,
false
)
if (!parentItem?.children?.length) {
const items: SidebarItemType[] = getTagChildSidebarItems(paths)
addItems(items, {
section: SidebarItemSections.BOTTOM,
parent: {
path: tagSlugName,
changeLoaded: true,
},
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paths])
return (
<div className={clsx("relative", className)}>
{loading && <DividedLoading className="mt-7" />}
{Object.entries(paths).map(([endpointPath, operations], pathIndex) => (
<Fragment key={pathIndex}>
{Object.entries(operations).map(
([method, operation], operationIndex) => (
<TagOperation
method={method}
operation={operation as Operation}
tag={tag}
key={`${pathIndex}-${operationIndex}`}
endpointPath={endpointPath}
className={clsx("pt-7")}
/>
)
)}
</Fragment>
))}
</div>
)
}
export default TagPaths
@@ -0,0 +1,128 @@
"use client"
import getSectionId from "@/utils/get-section-id"
import type { OpenAPIV3 } from "openapi-types"
import { useInView } from "react-intersection-observer"
import { useEffect, useMemo, useState } from "react"
import { useSidebar } from "@/providers/sidebar"
import dynamic from "next/dynamic"
import Loading from "@/components/Loading"
import type { SectionProps } from "../../Section"
import type { MDXContentClientProps } from "../../MDXContent/Client"
import TagPaths from "../Paths"
import DividedLayout from "@/layouts/Divided"
import LoadingProvider from "@/providers/loading"
import type { LinkProps } from "../../MDXComponents/Link"
import SectionContainer from "../../Section/Container"
import Feedback from "../../Feedback"
import { useArea } from "../../../providers/area"
import SectionDivider from "../../Section/Divider"
import clsx from "clsx"
export type TagSectionProps = {
tag: OpenAPIV3.TagObject
} & React.HTMLAttributes<HTMLDivElement>
const Section = dynamic<SectionProps>(
async () => import("../../Section")
) as React.FC<SectionProps>
const Link = dynamic<LinkProps>(
async () => import("../../MDXComponents/Link")
) as React.FC<LinkProps>
const MDXContentClient = dynamic<MDXContentClientProps>(
async () => import("../../MDXContent/Client"),
{
loading: () => <Loading />,
}
) as React.FC<MDXContentClientProps>
const TagSection = ({ tag }: TagSectionProps) => {
const { setActivePath } = useSidebar()
const [loadPaths, setLoadPaths] = useState(false)
const slugTagName = useMemo(() => getSectionId([tag.name]), [tag])
const { area } = useArea()
const { ref } = useInView({
threshold: 0.5,
rootMargin: `112px 0px 112px 0px`,
onChange: (inView) => {
if (inView && !loadPaths) {
setLoadPaths(true)
}
if (inView) {
// ensure that the hash link doesn't change if it links to an inner path
const currentHashArr = location.hash.replace("#", "").split("_")
if (currentHashArr.length < 2 || currentHashArr[0] !== slugTagName) {
// can't use next router as it doesn't support
// changing url without scrolling
history.replaceState({}, "", `#${slugTagName}`)
setActivePath(slugTagName)
}
}
},
})
useEffect(() => {
if (location.hash && location.hash.includes(slugTagName)) {
const tagName = location.hash.replace("#", "").split("_")
if (tagName.length === 1 && tagName[0] === slugTagName) {
const elm = document.getElementById(tagName[0]) as Element
elm?.scrollIntoView()
} else if (tagName.length > 1 && tagName[0] === slugTagName) {
setLoadPaths(true)
}
}
}, [slugTagName])
return (
<div
className={clsx("min-h-screen", !loadPaths && "relative")}
id={slugTagName}
>
<DividedLayout
ref={ref}
mainContent={
<SectionContainer>
<h2>{tag.name}</h2>
{tag.description && (
<Section>
<MDXContentClient
content={tag.description}
scope={{
addToSidebar: false,
}}
/>
</Section>
)}
{tag.externalDocs && (
<>
Related guide:{" "}
<Link href={tag.externalDocs.url} target="_blank">
{tag.externalDocs.description || "Read More"}
</Link>
</>
)}
<Feedback
event="survey_api-ref"
extraData={{
area,
section: tag.name,
}}
sectionTitle={tag.name}
/>
</SectionContainer>
}
codeContent={<></>}
/>
{loadPaths && (
<LoadingProvider initialLoading={true}>
<TagPaths tag={tag} />
</LoadingProvider>
)}
{!loadPaths && <SectionDivider />}
</div>
)
}
export default TagSection
+106
View File
@@ -0,0 +1,106 @@
"use client"
import type { OpenAPIV3 } from "openapi-types"
import { useEffect, useState } from "react"
import useSWR from "swr"
import fetcher from "@/utils/swr-fetcher"
import { useBaseSpecs } from "@/providers/base-specs"
import dynamic from "next/dynamic"
import type { TagSectionProps } from "./Section"
import { useArea } from "@/providers/area"
import getLinkWithBasePath from "@/utils/get-link-with-base-path"
import { SidebarItemSections, useSidebar } from "@/providers/sidebar"
import getSectionId from "@/utils/get-section-id"
import { ExpandedDocument } from "@/types/openapi"
import getTagChildSidebarItems from "@/utils/get-tag-child-sidebar-items"
import { useNavbar } from "@/providers/navbar"
const TagSection = dynamic<TagSectionProps>(
async () => import("./Section")
) as React.FC<TagSectionProps>
export type TagsProps = React.HTMLAttributes<HTMLDivElement>
function getCurrentTag() {
return typeof location !== "undefined"
? location.hash.replace("#", "").split("_")[0]
: ""
}
const Tags = () => {
const [tags, setTags] = useState<OpenAPIV3.TagObject[]>([])
const [loadData, setLoadData] = useState<boolean>(false)
const [expand, setExpand] = useState<string>("")
const { baseSpecs, setBaseSpecs } = useBaseSpecs()
const { addItems } = useSidebar()
const { area } = useArea()
const { activeItem, setActiveItem } = useNavbar()
const { data } = useSWR<ExpandedDocument>(
loadData && !baseSpecs
? getLinkWithBasePath(`/base-specs?area=${area}&expand=${expand}`)
: null,
fetcher,
{
errorRetryInterval: 2000,
}
)
useEffect(() => {
setExpand(getCurrentTag())
}, [])
useEffect(() => {
if (activeItem !== area) {
setActiveItem(area)
}
}, [activeItem, setActiveItem, area])
useEffect(() => {
setLoadData(true)
}, [expand])
useEffect(() => {
if (data) {
setBaseSpecs(data)
}
if (data?.tags) {
setTags(data.tags)
}
}, [data, setBaseSpecs])
useEffect(() => {
if (baseSpecs) {
addItems(
baseSpecs.tags?.map((tag) => {
const tagPathName = getSectionId([tag.name.toLowerCase()])
const childItems =
baseSpecs.expandedTags &&
Object.hasOwn(baseSpecs.expandedTags, tagPathName)
? getTagChildSidebarItems(baseSpecs.expandedTags[tagPathName])
: []
return {
path: tagPathName,
title: tag.name,
children: childItems,
loaded: childItems.length > 0,
hasChildren: true,
}
}) || [],
{
section: SidebarItemSections.BOTTOM,
}
)
}
}, [baseSpecs, addItems])
return (
<>
{tags.map((tag, index) => (
<TagSection tag={tag} key={index} />
))}
</>
)
}
export default Tags
@@ -0,0 +1,32 @@
import React from "react"
import clsx from "clsx"
type TextAreaProps = {
className?: string
} & React.DetailedHTMLProps<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
>
const TextArea = (props: TextAreaProps) => {
return (
<textarea
{...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",
"pt-0.4 px-0.75 text-medium font-base pb-[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",
props.className
)}
/>
)
}
export default TextArea
@@ -0,0 +1,64 @@
import clsx from "clsx"
import { useState, useEffect } from "react"
import { Tooltip as ReactTooltip } from "react-tooltip"
import type { ITooltip } from "react-tooltip"
import "react-tooltip/dist/react-tooltip.css"
export type TooltipProps = {
text?: string
tooltipClassName?: string
html?: string
tooltipChildren?: React.ReactNode
} & React.HTMLAttributes<HTMLSpanElement> &
ITooltip
const Tooltip = ({
text = "",
tooltipClassName = "",
children,
html = "",
tooltipChildren,
...rest
}: TooltipProps) => {
const [elementId, setElementId] = useState<string | null>(null)
useEffect(() => {
async function initElementId() {
if (!elementId) {
const uuid = (await import("uuid")).v4
setElementId(uuid())
}
}
void initElementId()
}, [elementId])
return (
<>
<span
id={elementId || ""}
data-tooltip-content={text}
data-tooltip-html={html}
>
{children}
</span>
<ReactTooltip
anchorId={elementId || ""}
className={clsx(
"!border-medusa-border-base dark:!border-medusa-border-base-dark !border !border-solid",
"!text-compact-x-small-plus !shadow-tooltip dark:!shadow-tooltip-dark !rounded",
"!py-0.4 !z-[1000] hidden !px-1 lg:block",
tooltipClassName
)}
wrapper="span"
noArrow={true}
positionStrategy={"fixed"}
{...rest}
>
{tooltipChildren}
</ReactTooltip>
</>
)
}
export default Tooltip