docs: prep for v2 documentation (#6710)
This PR includes documentation that preps for v2 docs (but doesn't introduce new docs). _Note: The number of file changes in the PR is due to find-and-replace within the `references` which is unavoidable. Let me know if I should move it to another PR._ ## Changes - Change Medusa version in base OAS used for v2. - Fix to docblock generator related to not catching all path parameters. - Added typedoc plugin that generates ER Diagrams, which will be used specifically for data model references in commerce modules. - Changed OAS tool to output references in `www/apps/api-reference/specs-v2` directory when the `--v2` option is used. - Added a version switcher to the API reference to switch between V1 and V2. This switcher is enabled by an environment variable, so it won't be visible/usable at the moment. - Upgraded docusaurus to v3.0.1 - Added new Vale rules to ensure correct spelling of Medusa Admin and module names. - Added new components to the `docs-ui` package that will be used in future documentation changes.
This commit is contained in:
1 parent
56a6ec0227
commit
bb87db8342
2008 files changed
+15716
-10536
No files matched your search
@@ -11,7 +11,7 @@ import {
|
||||
SearchSuggestionType,
|
||||
SearchHitGroupName,
|
||||
Tooltip,
|
||||
Link,
|
||||
LegacyLink,
|
||||
} from "@/components"
|
||||
import { useAiAssistant, useSearch } from "@/providers"
|
||||
import { ArrowUturnLeft, XMarkMini } from "@medusajs/icons"
|
||||
@@ -382,10 +382,14 @@ export const AiAssistant = () => {
|
||||
tooltipChildren={
|
||||
<>
|
||||
This site is protected by reCAPTCHA and the{" "}
|
||||
<Link href="https://policies.google.com/privacy">
|
||||
{/* TODO replace with Link once we move away from Docusaurus */}
|
||||
<LegacyLink href="https://policies.google.com/privacy">
|
||||
Google Privacy Policy
|
||||
</Link>{" "}
|
||||
and <Link href="https://policies.google.com/terms">ToS</Link>{" "}
|
||||
</LegacyLink>{" "}
|
||||
and{" "}
|
||||
<LegacyLink href="https://policies.google.com/terms">
|
||||
ToS
|
||||
</LegacyLink>{" "}
|
||||
apply
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,50 +1,71 @@
|
||||
import React from "react"
|
||||
import { ArrowUpRightOnBox } from "@medusajs/icons"
|
||||
import clsx from "clsx"
|
||||
import { Link } from "@/components"
|
||||
import { LegacyLink } from "@/components"
|
||||
|
||||
export type CardProps = {
|
||||
icon?: React.ReactNode
|
||||
title: string
|
||||
startIcon?: React.ReactNode
|
||||
endIcon?: React.ReactNode
|
||||
title?: string
|
||||
text?: string
|
||||
href?: string
|
||||
className?: string
|
||||
contentClassName?: string
|
||||
children?: React.ReactNode
|
||||
showLinkIcon?: boolean
|
||||
}
|
||||
|
||||
export const Card = ({ icon, title, text, href, className }: CardProps) => {
|
||||
export const Card = ({
|
||||
startIcon,
|
||||
endIcon,
|
||||
title,
|
||||
text,
|
||||
href,
|
||||
className,
|
||||
contentClassName,
|
||||
children,
|
||||
showLinkIcon = true,
|
||||
}: CardProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-medusa-bg-subtle w-full rounded",
|
||||
"shadow-card-rest dark:shadow-card-rest-dark py-docs_0.75 relative px-docs_1",
|
||||
"flex items-center gap-docs_1 transition-shadow",
|
||||
"flex items-start gap-docs_1 transition-shadow",
|
||||
href && "hover:shadow-card-hover dark:hover:shadow-card-hover-dark",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
<div className="flex items-center gap-docs_1 justify-between flex-1">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-compact-medium-plus text-medusa-fg-base">
|
||||
{title}
|
||||
</span>
|
||||
{startIcon}
|
||||
<div className="flex items-start gap-docs_1 justify-between flex-1">
|
||||
<div className={clsx("flex flex-col", contentClassName)}>
|
||||
{title && (
|
||||
<span className="text-compact-medium-plus text-medusa-fg-base">
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
{text && (
|
||||
<span className="text-compact-medium text-medusa-fg-subtle">
|
||||
{text}
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{href && (
|
||||
<>
|
||||
<ArrowUpRightOnBox className="text-medusa-fg-subtle" />
|
||||
<Link
|
||||
{showLinkIcon && (
|
||||
<ArrowUpRightOnBox className="text-medusa-fg-subtle min-w-[20px]" />
|
||||
)}
|
||||
{/* TODO replace with Link once we move away from Docusaurus */}
|
||||
<LegacyLink
|
||||
href={href}
|
||||
className="absolute left-0 top-0 h-full w-full rounded"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{endIcon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react"
|
||||
import { Card, CardProps } from "@/components"
|
||||
import clsx from "clsx"
|
||||
|
||||
type CardListProps = {
|
||||
items: CardProps[]
|
||||
itemsPerRow?: number
|
||||
}
|
||||
|
||||
export const CardList = ({ items, itemsPerRow }: CardListProps) => {
|
||||
if (!itemsPerRow) {
|
||||
// if length of items is even, set to `2`, else set to `3`
|
||||
itemsPerRow = items.length === 1 ? 0 : items.length % 2 === 0 ? 2 : 3
|
||||
}
|
||||
return (
|
||||
<section
|
||||
className={clsx(
|
||||
"grid gap-x-docs_1 auto-rows-fr gap-y-docs_1",
|
||||
itemsPerRow === 1 && "grid-cols-1",
|
||||
itemsPerRow === 2 && "md:grid-cols-2 grid-cols-1",
|
||||
itemsPerRow === 3 && "lg:grid-cols-3 md:grid-col-2 grid-cols-1"
|
||||
)}
|
||||
>
|
||||
{items.map((item, key) => (
|
||||
<Card {...item} key={key} />
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { Card, CardList, MDXComponents, useSidebar } from "../.."
|
||||
import { SidebarItemType } from "types"
|
||||
|
||||
type ChildDocsProps = {
|
||||
onlyTopLevel?: boolean
|
||||
}
|
||||
|
||||
export const ChildDocs = ({ onlyTopLevel = false }: ChildDocsProps) => {
|
||||
const { currentItems } = useSidebar()
|
||||
|
||||
const getTopLevelElms = (items?: SidebarItemType[]) => (
|
||||
<CardList
|
||||
items={
|
||||
items?.map((childItem) => ({
|
||||
title: childItem.title,
|
||||
href: childItem.path,
|
||||
showLinkIcon: false,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
const getAllLevelsElms = (items?: SidebarItemType[]) =>
|
||||
items?.map((item, key) => {
|
||||
const HeadingComponent = item.children?.length
|
||||
? MDXComponents["h2"]
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
{HeadingComponent && (
|
||||
<>
|
||||
<HeadingComponent>{item.title}</HeadingComponent>
|
||||
<CardList
|
||||
items={
|
||||
item.children?.map((childItem) => ({
|
||||
title: childItem.title,
|
||||
href: childItem.path,
|
||||
showLinkIcon: false,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!HeadingComponent && (
|
||||
<Card title={item.title} href={item.path} showLinkIcon={false} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
|
||||
const getElms = (items?: SidebarItemType[]) => {
|
||||
return onlyTopLevel ? getTopLevelElms(items) : getAllLevelsElms(items)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{getElms(currentItems?.top)}
|
||||
{getElms(currentItems?.bottom)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useRequestRunner } from "../../../hooks"
|
||||
import { CodeBlock } from ".."
|
||||
import { Card } from "../../Card"
|
||||
import { Button, InputText } from "../../.."
|
||||
import { ApiMethod, ApiDataOptions, ApiTestingOptions } from "types"
|
||||
|
||||
type ApiRunnerProps = {
|
||||
apiMethod: ApiMethod
|
||||
apiUrl: string
|
||||
pathData?: Record<string, unknown>
|
||||
queryData?: Record<string, unknown>
|
||||
bodyData?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const ApiRunner = ({
|
||||
apiMethod,
|
||||
apiUrl,
|
||||
pathData,
|
||||
bodyData,
|
||||
queryData,
|
||||
}: ApiRunnerProps) => {
|
||||
// assemble api testing options
|
||||
const [apiTestingOptions, setApiTestingOptions] = useState<ApiTestingOptions>(
|
||||
{
|
||||
method: apiMethod,
|
||||
url: apiUrl,
|
||||
pathData,
|
||||
bodyData,
|
||||
queryData,
|
||||
}
|
||||
)
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [ran, setRan] = useState(false)
|
||||
const hasData = (data?: Record<string, unknown>): boolean =>
|
||||
data !== undefined && Object.keys(data).length > 0
|
||||
// TODO change to be based on whether auth/data needed
|
||||
const manualTestTrigger = useMemo(
|
||||
() =>
|
||||
hasData(apiTestingOptions.pathData) ||
|
||||
hasData(apiTestingOptions.queryData) ||
|
||||
hasData(apiTestingOptions.bodyData),
|
||||
[apiTestingOptions]
|
||||
)
|
||||
const [responseLogs, setResponseLogs] = useState<string[]>([])
|
||||
const pushMessage = (...message: string[]) =>
|
||||
setResponseLogs((prev) => [...prev, ...message])
|
||||
const { runRequest } = useRequestRunner({
|
||||
pushLog: pushMessage,
|
||||
onFinish: () => setIsRunning(false),
|
||||
replaceLog: (message) => setResponseLogs([message]),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRunning && !manualTestTrigger && !ran) {
|
||||
setIsRunning(true)
|
||||
}
|
||||
}, [apiTestingOptions, manualTestTrigger, isRunning, ran])
|
||||
|
||||
useEffect(() => {
|
||||
if (isRunning && !ran) {
|
||||
setRan(true)
|
||||
setResponseLogs(["Sending request..."])
|
||||
runRequest(apiTestingOptions)
|
||||
}
|
||||
}, [isRunning, ran])
|
||||
|
||||
const getParamsElms = ({
|
||||
data,
|
||||
title,
|
||||
nameInApiOptions,
|
||||
}: {
|
||||
data: ApiDataOptions
|
||||
title: string
|
||||
nameInApiOptions: "pathData" | "bodyData" | "queryData"
|
||||
}) => (
|
||||
<div className="flex flex-col gap-docs_0.5">
|
||||
<span className="text-compact-medium-plus text-medusa-fg-base">
|
||||
{title}
|
||||
</span>
|
||||
<div className="flex gap-docs_0.5">
|
||||
{Object.keys(data).map((pathParam, index) => (
|
||||
<InputText
|
||||
name={pathParam}
|
||||
onChange={(e) =>
|
||||
setApiTestingOptions((prev) => ({
|
||||
...prev,
|
||||
[nameInApiOptions]: {
|
||||
...prev[nameInApiOptions],
|
||||
[pathParam]: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
key={index}
|
||||
placeholder={pathParam}
|
||||
value={
|
||||
typeof data[pathParam] === "string"
|
||||
? (data[pathParam] as string)
|
||||
: typeof data[pathParam] === "number"
|
||||
? (data[pathParam] as number)
|
||||
: `${data[pathParam]}`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{manualTestTrigger && (
|
||||
<Card className="font-base mb-docs_1" contentClassName="gap-docs_0.5">
|
||||
{apiTestingOptions.pathData &&
|
||||
getParamsElms({
|
||||
data: apiTestingOptions.pathData,
|
||||
title: "Path Parameters",
|
||||
nameInApiOptions: "pathData",
|
||||
})}
|
||||
{apiTestingOptions.bodyData &&
|
||||
getParamsElms({
|
||||
data: apiTestingOptions.bodyData,
|
||||
title: "Request Body Parameters",
|
||||
nameInApiOptions: "bodyData",
|
||||
})}
|
||||
{apiTestingOptions.queryData &&
|
||||
getParamsElms({
|
||||
data: apiTestingOptions.queryData,
|
||||
title: "Request Query Parameters",
|
||||
nameInApiOptions: "queryData",
|
||||
})}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsRunning(true)
|
||||
setRan(false)
|
||||
}}
|
||||
>
|
||||
Send Request
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
{(isRunning || ran) && (
|
||||
<CodeBlock
|
||||
source={responseLogs.join("\n")}
|
||||
lang="json"
|
||||
title="Testing Result"
|
||||
collapsed={true}
|
||||
blockStyle="subtle"
|
||||
noReport={true}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client"
|
||||
|
||||
import React, { useMemo } from "react"
|
||||
import clsx from "clsx"
|
||||
import { CodeBlockStyle } from ".."
|
||||
import { useColorMode } from "@/providers"
|
||||
import { Badge, BadgeVariant } from "@/components"
|
||||
|
||||
export type CodeBlockHeaderMeta = {
|
||||
badgeLabel?: string
|
||||
badgeColor?: BadgeVariant
|
||||
}
|
||||
|
||||
type CodeBlockHeaderProps = {
|
||||
children?: React.ReactNode
|
||||
title?: string
|
||||
blockStyle?: CodeBlockStyle
|
||||
} & CodeBlockHeaderMeta
|
||||
|
||||
export const CodeBlockHeader = ({
|
||||
children,
|
||||
title,
|
||||
blockStyle = "loud",
|
||||
badgeLabel,
|
||||
badgeColor,
|
||||
}: CodeBlockHeaderProps) => {
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
const borderColor = useMemo(
|
||||
() =>
|
||||
clsx(
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" && "border-medusa-code-border",
|
||||
colorMode === "dark" && "border-medusa-border-base",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" && "border-medusa-border-base",
|
||||
colorMode === "dark" && "border-medusa-code-border",
|
||||
]
|
||||
),
|
||||
[blockStyle, colorMode]
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"py-docs_0.75 rounded-t-docs_DEFAULT px-docs_1 mb-0",
|
||||
"flex gap-docs_2 items-start justify-between",
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" && "bg-medusa-code-bg-header",
|
||||
colorMode === "dark" && "bg-medusa-bg-base",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" && "bg-medusa-bg-component",
|
||||
colorMode === "dark" && "bg-medusa-code-bg-header",
|
||||
],
|
||||
borderColor && `border border-b-0 ${borderColor}`
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{title && (
|
||||
<div
|
||||
className={clsx(
|
||||
"txt-compact-small-plus",
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" && "text-medusa-code-text-subtle",
|
||||
colorMode === "dark" && "text-medusa-fg-muted",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" && "text-medusa-fg-subtle",
|
||||
colorMode === "dark" && "text-medusa-code-text-subtle",
|
||||
]
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{badgeLabel && (
|
||||
<Badge variant={badgeColor || "orange"} className="font-base">
|
||||
{badgeLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { Highlight } from ".."
|
||||
import { RenderProps, Token } from "prism-react-renderer"
|
||||
import clsx from "clsx"
|
||||
import { MarkdownContent, Tooltip } from "@/components"
|
||||
|
||||
type CodeBlockLineProps = {
|
||||
line: Token[]
|
||||
highlights?: Highlight[]
|
||||
lineNumber: number
|
||||
showLineNumber: boolean
|
||||
bgColorClassName: string
|
||||
lineNumberColorClassName: string
|
||||
noLineNumbers?: boolean
|
||||
} & Pick<RenderProps, "getLineProps" | "getTokenProps">
|
||||
|
||||
export const CodeBlockLine = ({
|
||||
line,
|
||||
highlights = [],
|
||||
lineNumber,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
showLineNumber,
|
||||
bgColorClassName,
|
||||
lineNumberColorClassName,
|
||||
}: CodeBlockLineProps) => {
|
||||
const lineProps = getLineProps({ line, key: lineNumber })
|
||||
// collect highlighted tokens, if there are any
|
||||
const highlightedTokens: {
|
||||
start: number
|
||||
end: number
|
||||
highlight: Highlight
|
||||
}[] = []
|
||||
|
||||
highlights.forEach((highlight) => {
|
||||
if (!highlight.text) {
|
||||
return
|
||||
}
|
||||
let startIndex: number | undefined = undefined
|
||||
let currentPositionInHighlightedText = 0
|
||||
let endIndex = 0
|
||||
const found = line.some((token, tokenIndex) => {
|
||||
if (token.empty || !token.content.length) {
|
||||
startIndex = undefined
|
||||
currentPositionInHighlightedText = 0
|
||||
return false
|
||||
}
|
||||
const comparisonLength = Math.min(
|
||||
token.content.length,
|
||||
highlight.text!.substring(currentPositionInHighlightedText).length
|
||||
)
|
||||
const nextPositionInHighlightedText =
|
||||
currentPositionInHighlightedText + comparisonLength
|
||||
|
||||
const canHighlight =
|
||||
!highlightedTokens.length ||
|
||||
!highlightedTokens.some(
|
||||
(token) => tokenIndex >= token.start && tokenIndex <= token.end
|
||||
)
|
||||
|
||||
if (
|
||||
token.content.substring(0, comparisonLength) ===
|
||||
highlight.text?.substring(
|
||||
currentPositionInHighlightedText,
|
||||
nextPositionInHighlightedText
|
||||
) &&
|
||||
canHighlight
|
||||
) {
|
||||
if (startIndex === undefined) {
|
||||
startIndex = tokenIndex
|
||||
}
|
||||
currentPositionInHighlightedText = nextPositionInHighlightedText
|
||||
}
|
||||
|
||||
if (currentPositionInHighlightedText === highlight.text!.length) {
|
||||
// matching text was found, break loop
|
||||
endIndex = tokenIndex
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if (found && startIndex !== undefined) {
|
||||
highlightedTokens.push({
|
||||
start: startIndex,
|
||||
end: endIndex,
|
||||
highlight,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// sort highlighted tokens by their start position
|
||||
highlightedTokens.sort((tokensA, tokensB) => {
|
||||
if (tokensA.start < tokensB.start) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return tokensA.start > tokensB.start ? 1 : 0
|
||||
})
|
||||
|
||||
// if there are highlighted tokens, split tokens in the
|
||||
// line by segments of not highlighted and highlighted token
|
||||
// if there are no highlighted tokens, the line is used as-is.
|
||||
const transformedLine: {
|
||||
tokens: Token[]
|
||||
type: "default" | "highlighted"
|
||||
highlight?: Highlight
|
||||
}[] = highlightedTokens.length
|
||||
? []
|
||||
: [
|
||||
{
|
||||
tokens: line,
|
||||
type: "default",
|
||||
},
|
||||
]
|
||||
|
||||
let lastIndex = 0
|
||||
// go through highlighted tokens to add the segments before/after to the
|
||||
// transformedLines array
|
||||
highlightedTokens.forEach((highlightedTokensItem, index) => {
|
||||
if (lastIndex < highlightedTokensItem.start) {
|
||||
transformedLine.push({
|
||||
tokens: line.slice(lastIndex, highlightedTokensItem.start),
|
||||
type: "default",
|
||||
})
|
||||
}
|
||||
transformedLine.push({
|
||||
tokens: line.slice(
|
||||
highlightedTokensItem.start,
|
||||
highlightedTokensItem.end + 1
|
||||
),
|
||||
type: "highlighted",
|
||||
highlight: highlightedTokensItem.highlight,
|
||||
})
|
||||
lastIndex = highlightedTokensItem.end + 1
|
||||
|
||||
// if this is the last item in `highlightedTokens` and
|
||||
// its end index is less than the line's length, that means
|
||||
// there are tokens at the end of the line that aren't highlighted
|
||||
// and should be pushed as-is to the `transformedLines` array.
|
||||
if (index === highlightedTokens.length - 1 && lastIndex < line.length - 1) {
|
||||
transformedLine.push({
|
||||
tokens: line.slice(lastIndex),
|
||||
type: "default",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const getTokensElm = ({
|
||||
tokens,
|
||||
isHighlighted,
|
||||
offset,
|
||||
}: {
|
||||
tokens: Token[]
|
||||
isHighlighted: boolean
|
||||
offset: number
|
||||
}) => (
|
||||
<span
|
||||
className={clsx(
|
||||
// TODO change code colors and class names based on figma colors
|
||||
isHighlighted && [
|
||||
"lg:py-px lg:px-[6px] lg:border-medusa-code-icon lg:rounded-docs_sm",
|
||||
"lg:bg-medusa-code-border lg:cursor-pointer",
|
||||
]
|
||||
)}
|
||||
>
|
||||
{tokens.map((token, key) => {
|
||||
const tokenKey = offset + key
|
||||
const { className: tokenClassName, ...rest } = getTokenProps({
|
||||
token,
|
||||
key: tokenKey,
|
||||
})
|
||||
return (
|
||||
<span key={tokenKey} className={clsx(tokenClassName)} {...rest} />
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
|
||||
const isHighlightedLine = useMemo(
|
||||
() => highlights.length && !highlightedTokens.length,
|
||||
[highlights, highlightedTokens]
|
||||
)
|
||||
|
||||
return (
|
||||
<span
|
||||
key={lineNumber}
|
||||
{...lineProps}
|
||||
className={clsx(
|
||||
"table-row",
|
||||
isHighlightedLine && "bg-medusa-code-bg-header",
|
||||
lineProps.className
|
||||
)}
|
||||
>
|
||||
{showLineNumber && (
|
||||
<span
|
||||
className={clsx(
|
||||
"mr-docs_1 table-cell select-none",
|
||||
"sticky left-0 w-[1%] px-docs_1 text-right",
|
||||
bgColorClassName,
|
||||
lineNumberColorClassName
|
||||
)}
|
||||
>
|
||||
{lineNumber + 1}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{transformedLine.map(({ tokens, type, highlight }, index) => {
|
||||
const offset =
|
||||
index === 0 ? 0 : transformedLine[index - 1].tokens.length
|
||||
const tooltipText =
|
||||
highlight?.tooltipText ||
|
||||
(isHighlightedLine
|
||||
? highlights.find((h) => h.tooltipText !== undefined)?.tooltipText
|
||||
: undefined)
|
||||
const isHighlighted = type === "highlighted"
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{tooltipText && (
|
||||
<Tooltip
|
||||
text={tooltipText}
|
||||
tooltipClassName="font-base"
|
||||
render={({ content }) => (
|
||||
<MarkdownContent
|
||||
allowedElements={["a", "strong", "code"]}
|
||||
unwrapDisallowed={true}
|
||||
>
|
||||
{content || ""}
|
||||
</MarkdownContent>
|
||||
)}
|
||||
>
|
||||
{getTokensElm({ tokens, isHighlighted, offset })}
|
||||
</Tooltip>
|
||||
)}
|
||||
{!tooltipText && getTokensElm({ tokens, isHighlighted, offset })}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -1,124 +1,329 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import React, { useMemo, useState } from "react"
|
||||
import clsx from "clsx"
|
||||
import { HighlightProps, Highlight, themes } from "prism-react-renderer"
|
||||
import { CopyButton, useColorMode } from "docs-ui"
|
||||
import { SquareTwoStackSolid } from "@medusajs/icons"
|
||||
import { CopyButton, Tooltip, LegacyLink } from "@/components"
|
||||
import { useColorMode } from "@/providers"
|
||||
import { ExclamationCircle, PlaySolid, SquareTwoStack } from "@medusajs/icons"
|
||||
import { CodeBlockHeader, CodeBlockHeaderMeta } from "./Header"
|
||||
import { CodeBlockLine } from "./Line"
|
||||
import { ApiAuthType, ApiDataOptions, ApiMethod } from "types"
|
||||
import { CSSTransition } from "react-transition-group"
|
||||
import { ApiRunner } from "./ApiRunner"
|
||||
import { GITHUB_ISSUES_PREFIX } from "../.."
|
||||
|
||||
export type Highlight = {
|
||||
line: number
|
||||
text?: string
|
||||
tooltipText?: string
|
||||
}
|
||||
|
||||
export type CodeBlockMetaFields = {
|
||||
title?: string
|
||||
npm2yarn?: boolean
|
||||
highlights?: string[][]
|
||||
apiTesting?: boolean
|
||||
testApiMethod?: ApiMethod
|
||||
testApiUrl?: string
|
||||
testAuthType?: ApiAuthType
|
||||
testPathParams?: ApiDataOptions
|
||||
testQueryParams?: ApiDataOptions
|
||||
testBodyParams?: ApiDataOptions
|
||||
noCopy?: boolean
|
||||
noReport?: boolean
|
||||
noLineNumbers?: boolean
|
||||
} & CodeBlockHeaderMeta
|
||||
|
||||
export type CodeBlockStyle = "loud" | "subtle"
|
||||
|
||||
export type CodeBlockProps = {
|
||||
source: string
|
||||
lang?: string
|
||||
className?: string
|
||||
collapsed?: boolean
|
||||
} & Omit<HighlightProps, "code" | "language" | "children">
|
||||
blockStyle?: CodeBlockStyle
|
||||
children?: React.ReactNode
|
||||
} & CodeBlockMetaFields &
|
||||
Omit<HighlightProps, "code" | "language" | "children">
|
||||
|
||||
export const CodeBlock = ({
|
||||
source,
|
||||
lang = "",
|
||||
className,
|
||||
collapsed = false,
|
||||
title = "",
|
||||
highlights = [],
|
||||
apiTesting = false,
|
||||
blockStyle = "loud",
|
||||
noCopy = false,
|
||||
noReport = false,
|
||||
noLineNumbers = false,
|
||||
children,
|
||||
...rest
|
||||
}: CodeBlockProps) => {
|
||||
if (!source && typeof children === "string") {
|
||||
source = children
|
||||
}
|
||||
|
||||
const { colorMode } = useColorMode()
|
||||
const [showTesting, setShowTesting] = useState(false)
|
||||
const canShowApiTesting = useMemo(
|
||||
() => apiTesting && rest.testApiMethod && rest.testApiUrl,
|
||||
[apiTesting, rest]
|
||||
)
|
||||
|
||||
const bgColor = useMemo(
|
||||
() =>
|
||||
clsx(
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" && "bg-medusa-code-bg-base",
|
||||
colorMode === "dark" && "bg-medusa-bg-component",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" && "bg-medusa-bg-subtle",
|
||||
colorMode === "dark" && "bg-medusa-code-bg-base",
|
||||
]
|
||||
),
|
||||
[blockStyle, colorMode]
|
||||
)
|
||||
|
||||
const lineNumbersColor = useMemo(
|
||||
() =>
|
||||
clsx(
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" && "text-medusa-code-text-subtle",
|
||||
colorMode === "dark" && "text-medusa-fg-muted",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" && "text-medusa-fg-muted",
|
||||
colorMode === "dark" && "text-medusa-code-text-subtle",
|
||||
]
|
||||
),
|
||||
[blockStyle, colorMode]
|
||||
)
|
||||
|
||||
const borderColor = useMemo(
|
||||
() =>
|
||||
clsx(
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" && "border-medusa-code-border",
|
||||
colorMode === "dark" && "border-medusa-border-base",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" && "border-medusa-border-base",
|
||||
colorMode === "dark" && "border-medusa-code-border",
|
||||
]
|
||||
),
|
||||
[blockStyle, colorMode]
|
||||
)
|
||||
|
||||
const iconColor = useMemo(
|
||||
() =>
|
||||
clsx(
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" && "text-medusa-code-icon",
|
||||
colorMode === "dark" && "text-medusa-fg-muted",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" && "text-medusa-fg-muted",
|
||||
colorMode === "dark" && "text-medusa-code-icon",
|
||||
]
|
||||
),
|
||||
[blockStyle, colorMode]
|
||||
)
|
||||
|
||||
if (!source.length) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const transformedHighlights: Highlight[] = highlights
|
||||
.filter((highlight) => highlight.length !== 0)
|
||||
.map((highlight) => ({
|
||||
line: parseInt(highlight[0]),
|
||||
text: highlight.length >= 2 ? highlight[1] : undefined,
|
||||
tooltipText: highlight.length >= 3 ? highlight[2] : undefined,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-medusa-code-bg-base relative mb-docs_1 rounded-docs_DEFAULT",
|
||||
"border-medusa-code-border w-full max-w-full border",
|
||||
collapsed && "max-h-[400px] overflow-auto",
|
||||
className
|
||||
<>
|
||||
{title && (
|
||||
<CodeBlockHeader
|
||||
title={title}
|
||||
blockStyle={blockStyle}
|
||||
badgeLabel={rest.badgeLabel}
|
||||
badgeColor={rest.badgeColor}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Highlight
|
||||
theme={{
|
||||
...themes.vsDark,
|
||||
plain: {
|
||||
...themes.vsDark.plain,
|
||||
backgroundColor: colorMode === "light" ? "#111827" : "#1B1B1F",
|
||||
},
|
||||
}}
|
||||
code={source.trim()}
|
||||
language={lang.toLowerCase()}
|
||||
{...rest}
|
||||
>
|
||||
{({
|
||||
className: preClassName,
|
||||
style,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
}) => (
|
||||
<>
|
||||
<pre
|
||||
style={{ ...style, fontStretch: "100%" }}
|
||||
className={clsx(
|
||||
"xs:max-w-[90%] relative !my-0 break-words bg-transparent !outline-none",
|
||||
"overflow-auto break-words rounded-docs_DEFAULT p-0",
|
||||
preClassName
|
||||
)}
|
||||
>
|
||||
<code
|
||||
className={clsx(
|
||||
"text-code-body font-monospace table min-w-full pb-docs_1.5 print:whitespace-pre-wrap",
|
||||
tokens.length > 1 && "pt-docs_1 pr-docs_1",
|
||||
tokens.length <= 1 && "!py-docs_0.5 px-docs_1"
|
||||
)}
|
||||
>
|
||||
{tokens.map((line, i) => {
|
||||
const lineProps = getLineProps({ line, key: i })
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
{...lineProps}
|
||||
className={clsx("table-row", lineProps.className)}
|
||||
>
|
||||
{tokens.length > 1 && (
|
||||
<span
|
||||
className={clsx(
|
||||
"text-medusa-code-text-subtle mr-docs_1 table-cell select-none",
|
||||
"bg-medusa-code-bg-base sticky left-0 w-[1%] px-docs_1 text-right"
|
||||
)}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token, key })} />
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</code>
|
||||
</pre>
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute hidden gap-docs_1 md:flex",
|
||||
"xs:rounded xs:absolute xs:right-0 xs:top-0 xs:w-[calc(10%+24px)] xs:h-full xs:bg-code-fade"
|
||||
)}
|
||||
>
|
||||
<CopyButton
|
||||
text={source}
|
||||
tooltipClassName="font-base"
|
||||
className={clsx(
|
||||
"absolute",
|
||||
tokens.length === 1 && "right-docs_0.75 top-[10px]",
|
||||
tokens.length > 1 && "right-docs_1 top-docs_1"
|
||||
)}
|
||||
>
|
||||
<SquareTwoStackSolid className="text-medusa-code-icon" />
|
||||
</CopyButton>
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
className={clsx(
|
||||
"relative mb-docs_1 rounded-b-docs_DEFAULT",
|
||||
"w-full max-w-full border",
|
||||
bgColor,
|
||||
borderColor,
|
||||
collapsed && "max-h-[400px] overflow-auto",
|
||||
!title && "rounded-t-docs_DEFAULT",
|
||||
(blockStyle === "loud" || colorMode !== "light") &&
|
||||
"code-block-highlight-dark",
|
||||
blockStyle === "subtle" &&
|
||||
colorMode === "light" &&
|
||||
"code-block-highlight-light",
|
||||
className
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
>
|
||||
<Highlight
|
||||
theme={
|
||||
blockStyle === "loud" || colorMode === "dark"
|
||||
? {
|
||||
...themes.vsDark,
|
||||
plain: {
|
||||
...themes.vsDark.plain,
|
||||
backgroundColor:
|
||||
blockStyle === "loud"
|
||||
? colorMode === "light"
|
||||
? "#111827"
|
||||
: "#27282D"
|
||||
: "#1B1B1F",
|
||||
},
|
||||
}
|
||||
: {
|
||||
...themes.vsLight,
|
||||
plain: {
|
||||
...themes.vsLight.plain,
|
||||
backgroundColor: "#F9FAFB",
|
||||
},
|
||||
}
|
||||
}
|
||||
code={source.trim()}
|
||||
language={lang.toLowerCase()}
|
||||
{...rest}
|
||||
>
|
||||
{({ className: preClassName, style, tokens, ...rest }) => (
|
||||
<>
|
||||
<pre
|
||||
style={{ ...style, fontStretch: "100%" }}
|
||||
className={clsx(
|
||||
"relative !my-0 break-words bg-transparent !outline-none",
|
||||
"overflow-auto break-words rounded-docs_DEFAULT p-0 xs:max-w-[83%]",
|
||||
preClassName
|
||||
)}
|
||||
>
|
||||
<code
|
||||
className={clsx(
|
||||
"text-code-body font-monospace table min-w-full pb-docs_1.5 print:whitespace-pre-wrap",
|
||||
tokens.length > 1 && "pt-docs_1 pr-docs_1",
|
||||
tokens.length <= 1 && "!py-docs_0.25 px-[6px]"
|
||||
)}
|
||||
>
|
||||
{tokens.map((line, i) => {
|
||||
const highlightedLines = transformedHighlights.filter(
|
||||
(highlight) => highlight.line - 1 === i
|
||||
)
|
||||
|
||||
return (
|
||||
<CodeBlockLine
|
||||
line={line}
|
||||
lineNumber={i}
|
||||
highlights={highlightedLines}
|
||||
showLineNumber={!noLineNumbers && tokens.length > 1}
|
||||
key={i}
|
||||
bgColorClassName={bgColor}
|
||||
lineNumberColorClassName={lineNumbersColor}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</code>
|
||||
</pre>
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute hidden md:flex md:justify-end",
|
||||
"xs:rounded xs:absolute xs:right-0 xs:top-0 xs:w-[calc(10%+24px)] xs:h-full xs:bg-transparent",
|
||||
tokens.length === 1 && "md:right-[6px] md:top-0",
|
||||
tokens.length > 1 && "md:right-docs_1 md:top-docs_1"
|
||||
)}
|
||||
>
|
||||
{canShowApiTesting && (
|
||||
<Tooltip
|
||||
text="Test API"
|
||||
tooltipClassName="font-base"
|
||||
className={clsx(
|
||||
"h-fit",
|
||||
tokens.length === 1 && "p-[6px]",
|
||||
tokens.length > 1 && "px-[6px] pb-[6px]"
|
||||
)}
|
||||
>
|
||||
<PlaySolid
|
||||
className={clsx("cursor-pointer", iconColor)}
|
||||
onClick={() => setShowTesting(true)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!noReport && (
|
||||
<Tooltip
|
||||
text="Report Issue"
|
||||
tooltipClassName="font-base"
|
||||
className={clsx(
|
||||
"h-fit",
|
||||
tokens.length === 1 && "p-[6px]",
|
||||
tokens.length > 1 && "px-[6px] pb-[6px]"
|
||||
)}
|
||||
>
|
||||
{/* TODO replace with Link once we move away from Docusaurus */}
|
||||
<LegacyLink
|
||||
href={`${GITHUB_ISSUES_PREFIX}&title=${encodeURIComponent(
|
||||
`Docs(Code Issue): `
|
||||
)}`}
|
||||
target="_blank"
|
||||
className={clsx(
|
||||
blockStyle === "loud" && "hover:bg-medusa-code-bg-base",
|
||||
"bg-transparent border-none cursor-pointer rounded",
|
||||
"[&:not(:first-child)]:ml-docs_0.5",
|
||||
"inline-flex justify-center items-center invisible xs:visible"
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ExclamationCircle className={clsx(iconColor)} />
|
||||
</LegacyLink>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!noCopy && (
|
||||
<CopyButton
|
||||
text={source}
|
||||
tooltipClassName="font-base"
|
||||
className={clsx(
|
||||
"h-fit",
|
||||
tokens.length === 1 && "p-[6px]",
|
||||
tokens.length > 1 && "px-[6px] pb-[6px]"
|
||||
)}
|
||||
>
|
||||
<SquareTwoStack className={clsx(iconColor)} />
|
||||
</CopyButton>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
{canShowApiTesting && (
|
||||
<CSSTransition
|
||||
unmountOnExit
|
||||
in={showTesting}
|
||||
timeout={150}
|
||||
classNames={{
|
||||
enter: "animate-fadeIn animate-fastest",
|
||||
exit: "animate-fadeOut animate-fastest",
|
||||
}}
|
||||
>
|
||||
<ApiRunner
|
||||
apiMethod={rest.testApiMethod!}
|
||||
apiUrl={rest.testApiUrl!}
|
||||
pathData={rest.testPathParams}
|
||||
bodyData={rest.testBodyParams}
|
||||
queryData={rest.testQueryParams}
|
||||
/>
|
||||
</CSSTransition>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
import React from "react"
|
||||
import { CodeBlock, InlineCode } from "@/components"
|
||||
import {
|
||||
CodeBlock,
|
||||
CodeBlockMetaFields,
|
||||
InlineCode,
|
||||
MermaidDiagram,
|
||||
} from "@/components"
|
||||
import { Npm2YarnCode } from "../Npm2YarnCode"
|
||||
|
||||
export type CodeMdxProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
} & CodeBlockMetaFields
|
||||
|
||||
// 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
|
||||
export const CodeMdx = ({ className, children }: CodeMdxProps) => {
|
||||
export const CodeMdx = ({ className, children, ...rest }: CodeMdxProps) => {
|
||||
if (!children) {
|
||||
return <></>
|
||||
}
|
||||
@@ -22,7 +28,12 @@ export const CodeMdx = ({ className, children }: CodeMdxProps) => {
|
||||
: (children as string)
|
||||
|
||||
if (match) {
|
||||
return <CodeBlock source={codeContent} lang={match[1]} />
|
||||
if (rest.npm2yarn) {
|
||||
return <Npm2YarnCode npmCode={codeContent} {...rest} />
|
||||
} else if (match[1] === "mermaid") {
|
||||
return <MermaidDiagram diagramContent={codeContent} />
|
||||
}
|
||||
return <CodeBlock source={codeContent} lang={match[1]} {...rest} />
|
||||
}
|
||||
|
||||
return <InlineCode>{codeContent}</InlineCode>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { BaseTabType, useScrollPositionBlocker } from "@/hooks"
|
||||
import { useColorMode } from "@/providers"
|
||||
import clsx from "clsx"
|
||||
|
||||
type CodeTabProps = BaseTabType & {
|
||||
children: React.ReactNode
|
||||
isSelected?: boolean
|
||||
blockStyle?: string
|
||||
changeSelectedTab?: (tab: BaseTabType) => void
|
||||
pushRef?: (tabButton: HTMLButtonElement | null) => void
|
||||
}
|
||||
|
||||
export const CodeTab = ({
|
||||
label,
|
||||
value,
|
||||
isSelected = false,
|
||||
blockStyle = "loud",
|
||||
changeSelectedTab,
|
||||
pushRef,
|
||||
}: CodeTabProps) => {
|
||||
const { colorMode } = useColorMode()
|
||||
const { blockElementScrollPositionUntilNextRender } =
|
||||
useScrollPositionBlocker()
|
||||
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
className={clsx(
|
||||
"text-compact-small-plus xs:border-0 py-docs_0.25 px-docs_0.75 relative rounded-full border",
|
||||
!isSelected && [
|
||||
"text-medusa-code-text-subtle border-transparent",
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" &&
|
||||
"text-medusa-code-text-subtle hover:bg-medusa-code-bg-base",
|
||||
colorMode === "dark" &&
|
||||
"text-medusa-fg-muted hover:bg-medusa-bg-component",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" &&
|
||||
"text-medusa-fg-subtle hover:bg-medusa-bg-base",
|
||||
colorMode === "dark" &&
|
||||
"text-medusa-code-text-subtle hover:bg-medusa-code-bg-base",
|
||||
],
|
||||
],
|
||||
isSelected && [
|
||||
"xs:!bg-transparent",
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" &&
|
||||
"border-medusa-code-border text-medusa-code-text-base",
|
||||
colorMode === "dark" &&
|
||||
"border-medusa-border-base text-medusa-fg-base",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" &&
|
||||
"xs:border-medusa-border-base text-medusa-code-text-base",
|
||||
colorMode === "dark" &&
|
||||
"xs:border-medusa-code-border text-medusa-code-text-base",
|
||||
],
|
||||
]
|
||||
)}
|
||||
ref={(tabButton) => pushRef?.(tabButton)}
|
||||
onClick={(e) => {
|
||||
blockElementScrollPositionUntilNextRender(
|
||||
e.target as HTMLButtonElement
|
||||
)
|
||||
changeSelectedTab?.({ label, value })
|
||||
}}
|
||||
aria-selected={isSelected}
|
||||
role="tab"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +1,82 @@
|
||||
"use client"
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import React, { Children, useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import {
|
||||
BaseTabType,
|
||||
CodeBlockProps,
|
||||
CodeBlockStyle,
|
||||
useColorMode,
|
||||
useTabs,
|
||||
} from "../.."
|
||||
import clsx from "clsx"
|
||||
import { CodeBlock, CodeBlockProps } from "@/components"
|
||||
import { useTabs, BaseTabType, useScrollPositionBlocker } from "@/hooks"
|
||||
import { CodeBlockHeader } from "../CodeBlock/Header"
|
||||
|
||||
export type TabType = {
|
||||
code?: CodeBlockProps
|
||||
codeBlock?: React.ReactNode
|
||||
} & BaseTabType
|
||||
type CodeTab = BaseTabType & {
|
||||
codeProps: CodeBlockProps
|
||||
codeBlock: React.ReactNode
|
||||
}
|
||||
|
||||
export type CodeTabsProps = {
|
||||
tabs: TabType[]
|
||||
type CodeTabProps = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
group?: string
|
||||
title?: string
|
||||
blockStyle?: CodeBlockStyle
|
||||
}
|
||||
|
||||
export const CodeTabs = ({
|
||||
tabs,
|
||||
children,
|
||||
className,
|
||||
group = "client",
|
||||
}: CodeTabsProps) => {
|
||||
const { selectedTab, changeSelectedTab } = useTabs<TabType>({
|
||||
title,
|
||||
blockStyle = "loud",
|
||||
}: CodeTabProps) => {
|
||||
const { colorMode } = useColorMode()
|
||||
const tabs: CodeTab[] = useMemo(() => {
|
||||
const tempTabs: CodeTab[] = []
|
||||
Children.forEach(children, (child) => {
|
||||
if (
|
||||
!React.isValidElement(child) ||
|
||||
!child.props.label ||
|
||||
!child.props.value ||
|
||||
!React.isValidElement(child.props.children)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// extract child code block
|
||||
const codeBlock =
|
||||
child.props.children.type === "pre" &&
|
||||
React.isValidElement(child.props.children.props.children)
|
||||
? child.props.children.props.children
|
||||
: child.props.children
|
||||
|
||||
tempTabs.push({
|
||||
label: child.props.label,
|
||||
value: child.props.value,
|
||||
codeProps: codeBlock.props,
|
||||
codeBlock: {
|
||||
...codeBlock,
|
||||
props: {
|
||||
...codeBlock.props,
|
||||
title: undefined,
|
||||
className: clsx("!mt-0 !rounded-t-none", codeBlock.props.className),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return tempTabs
|
||||
}, [children])
|
||||
|
||||
const { selectedTab, changeSelectedTab } = useTabs<CodeTab>({
|
||||
tabs,
|
||||
group,
|
||||
})
|
||||
|
||||
const tabRefs: (HTMLButtonElement | null)[] = useMemo(() => [], [])
|
||||
const codeTabSelectorRef = useRef<HTMLSpanElement | null>(null)
|
||||
const codeTabsWrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const { blockElementScrollPositionUntilNextRender } =
|
||||
useScrollPositionBlocker()
|
||||
|
||||
const changeTabSelectorCoordinates = useCallback(
|
||||
(selectedTabElm: HTMLElement) => {
|
||||
@@ -71,59 +118,54 @@ export const CodeTabs = ({
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
"xs:absolute xs:border xs:border-solid xs:border-medusa-code-border xs:bg-medusa-code-bg-base",
|
||||
"xs:transition-all xs:duration-200 xs:ease-ease xs:top-[13px] xs:rounded-full"
|
||||
"xs:absolute xs:border xs:border-solid",
|
||||
"xs:transition-all xs:duration-200 xs:ease-ease xs:top-[13px] xs:rounded-full",
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" &&
|
||||
"xs:border-medusa-code-border xs:bg-medusa-code-bg-base",
|
||||
colorMode === "dark" &&
|
||||
"xs:border-medusa-border-base xs:bg-medusa-bg-component",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" &&
|
||||
"xs:border-medusa-border-base xs:bg-medusa-bg-base",
|
||||
colorMode === "dark" &&
|
||||
"xs:border-medusa-code-border xs:bg-medusa-code-bg-base",
|
||||
]
|
||||
)}
|
||||
ref={codeTabSelectorRef}
|
||||
></span>
|
||||
<ul
|
||||
className={clsx(
|
||||
"bg-medusa-code-bg-header py-docs_0.75 flex !list-none rounded-t-docs_DEFAULT px-docs_1",
|
||||
"border-medusa-code-border border border-b-0",
|
||||
"gap-docs_0.25 mb-0"
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={clsx(
|
||||
"text-compact-small-plus xs:border-0 py-docs_0.25 px-docs_0.75 relative rounded-full border",
|
||||
(!selectedTab || selectedTab.value !== tab.value) && [
|
||||
"text-medusa-code-text-subtle border-transparent",
|
||||
"hover:bg-medusa-code-bg-base",
|
||||
],
|
||||
selectedTab?.value === tab.value && [
|
||||
"text-medusa-code-text-base bg-medusa-code-bg-base xs:!bg-transparent",
|
||||
"xs:!bg-transparent",
|
||||
]
|
||||
)}
|
||||
ref={(tabControl) => tabRefs.push(tabControl)}
|
||||
onClick={(e) => {
|
||||
blockElementScrollPositionUntilNextRender(
|
||||
e.target as HTMLButtonElement
|
||||
)
|
||||
changeSelectedTab(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}</>}
|
||||
</>
|
||||
<CodeBlockHeader title={selectedTab?.codeProps?.title || title}>
|
||||
<ul
|
||||
className={clsx(
|
||||
"!list-none flex gap-docs_0.25 items-center",
|
||||
"p-0 mb-0"
|
||||
)}
|
||||
>
|
||||
{Children.map(children, (child, index) => {
|
||||
if (!React.isValidElement(child)) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<child.type
|
||||
{...child.props}
|
||||
changeSelectedTab={changeSelectedTab}
|
||||
pushRef={(tabButton: HTMLButtonElement | null) =>
|
||||
tabRefs.push(tabButton)
|
||||
}
|
||||
blockStyle={blockStyle}
|
||||
isSelected={
|
||||
!selectedTab
|
||||
? index === 0
|
||||
: selectedTab.value === child.props.value
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</CodeBlockHeader>
|
||||
{selectedTab?.codeBlock}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import React, { Suspense, cloneElement, useRef, useState } from "react"
|
||||
import { Loading } from "@/components/Loading"
|
||||
import { Loading } from "@/components"
|
||||
import clsx from "clsx"
|
||||
import { CSSTransition } from "react-transition-group"
|
||||
import { DetailsSummary } from "./Summary"
|
||||
@@ -66,6 +66,7 @@ export const Details = ({
|
||||
>
|
||||
{summaryContent && (
|
||||
<DetailsSummary
|
||||
open={open}
|
||||
onClick={handleToggle}
|
||||
className="cursor-pointer"
|
||||
title={summaryContent}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { Details, MarkdownContent } from "@/components"
|
||||
|
||||
type TroubleshootingSection = {
|
||||
title: string
|
||||
content: React.ReactNode
|
||||
}
|
||||
|
||||
type DetailsListProps = {
|
||||
sections: TroubleshootingSection[]
|
||||
} & React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const DetailsList = ({ sections }: DetailsListProps) => {
|
||||
return (
|
||||
<>
|
||||
{sections.map(({ title, content }, index) => (
|
||||
<Details
|
||||
summaryContent={title}
|
||||
key={index}
|
||||
className={clsx(index !== 0 && "border-t-0")}
|
||||
>
|
||||
{React.isValidElement(content) && content}
|
||||
{!React.isValidElement(content) && typeof content === "string" && (
|
||||
<MarkdownContent>{content}</MarkdownContent>
|
||||
)}
|
||||
</Details>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react"
|
||||
import { Button } from "../.."
|
||||
|
||||
type EditButtonProps = {
|
||||
filePath: string
|
||||
}
|
||||
|
||||
export const EditButton = ({ filePath }: EditButtonProps) => {
|
||||
return (
|
||||
<Button variant="secondary" className="mb-docs_1">
|
||||
<a href={`https://github.com/medusajs/medusa/edit/develop${filePath}`}>
|
||||
Edit this page
|
||||
</a>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react"
|
||||
import { Badge, Link, Tooltip } from "@/components"
|
||||
import { Badge, LegacyLink, Tooltip } from "@/components"
|
||||
|
||||
export type ExpandableNoticeProps = {
|
||||
type: "request" | "method"
|
||||
@@ -20,8 +20,11 @@ export const ExpandableNotice = ({
|
||||
<>
|
||||
If this {type} accepts an <code>expand</code>{" "}
|
||||
{type === "request" ? "parameter" : "property or option"},
|
||||
<br /> this field can be <Link href={link}>expanded</Link> into an
|
||||
object.
|
||||
{/* TODO replace with Link once we move away from Docusaurus */}
|
||||
<br /> this field can be <LegacyLink href={link}>
|
||||
expanded
|
||||
</LegacyLink>{" "}
|
||||
into an object.
|
||||
</>
|
||||
}
|
||||
clickable
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react"
|
||||
import { Badge, Link, Tooltip } from "@/components"
|
||||
import { Badge, LegacyLink, Tooltip } from "@/components"
|
||||
|
||||
export type FeatureFlagNoticeProps = {
|
||||
featureFlag: string
|
||||
type?: "endpoint" | "parameter"
|
||||
type?: "endpoint" | "type"
|
||||
tooltipTextClassName?: string
|
||||
badgeClassName?: string
|
||||
badgeContent?: React.ReactNode
|
||||
@@ -22,12 +22,13 @@ export const FeatureFlagNotice = ({
|
||||
<span className={tooltipTextClassName}>
|
||||
To use this {type}, make sure to
|
||||
<br />
|
||||
<Link
|
||||
{/* TODO replace with Link once we move away from Docusaurus */}
|
||||
<LegacyLink
|
||||
href="https://docs.medusajs.com/development/feature-flags/toggle"
|
||||
target="__blank"
|
||||
>
|
||||
enable its feature flag: <code>{featureFlag}</code>
|
||||
</Link>
|
||||
</LegacyLink>
|
||||
</span>
|
||||
}
|
||||
clickable
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { request } from "@octokit/request"
|
||||
import { Link } from "@/components/Link"
|
||||
import { LegacyLink } from "@/components"
|
||||
|
||||
export type SolutionsProps = {
|
||||
feedback: boolean
|
||||
@@ -69,9 +69,14 @@ export const Solutions = ({ feedback, message }: SolutionsProps) => {
|
||||
<ul>
|
||||
{possibleSolutions.map((solution) => (
|
||||
<li key={solution.url} className="mb-docs_0.5 last:mb-0">
|
||||
<Link href={solution.html_url} target="_blank" rel="noreferrer">
|
||||
{/* TODO replace with Link once we move away from Docusaurus */}
|
||||
<LegacyLink
|
||||
href={solution.html_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{solution.title}
|
||||
</Link>
|
||||
</LegacyLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -5,11 +5,7 @@ import { CSSTransition, SwitchTransition } from "react-transition-group"
|
||||
import { Solutions } from "./Solutions"
|
||||
import { ExtraData, useAnalytics } from "@/providers/Analytics"
|
||||
import clsx from "clsx"
|
||||
import { TextArea } from "@/components/TextArea"
|
||||
import { Label } from "@/components/Label"
|
||||
import { Button } from "docs-ui"
|
||||
import { Details } from "@/components/Details"
|
||||
import { InputText } from "@/components/Input/Text"
|
||||
import { Button, TextArea, Label, Details, InputText } from "@/components"
|
||||
|
||||
export type FeedbackProps = {
|
||||
event: string
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useCurrentLearningPath, usePageScrollManager } from "../../hooks"
|
||||
|
||||
type HooksLoaderProps = {
|
||||
options?: {
|
||||
pageScrollManager?: boolean
|
||||
currentLearningPath?: boolean
|
||||
}
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const HooksLoader = ({ children, options = {} }: HooksLoaderProps) => {
|
||||
const { pageScrollManager, currentLearningPath } = options
|
||||
// load any hooks that require providers to be loaded here.
|
||||
if (pageScrollManager) {
|
||||
usePageScrollManager()
|
||||
}
|
||||
if (currentLearningPath) {
|
||||
useCurrentLearningPath()
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { IconProps } from "@medusajs/icons/dist/types"
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
|
||||
export const IconCircleDottedLine = (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}
|
||||
className={clsx("text-ui-fg-subtle", props.className)}
|
||||
>
|
||||
<path
|
||||
d="M12.5 2.93589C10.884 2.3547 9.116 2.3547 7.5 2.93589"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.63049 8.63198C2.94295 6.94471 3.82573 5.41606 5.13094 4.30209"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.13094 15.6979C3.82575 14.5839 2.94298 13.0552 2.63049 11.368"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 17.0641C9.116 17.6453 10.884 17.6453 12.5 17.0641"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M17.3695 8.63198C17.057 6.94471 16.1742 5.41606 14.869 4.30209"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.869 15.6979C16.1742 14.5839 17.057 13.0552 17.3695 11.368"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./CircleDottedLine"
|
||||
export * from "./QuestionMark"
|
||||
export * from "./ThumbDown"
|
||||
export * from "./ThumbUp"
|
||||
@@ -11,7 +11,8 @@ export const InlineCode = (props: InlineCodeProps) => {
|
||||
<CopyButton
|
||||
text={props.children as string}
|
||||
buttonClassName={clsx(
|
||||
"bg-transparent border-0 p-0 inline text-medusa-fg-subtle group"
|
||||
"bg-transparent border-0 p-0 inline text-medusa-fg-subtle group",
|
||||
"font-monospace"
|
||||
)}
|
||||
>
|
||||
<code
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react"
|
||||
import { IconProps } from "@medusajs/icons/dist/types"
|
||||
import clsx from "clsx"
|
||||
|
||||
type InlineIconProps = IconProps & {
|
||||
Icon: React.ComponentType<IconProps>
|
||||
alt?: string
|
||||
}
|
||||
|
||||
export const InlineIcon = ({ Icon, alt, ...props }: InlineIconProps) => {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={clsx("text-medusa-fg-subtle inline", props.className)}
|
||||
aria-label={alt}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { ThemeImage, ThemeImageProps } from "../.."
|
||||
|
||||
export const InlineThemeImage = (props: ThemeImageProps) => {
|
||||
return (
|
||||
<ThemeImage
|
||||
{...props}
|
||||
width={20}
|
||||
height={20}
|
||||
className={clsx(props.className, "inline")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export const Kbd = ({ children, className, ...props }: KbdProps) => {
|
||||
"border-medusa-tag-neutral-border border",
|
||||
"bg-medusa-tag-neutral-bg",
|
||||
"text-medusa-tag-neutral-text",
|
||||
"text-compact-x-small-plus font-monospace shadow-none",
|
||||
"text-compact-x-small-plus font-base shadow-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { LearningPathStepType } from "@/providers"
|
||||
import { Rating } from "docs-ui"
|
||||
import React from "react"
|
||||
|
||||
export type LearningPathFinishType =
|
||||
| {
|
||||
type: "rating"
|
||||
step: Omit<LearningPathStepType, "descriptionJSX"> & {
|
||||
eventName?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: "custom"
|
||||
step: LearningPathStepType & {
|
||||
descriptionJSX: JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
type LearningPathFinishProps = LearningPathFinishType & {
|
||||
onRating?: () => void
|
||||
}
|
||||
|
||||
export const LearningPathFinish = ({
|
||||
type,
|
||||
step,
|
||||
onRating,
|
||||
}: LearningPathFinishProps) => {
|
||||
return (
|
||||
<>
|
||||
{type === "rating" && (
|
||||
<Rating event={step.eventName} onRating={onRating} />
|
||||
)}
|
||||
{type === "custom" && (
|
||||
<span className="text-compact-small text-medusa-fg-subtle">
|
||||
{step.descriptionJSX}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { useLearningPath } from "../../.."
|
||||
|
||||
type LearningPathIconProps = {
|
||||
className?: string
|
||||
imgClassName?: string
|
||||
} & React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const LearningPathIcon = ({
|
||||
className = "",
|
||||
imgClassName = "",
|
||||
}: LearningPathIconProps) => {
|
||||
const { baseUrl } = useLearningPath()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"rounded-full shadow-card-rest dark:shadow-card-rest-dark w-docs_3 h-docs_3 bg-medusa-bg-base",
|
||||
"flex justify-center items-center flex-none",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={`${baseUrl}/images/learning-path-img.png`}
|
||||
className={clsx("rounded-full w-docs_2.5 h-docs_2.5", imgClassName)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { getLearningPaths } from "../../../utils/learning-paths"
|
||||
import { LearningPath } from ".."
|
||||
|
||||
type LearningPathListProps = {
|
||||
ignore?: string[]
|
||||
} & React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const LearningPathList = ({ ignore = [] }: LearningPathListProps) => {
|
||||
const paths = useMemo(() => {
|
||||
const paths = getLearningPaths()
|
||||
ignore.forEach((pathName) => {
|
||||
const pathIndex = paths.findIndex((path) => path.name === pathName)
|
||||
if (pathIndex !== -1) {
|
||||
paths.splice(pathIndex, 1)
|
||||
}
|
||||
})
|
||||
|
||||
return paths
|
||||
}, [ignore])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-wrap gap-docs_2 mt-docs_1.5">
|
||||
{paths.map((path, index) => (
|
||||
<LearningPath
|
||||
pathName={path.name}
|
||||
key={index}
|
||||
className="!mt-0 !mb-0"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react"
|
||||
import { useLearningPath } from "../../../../providers/LearningPath"
|
||||
import { Button } from "docs-ui"
|
||||
import { ArrowDownLeftMini, ArrowDownMini } from "@medusajs/icons"
|
||||
|
||||
type LearningPathStepActionsType = {
|
||||
onFinish?: () => void
|
||||
onClose?: () => void
|
||||
setCollapsed: React.Dispatch<React.SetStateAction<boolean>>
|
||||
} & React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const LearningPathStepActions = ({
|
||||
onFinish,
|
||||
onClose,
|
||||
setCollapsed,
|
||||
}: LearningPathStepActionsType) => {
|
||||
const { hasNextStep, nextStep, endPath } = useLearningPath()
|
||||
|
||||
const handleFinish = () => {
|
||||
if (onFinish) {
|
||||
onFinish()
|
||||
} else {
|
||||
endPath()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex p-docs_1 justify-between items-center">
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => setCollapsed(true)}
|
||||
variant="secondary"
|
||||
className="!text-medusa-fg-subtle !p-[6px]"
|
||||
>
|
||||
<ArrowDownLeftMini className="flip-y hidden md:inline" />
|
||||
<ArrowDownMini className="inline md:hidden" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-docs_0.5 items-center">
|
||||
{hasNextStep() && (
|
||||
<>
|
||||
<Button onClick={onClose} variant="secondary">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={nextStep} variant="primary">
|
||||
Next
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!hasNextStep() && (
|
||||
<Button onClick={handleFinish} variant="primary">
|
||||
Finish
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useLearningPath } from "@/providers"
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { LearningPathStepActions } from "./Actions"
|
||||
import clsx from "clsx"
|
||||
import { IconCircleDottedLine } from "@/components/Icons"
|
||||
import { CheckCircleSolid, CircleMiniSolid, ListBullet } from "@medusajs/icons"
|
||||
import { Badge, Button, Link } from "@/components"
|
||||
import { CSSTransition, SwitchTransition } from "react-transition-group"
|
||||
|
||||
type LearningPathStepsProps = {
|
||||
onFinish?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export const LearningPathSteps = ({ ...rest }: LearningPathStepsProps) => {
|
||||
const { path, currentStep, goToStep } = useLearningPath()
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const stepsRef = useRef<HTMLDivElement>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const nodeRef: React.RefObject<HTMLElement> = collapsed ? buttonRef : stepsRef
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (window.scrollY > 100 && !collapsed) {
|
||||
// automatically collapse steps
|
||||
setCollapsed(true)
|
||||
} else if (
|
||||
(window.scrollY === 0 ||
|
||||
window.scrollY + window.innerHeight >= document.body.scrollHeight) &&
|
||||
collapsed
|
||||
) {
|
||||
// automatically open steps
|
||||
setCollapsed(false)
|
||||
}
|
||||
}, [collapsed])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll)
|
||||
}
|
||||
}, [handleScroll])
|
||||
|
||||
if (!path) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<SwitchTransition>
|
||||
<CSSTransition
|
||||
key={collapsed ? "show_path" : "show_button"}
|
||||
nodeRef={nodeRef}
|
||||
timeout={300}
|
||||
addEndListener={(done) => {
|
||||
nodeRef.current?.addEventListener("transitionend", done, false)
|
||||
}}
|
||||
classNames={{
|
||||
enter: "animate-maximize animate-fast",
|
||||
exit: "animate-minimize animate-fast",
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{!collapsed && (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-medusa-bg-base shadow-flyout dark:shadow-flyout-dark rounded",
|
||||
"transition-transform origin-bottom-right flex flex-col"
|
||||
)}
|
||||
ref={stepsRef}
|
||||
>
|
||||
<div className="overflow-auto basis-3/4">
|
||||
{path.steps.map((step, index) => (
|
||||
<div
|
||||
className={clsx(
|
||||
"border-0 border-b border-solid border-medusa-border-base",
|
||||
"relative p-docs_1"
|
||||
)}
|
||||
key={index}
|
||||
>
|
||||
<div
|
||||
className={clsx("flex items-center gap-docs_1 relative")}
|
||||
>
|
||||
<div className="w-docs_2 flex-none flex items-center justify-center">
|
||||
{index === currentStep && (
|
||||
<IconCircleDottedLine
|
||||
className={clsx(
|
||||
"shadow-active dark:shadow-active-dark rounded-full",
|
||||
"!text-ui-fg-interactive"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{index < currentStep && (
|
||||
<CheckCircleSolid className="text-ui-fg-interactive" />
|
||||
)}
|
||||
{index > currentStep && (
|
||||
<CircleMiniSolid className="text-ui-fg-subtle" />
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
"text-compact-medium-plus text-medusa-fg-base"
|
||||
)}
|
||||
>
|
||||
{step.title}
|
||||
</span>
|
||||
<Link
|
||||
href={step.path}
|
||||
className={clsx("absolute top-0 left-0 w-full h-full")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
goToStep(index)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{index === currentStep && (
|
||||
<div className={clsx("flex items-center gap-docs_1")}>
|
||||
<div className="w-docs_2 flex-none"></div>
|
||||
<div
|
||||
className={clsx(
|
||||
"text-medium text-ui-fg-subtle mt-docs_1"
|
||||
)}
|
||||
>
|
||||
{step.descriptionJSX ?? step.description}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<LearningPathStepActions setCollapsed={setCollapsed} {...rest} />
|
||||
</div>
|
||||
)}
|
||||
{collapsed && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={clsx(
|
||||
"!p-[10px] !shadow-flyout dark:!shadow-flyout-dark !text-medusa-fg-subtle w-fit h-fit",
|
||||
"rounded-full border-0 mr-0 ml-auto fixed md:relative max-[767px]:bottom-docs_1 max-[767px]:right-docs_1 "
|
||||
)}
|
||||
onClick={() => setCollapsed(false)}
|
||||
buttonRef={buttonRef}
|
||||
>
|
||||
<ListBullet />
|
||||
<Badge
|
||||
variant="blue"
|
||||
className={clsx("absolute -top-docs_0.25 -right-docs_0.25")}
|
||||
>
|
||||
!
|
||||
</Badge>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</CSSTransition>
|
||||
</SwitchTransition>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client"
|
||||
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { LearningPathIcon } from "./Icon"
|
||||
import { getLearningPath } from "@/utils"
|
||||
import { useLearningPath } from "@/providers"
|
||||
import { Button, Link } from "@/components"
|
||||
import { useNotifications } from "@/providers"
|
||||
import { CircleMiniSolid } from "@medusajs/icons"
|
||||
|
||||
type LearningPathProps = {
|
||||
pathName: string
|
||||
className?: string
|
||||
} & React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const LearningPath = ({
|
||||
pathName,
|
||||
className = "",
|
||||
}: LearningPathProps) => {
|
||||
const path = getLearningPath(pathName)
|
||||
if (!path) {
|
||||
throw new Error(`Learning path ${pathName} does not exist.`)
|
||||
}
|
||||
const { startPath, path: currentPath } = useLearningPath()
|
||||
const notificationContext = useNotifications()
|
||||
|
||||
const handleClick = () => {
|
||||
if (notificationContext && currentPath?.notificationId) {
|
||||
notificationContext.removeNotification(currentPath.notificationId)
|
||||
}
|
||||
startPath(path)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"rounded shadow-card-rest dark:shadow-card-rest-dark bg-docs-bg-surface mt-docs_1.5 mb-docs_4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center gap-docs_1 p-docs_1 border-0 border-b border-solid border-medusa-border-base"
|
||||
)}
|
||||
>
|
||||
<LearningPathIcon />
|
||||
<div className={clsx("basis-3/4")}>
|
||||
<span
|
||||
className={clsx(
|
||||
"text-medusa-fg-base text-compact-large-plus block"
|
||||
)}
|
||||
>
|
||||
{path.label}
|
||||
</span>
|
||||
{path.description && (
|
||||
<span
|
||||
className={clsx(
|
||||
"text-medusa-fg-subtle text-compact-medium mt-docs_0.25 inline-block"
|
||||
)}
|
||||
>
|
||||
{path.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button onClick={handleClick} className={clsx("basis-1/4 max-w-fit")}>
|
||||
Start Path
|
||||
</Button>
|
||||
</div>
|
||||
{path.steps.map((step, index) => (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center p-docs_1 gap-docs_1 relative",
|
||||
index !== path.steps.length - 1 &&
|
||||
"border-0 border-b border-solid border-medusa-border-base"
|
||||
)}
|
||||
key={index}
|
||||
>
|
||||
<div className={clsx("w-docs_3 flex items-center justify-center")}>
|
||||
<CircleMiniSolid className="text-medusa-fg-muted" />
|
||||
</div>
|
||||
<span
|
||||
className={clsx("text-medusa-fg-base text-compact-medium-plus")}
|
||||
>
|
||||
{step.title}
|
||||
</span>
|
||||
<Link
|
||||
href={step.path}
|
||||
className={clsx("absolute top-0 left-0 w-full h-full")}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
"use client"
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import clsx from "clsx"
|
||||
import { CodeBlock, CodeBlockProps, CodeBlockStyle } from "@/components"
|
||||
import { useTabs, BaseTabType, useScrollPositionBlocker } from "@/hooks"
|
||||
import { CodeBlockHeader } from "../CodeBlock/Header"
|
||||
import { useColorMode } from "../../providers"
|
||||
|
||||
export type LegacyTabType = {
|
||||
code?: CodeBlockProps
|
||||
codeBlock?: React.ReactNode
|
||||
} & BaseTabType
|
||||
|
||||
export type LegacyCodeTabsProps = {
|
||||
tabs: LegacyTabType[]
|
||||
className?: string
|
||||
group?: string
|
||||
title?: string
|
||||
blockStyle?: CodeBlockStyle
|
||||
}
|
||||
|
||||
export const LegacyCodeTabs = ({
|
||||
tabs,
|
||||
className,
|
||||
group = "client",
|
||||
title,
|
||||
blockStyle = "loud",
|
||||
}: LegacyCodeTabsProps) => {
|
||||
const { colorMode } = useColorMode()
|
||||
const { selectedTab, changeSelectedTab } = useTabs<LegacyTabType>({
|
||||
tabs,
|
||||
group,
|
||||
})
|
||||
const tabRefs: (HTMLButtonElement | null)[] = useMemo(() => [], [])
|
||||
const codeTabSelectorRef = useRef<HTMLSpanElement | null>(null)
|
||||
const codeTabsWrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const { blockElementScrollPositionUntilNextRender } =
|
||||
useScrollPositionBlocker()
|
||||
|
||||
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-docs_1 w-full max-w-full overflow-auto",
|
||||
className
|
||||
)}
|
||||
ref={codeTabsWrapperRef}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
"xs:absolute xs:border xs:border-solid",
|
||||
"xs:transition-all xs:duration-200 xs:ease-ease xs:top-[13px] xs:rounded-full",
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" &&
|
||||
"xs:border-medusa-code-border xs:bg-medusa-code-bg-base",
|
||||
colorMode === "dark" &&
|
||||
"xs:border-medusa-border-base xs:bg-medusa-bg-component",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" &&
|
||||
"xs:border-medusa-border-base xs:bg-medusa-bg-base",
|
||||
colorMode === "dark" &&
|
||||
"xs:border-medusa-code-border xs:bg-medusa-code-bg-base",
|
||||
]
|
||||
)}
|
||||
ref={codeTabSelectorRef}
|
||||
></span>
|
||||
<CodeBlockHeader title={selectedTab?.code?.title || title}>
|
||||
<ul
|
||||
className={clsx(
|
||||
"!list-none flex gap-docs_0.25 items-center",
|
||||
"p-0 mb-0"
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={clsx(
|
||||
"text-compact-small-plus xs:border-0 py-docs_0.25 px-docs_0.75 relative rounded-full border",
|
||||
(!selectedTab || selectedTab.value !== tab.value) && [
|
||||
"text-medusa-code-text-subtle border-transparent",
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" &&
|
||||
"text-medusa-code-text-subtle hover:bg-medusa-code-bg-base",
|
||||
colorMode === "dark" &&
|
||||
"text-medusa-fg-muted hover:bg-medusa-bg-component",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" &&
|
||||
"text-medusa-fg-subtle hover:bg-medusa-bg-base",
|
||||
colorMode === "dark" &&
|
||||
"text-medusa-code-text-subtle hover:bg-medusa-code-bg-base",
|
||||
],
|
||||
],
|
||||
selectedTab?.value === tab.value && [
|
||||
"xs:!bg-transparent",
|
||||
blockStyle === "loud" && [
|
||||
colorMode === "light" &&
|
||||
"border-medusa-code-border text-medusa-code-text-base",
|
||||
colorMode === "dark" &&
|
||||
"border-medusa-border-base text-medusa-fg-base",
|
||||
],
|
||||
blockStyle === "subtle" && [
|
||||
colorMode === "light" &&
|
||||
"xs:border-medusa-border-base text-medusa-code-text-base",
|
||||
colorMode === "dark" &&
|
||||
"xs:border-medusa-code-border text-medusa-code-text-base",
|
||||
],
|
||||
]
|
||||
)}
|
||||
ref={(tabControl) => tabRefs.push(tabControl)}
|
||||
onClick={(e) => {
|
||||
blockElementScrollPositionUntilNextRender(
|
||||
e.target as HTMLButtonElement
|
||||
)
|
||||
changeSelectedTab(tab)
|
||||
}}
|
||||
aria-selected={selectedTab?.value === tab.value}
|
||||
role="tab"
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CodeBlockHeader>
|
||||
<>
|
||||
{selectedTab?.code && (
|
||||
<CodeBlock
|
||||
{...selectedTab?.code}
|
||||
title={undefined}
|
||||
className={clsx(
|
||||
"!mt-0 !rounded-t-none",
|
||||
selectedTab.code.className
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{selectedTab?.codeBlock && <>{selectedTab.codeBlock}</>}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+10
-8
@@ -1,18 +1,20 @@
|
||||
import React from "react"
|
||||
import NextLink from "next/link"
|
||||
import type { LinkProps as NextLinkProps } from "next/link"
|
||||
import clsx from "clsx"
|
||||
|
||||
export type LinkProps = {
|
||||
export type LegacyLinkProps = {
|
||||
href?: string
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
} & Partial<NextLinkProps> &
|
||||
React.AllHTMLAttributes<HTMLAnchorElement>
|
||||
} & React.AllHTMLAttributes<HTMLAnchorElement>
|
||||
|
||||
export const Link = ({ href, children, className, ...rest }: LinkProps) => {
|
||||
export const LegacyLink = ({
|
||||
href,
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: LegacyLinkProps) => {
|
||||
return (
|
||||
<NextLink
|
||||
<a
|
||||
href={href || ""}
|
||||
{...rest}
|
||||
className={clsx(
|
||||
@@ -21,6 +23,6 @@ export const Link = ({ href, children, className, ...rest }: LinkProps) => {
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</NextLink>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from "react"
|
||||
import NextLink from "next/link"
|
||||
import type { LinkProps as NextLinkProps } from "next/link"
|
||||
import clsx from "clsx"
|
||||
|
||||
export type LinkProps = {
|
||||
href?: string
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
} & React.AllHTMLAttributes<HTMLAnchorElement>
|
||||
target?: string
|
||||
rel?: string
|
||||
} & Partial<NextLinkProps>
|
||||
|
||||
export const Link = ({ href, children, className, ...rest }: LinkProps) => {
|
||||
if (href?.replace(/#.*$/, "").endsWith("page.mdx")) {
|
||||
href = href.replace("/page.mdx", "")
|
||||
}
|
||||
return (
|
||||
<a
|
||||
<NextLink
|
||||
href={href || ""}
|
||||
{...rest}
|
||||
className={clsx(
|
||||
@@ -18,6 +25,6 @@ export const Link = ({ href, children, className, ...rest }: LinkProps) => {
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import React from "react"
|
||||
import type { MDXComponents as MDXComponentsType } from "mdx/types"
|
||||
import {
|
||||
CodeMdx,
|
||||
Details,
|
||||
Kbd,
|
||||
Note,
|
||||
Card,
|
||||
CardList,
|
||||
LegacyLink,
|
||||
DetailsSummary,
|
||||
DetailsProps,
|
||||
ZoomImg,
|
||||
} from "@/components"
|
||||
import clsx from "clsx"
|
||||
import { Text } from "@medusajs/ui"
|
||||
|
||||
export const MDXComponents: MDXComponentsType = {
|
||||
code: CodeMdx,
|
||||
kbd: Kbd,
|
||||
Kbd,
|
||||
Note,
|
||||
details: Details,
|
||||
Details: ({ className, ...props }: DetailsProps) => {
|
||||
return <Details {...props} className={clsx(className, "my-docs_1")} />
|
||||
},
|
||||
Summary: DetailsSummary,
|
||||
Card,
|
||||
CardList,
|
||||
h1: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => {
|
||||
return (
|
||||
<h1
|
||||
className={clsx(
|
||||
"h1-docs [&_code]:!h1-docs [&_code]:!font-mono mb-docs_1 text-medusa-fg-base",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
h2: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLHeadingElement>) => {
|
||||
return (
|
||||
<h2
|
||||
className={clsx(
|
||||
"h2-docs [&_code]:!h2-docs [&_code]:!font-mono mb-docs_1 mt-docs_4 text-medusa-fg-base",
|
||||
props.id && "group/h2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{props.id && (
|
||||
// TODO replace with Link once we move away from Docusaurus
|
||||
<LegacyLink
|
||||
href={`#${props.id}`}
|
||||
className="opacity-0 group-hover/h2:opacity-100 transition-opacity ml-docs_0.5 inline-block"
|
||||
>
|
||||
#
|
||||
</LegacyLink>
|
||||
)}
|
||||
</h2>
|
||||
)
|
||||
},
|
||||
h3: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLHeadingElement>) => {
|
||||
return (
|
||||
<h3
|
||||
className={clsx(
|
||||
"h3-docs [&_code]:!h3-docs [&_code]:!font-mono mb-docs_0.5 mt-docs_3 text-medusa-fg-base",
|
||||
props.id && "group/h3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{props.id && (
|
||||
// TODO replace with Link once we move away from Docusaurus
|
||||
<LegacyLink
|
||||
href={`#${props.id}`}
|
||||
className="opacity-0 group-hover/h3:opacity-100 transition-opacity ml-docs_0.5 inline-block"
|
||||
>
|
||||
#
|
||||
</LegacyLink>
|
||||
)}
|
||||
</h3>
|
||||
)
|
||||
},
|
||||
h4: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => {
|
||||
return (
|
||||
<h4
|
||||
className={clsx("mb-docs_0.5 text-medusa-fg-base text-h4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
p: ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => {
|
||||
return (
|
||||
<p
|
||||
className={clsx(
|
||||
"text-medusa-fg-subtle [&:not(:last-child)]:mb-docs_1.5 last:!mb-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
ul: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLUListElement>) => {
|
||||
return (
|
||||
<ul
|
||||
{...props}
|
||||
className={clsx(
|
||||
"list-disc px-docs_1 mb-docs_1.5 [&_ul]:mb-0 [&_ol]:mb-0 [&_p]:!mb-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
},
|
||||
ol: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLOListElement>) => {
|
||||
return (
|
||||
<ol
|
||||
{...props}
|
||||
className={clsx(
|
||||
"list-decimal px-docs_1 mb-docs_1.5 [&_ul]:mb-0 [&_ol]:mb-0 [&_p]:!mb-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ol>
|
||||
)
|
||||
},
|
||||
li: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) => {
|
||||
return (
|
||||
<li className={clsx("text-medusa-fg-subtle", className)} {...props}>
|
||||
<Text as="span">{children}</Text>
|
||||
</li>
|
||||
)
|
||||
},
|
||||
hr: ({ className, ...props }: React.HTMLAttributes<HTMLHRElement>) => {
|
||||
return (
|
||||
<hr
|
||||
className={clsx(
|
||||
"my-docs_2 h-[1px] w-full border-0 bg-medusa-border-base",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
img: (
|
||||
props: React.DetailedHTMLProps<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
HTMLImageElement
|
||||
>
|
||||
) => {
|
||||
// omit key to resolve errors
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { key, ...rest } = props
|
||||
return <ZoomImg {...rest} />
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import { ReactMarkdownOptions } from "react-markdown/lib/react-markdown"
|
||||
import { CodeMdx, Details, Kbd, Link } from "@/components"
|
||||
import { MDXComponents, LegacyLink } from "@/components"
|
||||
import clsx from "clsx"
|
||||
import { NormalComponents } from "react-markdown/lib/complex-types"
|
||||
import { SpecialComponents } from "react-markdown/lib/ast-to-react"
|
||||
@@ -21,7 +21,7 @@ export const MarkdownContent = ({
|
||||
<ReactMarkdown
|
||||
components={
|
||||
components || {
|
||||
code: CodeMdx,
|
||||
...MDXComponents,
|
||||
pre: ({
|
||||
className,
|
||||
children,
|
||||
@@ -33,68 +33,8 @@ export const MarkdownContent = ({
|
||||
</pre>
|
||||
)
|
||||
},
|
||||
kbd: Kbd,
|
||||
details: Details,
|
||||
a: Link,
|
||||
ul: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLUListElement>) => {
|
||||
return (
|
||||
<ul
|
||||
{...props}
|
||||
className={clsx("list-disc px-docs_1 mb-docs_1.5", className)}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
},
|
||||
ol: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLOListElement>) => {
|
||||
return (
|
||||
<ol
|
||||
{...props}
|
||||
className={clsx(
|
||||
"list-decimal px-docs_1 mb-docs_1.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ol>
|
||||
)
|
||||
},
|
||||
li: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLLIElement>) => {
|
||||
return (
|
||||
<li
|
||||
className={clsx("text-medusa-fg-subtle", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>{children}</span>
|
||||
</li>
|
||||
)
|
||||
},
|
||||
p: ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLParagraphElement>) => {
|
||||
return (
|
||||
<p
|
||||
className={clsx(
|
||||
"text-medusa-fg-subtle [&:not(:last-child)]:mb-docs_1.5 last:!mb-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
// TODO replace with Link once we move away from Docusaurus
|
||||
a: LegacyLink,
|
||||
}
|
||||
}
|
||||
{...props}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client"
|
||||
import React, {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { Loading } from "@/components"
|
||||
import mermaid from "mermaid"
|
||||
import type { RenderResult } from "mermaid"
|
||||
import { Controlled as ControlledZoom } from "react-medium-image-zoom"
|
||||
import "react-medium-image-zoom/dist/styles.css"
|
||||
import clsx from "clsx"
|
||||
|
||||
type MermaidDiagramProps = {
|
||||
diagramContent: string
|
||||
}
|
||||
|
||||
const VIEWBOX_REGEX = /viewBox="([0-9.-]+\s*){4}"/
|
||||
|
||||
export const MermaidDiagram = ({ diagramContent }: MermaidDiagramProps) => {
|
||||
const [result, setResult] = useState<RenderResult | null>(null)
|
||||
const [isZoomed, setIsZoomed] = useState(false)
|
||||
const mermaidId = useRef(
|
||||
`mermaid-svg-${Math.round(Math.random() * 10000000)}`
|
||||
).current!
|
||||
|
||||
useEffect(() => {
|
||||
mermaid.mermaidAPI.initialize()
|
||||
|
||||
mermaid
|
||||
.render(mermaidId, diagramContent)
|
||||
.then(setResult)
|
||||
.catch((e) =>
|
||||
console.error(
|
||||
`An error occurred while rendering Mermaid.js diagram. Content: \n ${diagramContent}\n Error: ${e}`
|
||||
)
|
||||
)
|
||||
}, [mermaidId, diagramContent])
|
||||
|
||||
const matchedRegex = useMemo(() => {
|
||||
return result ? VIEWBOX_REGEX.exec(result.svg) : undefined
|
||||
}, [result])
|
||||
|
||||
const handleZoomChange = useCallback((shouldZoom: boolean) => {
|
||||
setIsZoomed(shouldZoom)
|
||||
}, [])
|
||||
|
||||
console.log(matchedRegex, result?.svg)
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<ControlledZoom
|
||||
isZoomed={isZoomed}
|
||||
onZoomChange={handleZoomChange}
|
||||
classDialog={clsx(
|
||||
"[&_data-rmiz-modal-img]:!top-0 [&_data-rmiz-modal-img]:!left-0 [&_data-rmiz-modal-img]:!transform-x-0",
|
||||
["[&_data-rmiz-modal-img]:!transform-y-0 [&_data-rmiz-modal-img]:"]
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
dangerouslySetInnerHTML={result ? { __html: result.svg } : undefined}
|
||||
width={isZoomed ? "100vw" : "100%"}
|
||||
height={
|
||||
isZoomed
|
||||
? `100vh`
|
||||
: matchedRegex && matchedRegex.length >= 1
|
||||
? `${matchedRegex[1]}px`
|
||||
: "100%"
|
||||
}
|
||||
/>
|
||||
</ControlledZoom>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { ModalHeader } from "./Header"
|
||||
import { ModalFooter } from "./Footer"
|
||||
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"
|
||||
import { ButtonProps } from "@/components"
|
||||
import { Ref } from "@/types"
|
||||
import { Ref } from "types"
|
||||
|
||||
export type ModalProps = {
|
||||
className?: string
|
||||
|
||||
@@ -1,35 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import React, { useMemo } from "react"
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { useNavbar } from "@/providers"
|
||||
import { NextLink, NextLinkProps } from "@/components"
|
||||
import { Link, LinkProps } from "@/components"
|
||||
|
||||
export type NavbarLinkProps = {
|
||||
href: string
|
||||
label: string
|
||||
className?: string
|
||||
activeValuePattern?: RegExp
|
||||
} & NextLinkProps
|
||||
isActive?: boolean
|
||||
} & LinkProps
|
||||
|
||||
export const NavbarLink = ({
|
||||
href,
|
||||
label,
|
||||
className,
|
||||
activeValuePattern,
|
||||
isActive,
|
||||
}: NavbarLinkProps) => {
|
||||
const { activeItem } = useNavbar()
|
||||
|
||||
const isActive = useMemo(() => {
|
||||
return activeItem
|
||||
? activeValuePattern
|
||||
? activeValuePattern.test(activeItem)
|
||||
: href === activeItem
|
||||
: false
|
||||
}, [activeItem, href, activeValuePattern])
|
||||
|
||||
return (
|
||||
<NextLink
|
||||
<Link
|
||||
href={href}
|
||||
className={clsx(
|
||||
isActive && "!text-medusa-fg-base",
|
||||
@@ -40,6 +30,6 @@ export const NavbarLink = ({
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</NextLink>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,8 @@ export type NavbarProps = {
|
||||
items: NavbarLinkProps[]
|
||||
showSearchOpener?: boolean
|
||||
showColorModeToggle?: boolean
|
||||
additionalActions?: React.ReactNode
|
||||
additionalActionsAfter?: React.ReactNode
|
||||
additionalActionsBefore?: React.ReactNode
|
||||
mobileMenuButton: NavbarMobileMenuButtonProps
|
||||
isLoading?: boolean
|
||||
className?: string
|
||||
@@ -23,7 +24,8 @@ export const Navbar = ({
|
||||
items,
|
||||
showSearchOpener = true,
|
||||
showColorModeToggle = true,
|
||||
additionalActions,
|
||||
additionalActionsBefore,
|
||||
additionalActionsAfter,
|
||||
mobileMenuButton,
|
||||
isLoading,
|
||||
className,
|
||||
@@ -48,11 +50,12 @@ export const Navbar = ({
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden min-w-0 flex-1 items-center justify-end gap-docs_0.5 lg:flex">
|
||||
{additionalActionsBefore}
|
||||
{showSearchOpener && (
|
||||
<NavbarSearchModalOpener isLoading={isLoading} />
|
||||
)}
|
||||
{showColorModeToggle && <NavbarColorModeToggle />}
|
||||
{additionalActions}
|
||||
{additionalActionsAfter}
|
||||
</div>
|
||||
<NavbarMobileMenu
|
||||
logo={logo}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react"
|
||||
import { NoteProps } from ".."
|
||||
import clsx from "clsx"
|
||||
|
||||
type NoteLayoutProps = NoteProps & {
|
||||
icon: React.ReactNode
|
||||
}
|
||||
|
||||
export const NoteLayout = ({
|
||||
type,
|
||||
title,
|
||||
children,
|
||||
icon,
|
||||
}: NoteLayoutProps) => {
|
||||
const isDefaultStyle =
|
||||
type === "default" ||
|
||||
type === "success" ||
|
||||
type === "error" ||
|
||||
type === "check"
|
||||
const isWarningStyle = type === "warning"
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"p-docs_1 border border-solid rounded shadow-none",
|
||||
isDefaultStyle &&
|
||||
"bg-medusa-tag-neutral-bg border-medusa-tag-neutral-border",
|
||||
isWarningStyle && "bg-medusa-tag-red-bg border-medusa-tag-red-border",
|
||||
"[&_a]:no-underline [&_a]:text-medusa-fg-interactive hover:[&_a]:text-medusa-fg-interactive-hover ",
|
||||
"mb-docs_2 alert"
|
||||
)}
|
||||
>
|
||||
<div className={clsx("flex")}>
|
||||
<span className={clsx("inline-block h-1.5 w-1.5 mr-1")}>{icon}</span>
|
||||
<div
|
||||
className={clsx(
|
||||
isDefaultStyle && "text-medusa-tag-neutral-text",
|
||||
isWarningStyle && "text-medusa-tag-red-text",
|
||||
"text-medium flex-1 [&>*:last-child]:mb-0",
|
||||
"[&>p>code]:px-docs_0.5 [&>p>code]:text-code-label"
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<span
|
||||
className={clsx(
|
||||
"txt-compact-medium-plus block mb-docs_0.125",
|
||||
isDefaultStyle && "text-medusa-fg-base",
|
||||
isWarningStyle && "text-medusa-tag-red-text"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react"
|
||||
import { NoteProps } from ".."
|
||||
import { NoteLayout } from "../Layout"
|
||||
import { CheckCircleSolid } from "@medusajs/icons"
|
||||
import clsx from "clsx"
|
||||
|
||||
export const CheckNote = ({
|
||||
title = "Prerequisites",
|
||||
icon,
|
||||
...props
|
||||
}: NoteProps) => {
|
||||
return (
|
||||
<NoteLayout
|
||||
title={title}
|
||||
icon={
|
||||
icon || (
|
||||
<CheckCircleSolid
|
||||
className={clsx(
|
||||
"inline-block mr-docs_0.125 text-medusa-tag-neutral-icon"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react"
|
||||
import { NoteProps } from ".."
|
||||
import { NoteLayout } from "../Layout"
|
||||
import { InformationCircleSolid } from "@medusajs/icons"
|
||||
import clsx from "clsx"
|
||||
|
||||
export const DefaultNote = ({ title = "Note", icon, ...props }: NoteProps) => {
|
||||
return (
|
||||
<NoteLayout
|
||||
title={title}
|
||||
icon={
|
||||
icon || (
|
||||
<InformationCircleSolid
|
||||
className={clsx(
|
||||
"inline-block mr-docs_0.125 text-medusa-tag-neutral-icon"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react"
|
||||
import { NoteProps } from ".."
|
||||
import { NoteLayout } from "../Layout"
|
||||
import { XMark } from "@medusajs/icons"
|
||||
import clsx from "clsx"
|
||||
|
||||
export const ErrorNote = ({ title = "Error", icon, ...props }: NoteProps) => {
|
||||
return (
|
||||
<NoteLayout
|
||||
title={title}
|
||||
icon={
|
||||
icon || (
|
||||
<XMark
|
||||
className={clsx(
|
||||
"inline-block mr-docs_0.125 text-medusa-tag-red-icon"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react"
|
||||
import { NoteProps } from ".."
|
||||
import { NoteLayout } from "../Layout"
|
||||
import { Check } from "@medusajs/icons"
|
||||
import clsx from "clsx"
|
||||
|
||||
export const SuccessNote = ({
|
||||
title = "Sucess",
|
||||
icon,
|
||||
...props
|
||||
}: NoteProps) => {
|
||||
return (
|
||||
<NoteLayout
|
||||
title={title}
|
||||
icon={
|
||||
icon || (
|
||||
<Check
|
||||
className={clsx(
|
||||
"inline-block mr-docs_0.125 text-medusa-tag-green-icon"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react"
|
||||
import { NoteProps } from ".."
|
||||
import { NoteLayout } from "../Layout"
|
||||
import { ExclamationCircleSolid } from "@medusajs/icons"
|
||||
import clsx from "clsx"
|
||||
|
||||
export const WarningNote = ({
|
||||
title = "Warning",
|
||||
icon,
|
||||
...props
|
||||
}: NoteProps) => {
|
||||
return (
|
||||
<NoteLayout
|
||||
title={title}
|
||||
icon={
|
||||
icon || (
|
||||
<ExclamationCircleSolid
|
||||
className={clsx(
|
||||
"inline-block mr-docs_0.125 text-medusa-tag-red-icon"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react"
|
||||
import { WarningNote } from "./Types/warning"
|
||||
import { DefaultNote } from "./Types/default"
|
||||
import { SuccessNote } from "./Types/sucess"
|
||||
import { ErrorNote } from "./Types/error"
|
||||
import { CheckNote } from "./Types/checks"
|
||||
|
||||
export type NoteProps = {
|
||||
type?: "default" | "warning" | "success" | "error" | "check"
|
||||
title?: string
|
||||
children?: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Note = ({ type = "default", ...props }: NoteProps) => {
|
||||
switch (type) {
|
||||
case "warning":
|
||||
return <WarningNote type={type} {...props} />
|
||||
case "success":
|
||||
return <SuccessNote type={type} {...props} />
|
||||
case "error":
|
||||
return <ErrorNote type={type} {...props} />
|
||||
case "check":
|
||||
return <CheckNote type={type} {...props} />
|
||||
default:
|
||||
return <DefaultNote type={type} {...props} />
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from "react"
|
||||
import { CodeBlockMetaFields, LegacyCodeTabs } from "../.."
|
||||
import convert from "npm-to-yarn"
|
||||
|
||||
type Npm2YarnCodeProps = {
|
||||
npmCode: string
|
||||
} & Omit<CodeBlockMetaFields, "npm2yarn">
|
||||
|
||||
export const Npm2YarnCode = ({ npmCode, ...rest }: Npm2YarnCodeProps) => {
|
||||
// convert npm code
|
||||
const yarnCode = convert(npmCode, "yarn").replaceAll(/([^\s])&&/g, "$1 &&")
|
||||
const pnpmCode = convert(npmCode, "pnpm").replaceAll(/([^\s])&&/g, "$1 &&")
|
||||
const lang = "bash"
|
||||
|
||||
const { title = "", ...codeOptions } = rest
|
||||
|
||||
return (
|
||||
<LegacyCodeTabs
|
||||
group="npm2yarn"
|
||||
title={title}
|
||||
tabs={[
|
||||
{
|
||||
label: "npm",
|
||||
value: "npm",
|
||||
code: {
|
||||
source: npmCode,
|
||||
lang,
|
||||
...codeOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "yarn",
|
||||
value: "yarn",
|
||||
code: {
|
||||
source: yarnCode,
|
||||
lang,
|
||||
...codeOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "pnpm",
|
||||
value: "pnpm",
|
||||
code: {
|
||||
source: pnpmCode,
|
||||
lang,
|
||||
...codeOptions,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { usePagination } from "../../providers"
|
||||
import { Card } from "../Card"
|
||||
import { ChevronLeft, ChevronRight } from "@medusajs/icons"
|
||||
|
||||
export const Pagination = () => {
|
||||
const { previousPage, nextPage } = usePagination()
|
||||
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
{previousPage && (
|
||||
<Card
|
||||
title={previousPage.title}
|
||||
startIcon={<ChevronLeft />}
|
||||
showLinkIcon={false}
|
||||
href={previousPage.link}
|
||||
className="max-w-[45%] ml-0 mr-auto"
|
||||
/>
|
||||
)}
|
||||
{nextPage && (
|
||||
<Card
|
||||
title={nextPage.title}
|
||||
endIcon={<ChevronRight />}
|
||||
showLinkIcon={false}
|
||||
href={nextPage.link}
|
||||
className="max-w-[45%] mr-0 ml-auto"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { SearchNoResult } from "../NoResults"
|
||||
import { SearchHitGroupName } from "./GroupName"
|
||||
import { useSearch } from "@/providers"
|
||||
import { Link } from "@/components/Link"
|
||||
import { LegacyLink } from "@/components"
|
||||
|
||||
export type Hierarchy = "lvl0" | "lvl1" | "lvl2" | "lvl3" | "lvl4" | "lvl5"
|
||||
|
||||
@@ -205,7 +205,8 @@ export const SearchHits = ({
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<Link
|
||||
{/* TODO replace with Link once we move away from Docusaurus */}
|
||||
<LegacyLink
|
||||
href={item.url}
|
||||
className="absolute top-0 left-0 h-full w-full"
|
||||
target="_self"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useCallback, useEffect, useRef } from "react"
|
||||
import clsx from "clsx"
|
||||
import { OptionType } from "@/hooks"
|
||||
import { Ref } from "@/types"
|
||||
import { Ref } from "types"
|
||||
import { CheckMini, EllipseMiniSolid } from "@medusajs/icons"
|
||||
|
||||
export type SelectDropdownProps = {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useSidebar } from "../../../providers"
|
||||
import clsx from "clsx"
|
||||
import { ArrowUturnLeft } from "@medusajs/icons"
|
||||
|
||||
export const SidebarBack = () => {
|
||||
const { goBack } = useSidebar()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"my-docs_1.5 cursor-pointer",
|
||||
"flex items-center gap-docs_0.5 rounded-docs_sm px-docs_0.5 py-[6px] hover:no-underline",
|
||||
"border border-transparent",
|
||||
"text-medusa-fg-subtle text-medium-plus"
|
||||
)}
|
||||
tabIndex={-1}
|
||||
onClick={goBack}
|
||||
>
|
||||
<ArrowUturnLeft className="mr-docs_0.5" />
|
||||
<span>Back</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
"use client"
|
||||
|
||||
// @refresh reset
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react"
|
||||
import type { SidebarItemType } from "@/providers"
|
||||
import { useSidebar } from "@/providers"
|
||||
import clsx from "clsx"
|
||||
import Link from "next/link"
|
||||
import { checkSidebarItemVisibility } from "@/utils"
|
||||
import { Loading } from "@/components"
|
||||
import { SidebarItemType } from "types"
|
||||
|
||||
export type SidebarItemProps = {
|
||||
item: SidebarItemType
|
||||
nested?: boolean
|
||||
expandItems?: boolean
|
||||
currentLevel?: number
|
||||
isSidebarTitle?: boolean
|
||||
} & React.AllHTMLAttributes<HTMLLIElement>
|
||||
|
||||
export const SidebarItem = ({
|
||||
@@ -19,9 +23,16 @@ export const SidebarItem = ({
|
||||
nested = false,
|
||||
expandItems = false,
|
||||
className,
|
||||
currentLevel = 1,
|
||||
}: SidebarItemProps) => {
|
||||
const [showLoading, setShowLoading] = useState(false)
|
||||
const { isItemActive, setMobileSidebarOpen: setSidebarOpen } = useSidebar()
|
||||
const {
|
||||
isItemActive,
|
||||
setMobileSidebarOpen: setSidebarOpen,
|
||||
disableActiveTransition,
|
||||
noTitleStyling,
|
||||
sidebarRef,
|
||||
} = useSidebar()
|
||||
const active = useMemo(
|
||||
() => isItemActive(item, nested),
|
||||
[isItemActive, item, nested]
|
||||
@@ -29,49 +40,106 @@ export const SidebarItem = ({
|
||||
const collapsed = !expandItems && !isItemActive(item, true)
|
||||
const ref = useRef<HTMLLIElement>(null)
|
||||
|
||||
const itemChildren = useMemo(() => {
|
||||
return item.isChildSidebar ? undefined : item.children
|
||||
}, [item])
|
||||
const canHaveTitleStyling = useMemo(
|
||||
() =>
|
||||
item.hasTitleStyling ||
|
||||
((itemChildren?.length || !item.loaded) && !noTitleStyling && !nested),
|
||||
[itemChildren, noTitleStyling, item, nested]
|
||||
)
|
||||
|
||||
const classNames = useMemo(
|
||||
() =>
|
||||
clsx(
|
||||
"flex items-center justify-between gap-docs_0.5 rounded-docs_sm px-docs_0.5 py-[6px] hover:no-underline",
|
||||
"border",
|
||||
!canHaveTitleStyling && "text-compact-small-plus text-medusa-fg-subtle",
|
||||
canHaveTitleStyling &&
|
||||
"text-compact-x-small-plus text-medusa-fg-muted uppercase",
|
||||
item.path !== undefined &&
|
||||
active && ["!text-medusa-fg-base bg-medusa-bg-base-pressed"],
|
||||
(item.path === undefined || !active) && "border-transparent",
|
||||
item.path !== undefined && active && " border-medusa-border-base",
|
||||
item.path !== undefined &&
|
||||
!active &&
|
||||
"hover:bg-medusa-bg-base-hover border-transparent"
|
||||
),
|
||||
[canHaveTitleStyling, active, item.path]
|
||||
)
|
||||
|
||||
/**
|
||||
* Tries to place the item in the center of the sidebar
|
||||
*/
|
||||
const newTopCalculator = (): number => {
|
||||
if (!sidebarRef.current || !ref.current) {
|
||||
return 0
|
||||
}
|
||||
const sidebarBoundingRect = sidebarRef.current.getBoundingClientRect()
|
||||
const sidebarHalf = sidebarBoundingRect.height / 2
|
||||
const itemTop = ref.current.offsetTop
|
||||
const itemBottom =
|
||||
itemTop + (ref.current.children.item(0) as HTMLElement)?.clientHeight
|
||||
|
||||
// try deducting half
|
||||
let newTop = itemTop - sidebarHalf
|
||||
let newBottom = newTop + sidebarBoundingRect.height
|
||||
if (newTop <= itemTop && newBottom >= itemBottom) {
|
||||
return newTop
|
||||
}
|
||||
|
||||
// try adding half
|
||||
newTop = itemTop + sidebarHalf
|
||||
newBottom = newTop + sidebarBoundingRect.height
|
||||
if (newTop <= itemTop && newBottom >= itemBottom) {
|
||||
return newTop
|
||||
}
|
||||
|
||||
//return the item's top minus some top margin
|
||||
return itemTop - sidebarBoundingRect.top
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (active && ref.current && window.innerWidth >= 1025) {
|
||||
if (
|
||||
active &&
|
||||
ref.current &&
|
||||
sidebarRef.current &&
|
||||
window.innerWidth >= 1025
|
||||
) {
|
||||
if (
|
||||
!checkSidebarItemVisibility(ref.current, {
|
||||
topMargin: 57,
|
||||
})
|
||||
!disableActiveTransition &&
|
||||
!checkSidebarItemVisibility(
|
||||
(ref.current.children.item(0) as HTMLElement) || ref.current,
|
||||
!disableActiveTransition
|
||||
)
|
||||
) {
|
||||
// scroll to element
|
||||
ref.current.scrollIntoView({
|
||||
block: "center",
|
||||
})
|
||||
} else if (disableActiveTransition) {
|
||||
sidebarRef.current.scrollTo({
|
||||
top: newTopCalculator(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if (active) {
|
||||
setShowLoading(true)
|
||||
}
|
||||
}, [active])
|
||||
|
||||
const classNames = useMemo(
|
||||
() =>
|
||||
clsx(
|
||||
"flex items-center justify-between gap-docs_0.5 rounded-docs_sm px-docs_0.5 py-[6px] hover:no-underline",
|
||||
!item.children && "text-compact-small-plus text-medusa-fg-subtle",
|
||||
item.children &&
|
||||
"text-compact-x-small-plus text-medusa-fg-muted uppercase",
|
||||
item.path !== undefined &&
|
||||
active && ["!text-medusa-fg-base bg-medusa-bg-base-pressed"],
|
||||
item.path !== undefined && active && "border border-medusa-border-base",
|
||||
item.path !== undefined &&
|
||||
!active &&
|
||||
"hover:bg-medusa-bg-base-hover border-transparent"
|
||||
),
|
||||
[item.children, active, item.path]
|
||||
)
|
||||
}, [active, sidebarRef.current, disableActiveTransition])
|
||||
|
||||
return (
|
||||
<li
|
||||
className={clsx(
|
||||
item.children && !collapsed && "my-docs_1.5",
|
||||
!item.children && !nested && active && "mt-docs_1.5",
|
||||
canHaveTitleStyling && !collapsed && "my-docs_1.5",
|
||||
!canHaveTitleStyling &&
|
||||
!nested &&
|
||||
active &&
|
||||
!disableActiveTransition &&
|
||||
"mt-docs_1.5",
|
||||
!expandItems &&
|
||||
((item.children && !collapsed) ||
|
||||
(!item.children && !nested && active)) &&
|
||||
((canHaveTitleStyling && !collapsed) ||
|
||||
(!canHaveTitleStyling && !nested && active)) &&
|
||||
"-translate-y-docs_1 transition-transform",
|
||||
className
|
||||
)}
|
||||
@@ -93,17 +161,20 @@ export const SidebarItem = ({
|
||||
setSidebarOpen(false)
|
||||
}
|
||||
}}
|
||||
replace
|
||||
shallow
|
||||
replace={!item.isPathHref}
|
||||
shallow={!item.isPathHref}
|
||||
{...item.linkProps}
|
||||
>
|
||||
<span>{item.title}</span>
|
||||
{item.additionalElms}
|
||||
</Link>
|
||||
)}
|
||||
{item.children && (
|
||||
{itemChildren && (
|
||||
<ul
|
||||
className={clsx("ease-ease overflow-hidden", collapsed && "m-0 h-0")}
|
||||
style={{
|
||||
paddingLeft: noTitleStyling ? `${currentLevel * 6}px` : 0,
|
||||
}}
|
||||
>
|
||||
{showLoading && !item.loaded && (
|
||||
<Loading
|
||||
@@ -112,8 +183,14 @@ export const SidebarItem = ({
|
||||
barClassName="h-[20px]"
|
||||
/>
|
||||
)}
|
||||
{item.children?.map((childItem, index) => (
|
||||
<SidebarItem item={childItem} key={index} nested={true} />
|
||||
{itemChildren?.map((childItem, index) => (
|
||||
<SidebarItem
|
||||
item={childItem}
|
||||
key={index}
|
||||
nested={true}
|
||||
currentLevel={currentLevel + 1}
|
||||
expandItems={expandItems}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react"
|
||||
import Link from "next/link"
|
||||
import clsx from "clsx"
|
||||
import { SidebarItemType } from "types"
|
||||
|
||||
type SidebarTitleProps = {
|
||||
item: SidebarItemType
|
||||
}
|
||||
|
||||
export const SidebarTitle = ({ item }: SidebarTitleProps) => {
|
||||
return (
|
||||
<Link
|
||||
className={clsx(
|
||||
"flex items-center justify-between gap-docs_0.5 rounded-docs_sm px-docs_0.5 py-[6px] hover:no-underline",
|
||||
"border border-transparent",
|
||||
"text-medusa-fg-subtle text-medium-plus"
|
||||
)}
|
||||
href={item.isPathHref && item.path ? item.path : `#${item.path}`}
|
||||
replace={!item.isPathHref}
|
||||
shallow={!item.isPathHref}
|
||||
{...item.linkProps}
|
||||
>
|
||||
<span>{item.childSidebarTitle || item.title}</span>
|
||||
{item.additionalElms}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import React, { useMemo } from "react"
|
||||
import { useSidebar } from "@/providers"
|
||||
import clsx from "clsx"
|
||||
import { Loading } from "@/components"
|
||||
import { SidebarItem } from "./Item"
|
||||
import { SidebarTitle } from "./Title"
|
||||
import { SidebarBack } from "./Back"
|
||||
import { CSSTransition, SwitchTransition } from "react-transition-group"
|
||||
|
||||
export type SidebarProps = {
|
||||
className?: string
|
||||
@@ -15,12 +18,24 @@ export const Sidebar = ({
|
||||
className = "",
|
||||
expandItems = false,
|
||||
}: SidebarProps) => {
|
||||
const { items, mobileSidebarOpen, desktopSidebarOpen } = useSidebar()
|
||||
const {
|
||||
items,
|
||||
currentItems,
|
||||
mobileSidebarOpen,
|
||||
desktopSidebarOpen,
|
||||
staticSidebarItems,
|
||||
sidebarRef,
|
||||
} = useSidebar()
|
||||
|
||||
const sidebarItems = useMemo(
|
||||
() => currentItems || items,
|
||||
[items, currentItems]
|
||||
)
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={clsx(
|
||||
"clip bg-docs-bg dark:bg-docs-bg-dark w-ref-sidebar block",
|
||||
"clip bg-docs-bg dark:bg-docs-bg-dark block",
|
||||
"border-medusa-border-base border-0 border-r border-solid",
|
||||
"fixed -left-full top-0 h-screen transition-[left] lg:relative lg:left-0 lg:top-auto lg:h-auto",
|
||||
"lg:w-sidebar w-full",
|
||||
@@ -32,32 +47,69 @@ export const Sidebar = ({
|
||||
animationFillMode: "forwards",
|
||||
}}
|
||||
>
|
||||
<ul
|
||||
className={clsx(
|
||||
"sticky top-0 h-screen max-h-screen w-full list-none overflow-auto p-0",
|
||||
"px-docs_1.5 pb-[57px] pt-docs_1.5"
|
||||
)}
|
||||
id="sidebar"
|
||||
>
|
||||
<div className="mb-docs_1.5 lg:hidden">
|
||||
{!items.mobile.length && <Loading className="px-0" />}
|
||||
{items.mobile.map((item, index) => (
|
||||
<SidebarItem item={item} key={index} expandItems={expandItems} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-docs_1.5">
|
||||
{!items.top.length && <Loading className="px-0" />}
|
||||
{items.top.map((item, index) => (
|
||||
<SidebarItem item={item} key={index} expandItems={expandItems} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-docs_1.5">
|
||||
{!items.bottom.length && <Loading className="px-0" />}
|
||||
{items.bottom.map((item, index) => (
|
||||
<SidebarItem item={item} key={index} expandItems={expandItems} />
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
<SwitchTransition>
|
||||
<CSSTransition
|
||||
key={sidebarItems.parentItem?.title || "home"}
|
||||
nodeRef={sidebarRef}
|
||||
classNames={{
|
||||
enter: "animate-fadeInLeft animate-fast",
|
||||
exit: "animate-fadeOutLeft animate-fast",
|
||||
}}
|
||||
timeout={200}
|
||||
>
|
||||
<ul
|
||||
className={clsx(
|
||||
"sticky top-0 h-screen max-h-screen w-full list-none overflow-auto p-0",
|
||||
"px-docs_1.5 pb-[57px] pt-docs_1.5"
|
||||
)}
|
||||
id="sidebar"
|
||||
ref={sidebarRef}
|
||||
>
|
||||
{sidebarItems.parentItem && (
|
||||
<>
|
||||
<SidebarBack />
|
||||
<SidebarTitle item={sidebarItems.parentItem} />
|
||||
</>
|
||||
)}
|
||||
<div className="mb-docs_1.5 lg:hidden">
|
||||
{!sidebarItems.mobile.length && !staticSidebarItems && (
|
||||
<Loading className="px-0" />
|
||||
)}
|
||||
{sidebarItems.mobile.map((item, index) => (
|
||||
<SidebarItem
|
||||
item={item}
|
||||
key={index}
|
||||
expandItems={expandItems}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-docs_1.5">
|
||||
{!sidebarItems.top.length && !staticSidebarItems && (
|
||||
<Loading className="px-0" />
|
||||
)}
|
||||
{sidebarItems.top.map((item, index) => (
|
||||
<SidebarItem
|
||||
item={item}
|
||||
key={index}
|
||||
expandItems={expandItems}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-docs_1.5">
|
||||
{!sidebarItems.bottom.length && !staticSidebarItems && (
|
||||
<Loading className="px-0" />
|
||||
)}
|
||||
{sidebarItems.bottom.map((item, index) => (
|
||||
<SidebarItem
|
||||
item={item}
|
||||
key={index}
|
||||
expandItems={expandItems}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
</CSSTransition>
|
||||
</SwitchTransition>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react"
|
||||
import { Table as UiTable } from "@medusajs/ui"
|
||||
import clsx from "clsx"
|
||||
|
||||
type RootProps = React.HTMLAttributes<HTMLTableElement>
|
||||
|
||||
const Root = ({ className, ...props }: RootProps) => {
|
||||
return (
|
||||
<UiTable
|
||||
className={clsx(
|
||||
className,
|
||||
"table-fixed mb-docs_1",
|
||||
"[&_pre_span]:!max-w-full [&_pre_span]:!break-words [&_pre_span]:!whitespace-break-spaces"
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type HeaderCellProps = React.HTMLAttributes<HTMLTableCellElement>
|
||||
|
||||
const HeaderCell = ({ className, ...props }: HeaderCellProps) => {
|
||||
return (
|
||||
<UiTable.HeaderCell
|
||||
className={clsx(className, "text-left pr-docs_1.5 h-docs_3 break-words")}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type RowProps = React.HTMLAttributes<HTMLTableRowElement>
|
||||
|
||||
const Row = ({ className, ...props }: RowProps) => {
|
||||
return (
|
||||
<UiTable.Row
|
||||
className={clsx(
|
||||
className,
|
||||
"[&_td:last-child]:pr-docs_1.5 [&_th:last-child]:pr-docs_1.5 [&_td:first-child]:pl-docs_1.5 [&_th:first-child]:pl-docs_1.5"
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type CellProps = React.HTMLAttributes<HTMLTableCellElement>
|
||||
|
||||
const Cell = ({ className, ...props }: CellProps) => {
|
||||
return (
|
||||
<UiTable.Cell
|
||||
className={clsx(className, "pr-docs_1.5 h-docs_3 break-words")}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Table = Object.assign(Root, {
|
||||
Row,
|
||||
Cell,
|
||||
Header: UiTable.Header,
|
||||
HeaderCell,
|
||||
Body: UiTable.Body,
|
||||
})
|
||||
|
||||
export { Table }
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { Tabs as UiTabs } from "@medusajs/ui"
|
||||
import { ComponentProps } from "react"
|
||||
import clsx from "clsx"
|
||||
import { Key } from "types"
|
||||
|
||||
export const Tabs = (props: ComponentProps<typeof UiTabs> & Key) => (
|
||||
<UiTabs {...props} />
|
||||
)
|
||||
|
||||
export const TabsList = ({
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof UiTabs.List> & Key) => (
|
||||
<UiTabs.List {...props} className={clsx(className, "gap-docs_0.5")} />
|
||||
)
|
||||
|
||||
export const TabsTrigger = ({
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof UiTabs.Trigger> & Key) => (
|
||||
<UiTabs.Trigger
|
||||
{...props}
|
||||
className={clsx(
|
||||
className,
|
||||
"px-[10px] py-docs_0.25 data-[state=active]:text-ui-fg-base data-[state=active]:bg-ui-bg-base data-[state=active]:shadow-elevation-card-rest",
|
||||
"hover:text-ui-fg-base focus-visible:border-ui-border-interactive focus-visible:!shadow-borders-focus focus-visible:bg-ui-bg-base"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
type TabsContentWrapperProps = {
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const TabsContentWrapper = ({
|
||||
className,
|
||||
...props
|
||||
}: TabsContentWrapperProps) => (
|
||||
<div {...props} className={clsx(className, "mt-docs_0.5")} />
|
||||
)
|
||||
|
||||
export const TabsContent = (
|
||||
props: ComponentProps<typeof UiTabs.Content> & Key
|
||||
) => <UiTabs.Content {...props} />
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useColorMode } from "@/providers"
|
||||
|
||||
export type ThemeImageProps = {
|
||||
light: string
|
||||
dark?: string
|
||||
alt?: string
|
||||
} & React.AllHTMLAttributes<HTMLImageElement>
|
||||
|
||||
export const ThemeImage = ({
|
||||
light,
|
||||
dark,
|
||||
alt = "",
|
||||
...props
|
||||
}: ThemeImageProps) => {
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
return (
|
||||
<img
|
||||
alt={alt}
|
||||
src={colorMode === "light" ? light : dark || light}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Switch } from "@medusajs/ui"
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { Key } from "types"
|
||||
|
||||
export const Toggle = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Switch> & Key) => {
|
||||
return (
|
||||
<Switch
|
||||
{...props}
|
||||
className={clsx(
|
||||
className,
|
||||
"bg-ui-bg-switch-off hover:bg-ui-bg-switch-off-hover data-[state=unchecked]:hover:after:bg-switch-off-hover-gradient",
|
||||
"before:shadow-details-switch-background focus-visible:shadow-details-switch-background-focus data-[state=checked]:bg-ui-bg-interactive",
|
||||
"disabled:!bg-ui-bg-disabled group relative inline-flex items-center rounded-full outline-none transition-all before:absolute",
|
||||
"before:inset-0 before:rounded-full before:content-[''] after:absolute after:inset-0 after:rounded-full after:content-['']",
|
||||
"disabled:cursor-not-allowed",
|
||||
// eslint-disable-next-line react/prop-types
|
||||
props.size !== "small" &&
|
||||
"h-[18px] w-[32px] [&_span]:h-[14px] [&_span]:w-[14px]",
|
||||
"[&_span]:bg-medusa-fg-on-color [&_span]:shadow-details-switch-handle [&_span]:group-disabled:bg-ui-fg-disabled [&_span]:pointer-events-none",
|
||||
"[&_span]:rounded-full [&_span]:group-disabled:shadow-none [&_span]:transition-transform",
|
||||
"[&_span]:data-[state=checked]:translate-x-docs_1 [&_span]:data-[state=unchecked]:translate-x-docs_0.125"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react"
|
||||
import {
|
||||
Details,
|
||||
CopyButton,
|
||||
DetailsSummary,
|
||||
ExpandableNotice,
|
||||
FeatureFlagNotice,
|
||||
InlineCode,
|
||||
MarkdownContent,
|
||||
} from "@/components"
|
||||
import clsx from "clsx"
|
||||
import { Type, CommonProps as ParentCommonProps } from ".."
|
||||
import {
|
||||
ArrowDownLeftMini,
|
||||
ArrowsPointingOutMini,
|
||||
FlagMini,
|
||||
Link,
|
||||
TriangleRightMini,
|
||||
} from "@medusajs/icons"
|
||||
import { decodeStr, isInView } from "@/utils"
|
||||
import { useIsBrowser } from "../../.."
|
||||
|
||||
type CommonProps = ParentCommonProps & {
|
||||
level?: number
|
||||
}
|
||||
|
||||
type TypeListItemProps = {
|
||||
typeItem: Type
|
||||
elementKey: number
|
||||
} & CommonProps &
|
||||
React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
const TypeListItem = ({
|
||||
typeItem,
|
||||
level = 1,
|
||||
expandUrl,
|
||||
elementKey,
|
||||
sectionTitle,
|
||||
siteUrl = "",
|
||||
pathname,
|
||||
}: TypeListItemProps) => {
|
||||
const isBrowser = useIsBrowser()
|
||||
|
||||
const groupName = useMemo(() => {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return "group/typeOne"
|
||||
case 2:
|
||||
return "group/typeTwo"
|
||||
case 3:
|
||||
return "group/typeThree"
|
||||
case 4:
|
||||
return "group/typeFour"
|
||||
}
|
||||
}, [level])
|
||||
const borderForGroupName = useMemo(() => {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return "group-open/typeOne:border-solid group-open/typeOne:border-0 group-open/typeOne:border-b"
|
||||
case 2:
|
||||
return "group-open/typeTwo:border-solid group-open/typeTwo:border-0 group-open/typeTwo:border-b"
|
||||
case 3:
|
||||
return "group-open/typeThree:border-solid group-open/typeThree:border-0 group-open/typeThree:border-b"
|
||||
case 4:
|
||||
return "group-open/typeFour:border-solid group-open/typeFour:border-0 group-open/typeFour:border-b"
|
||||
}
|
||||
}, [level])
|
||||
const rotateForGroupName = useMemo(() => {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return "group-open/typeOne:rotate-90"
|
||||
case 2:
|
||||
return "group-open/typeTwo:rotate-90"
|
||||
case 3:
|
||||
return "group-open/typeThree:rotate-90"
|
||||
case 4:
|
||||
return "group-open/typeFour:rotate-90"
|
||||
}
|
||||
}, [level])
|
||||
function getItemClassNames(details = true) {
|
||||
return clsx(
|
||||
"odd:[&:not(:first-child):not(:last-child)]:!border-y last:not(:first-child):!border-t",
|
||||
"first:!border-t-0 first:not(:last-child):!border-b last:!border-b-0 even:!border-y-0",
|
||||
details && groupName,
|
||||
!details && borderForGroupName
|
||||
)
|
||||
}
|
||||
const formatId = (str: string) => {
|
||||
return str.replaceAll(" ", "_")
|
||||
}
|
||||
const typeId = useMemo(() => {
|
||||
return sectionTitle
|
||||
? `#${formatId(sectionTitle)}-${formatId(
|
||||
typeItem.name
|
||||
)}-${level}-${elementKey}`
|
||||
: ""
|
||||
}, [sectionTitle, typeItem, elementKey])
|
||||
const typePath = useMemo(() => `${pathname}${typeId}`, [pathname, typeId])
|
||||
const typeUrl = useMemo(() => `${siteUrl}${typePath}`, [siteUrl, typePath])
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [isSelected, setIsSelected] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!typeId.length || !isBrowser) {
|
||||
return
|
||||
}
|
||||
|
||||
const shouldScroll = window.location.hash === typeId
|
||||
|
||||
if (shouldScroll && !isSelected && ref.current && !isInView(ref.current)) {
|
||||
ref.current.scrollIntoView({
|
||||
block: "center",
|
||||
})
|
||||
}
|
||||
|
||||
setIsSelected(shouldScroll)
|
||||
}, [typeId, isBrowser])
|
||||
|
||||
function getSummary(item: Type, nested = true) {
|
||||
return (
|
||||
<DetailsSummary
|
||||
subtitle={
|
||||
item.description || item.defaultValue ? (
|
||||
<>
|
||||
{item.description && (
|
||||
<MarkdownContent
|
||||
allowedElements={["a", "strong", "code", "ul", "ol", "li"]}
|
||||
unwrapDisallowed={true}
|
||||
className="text-medium"
|
||||
>
|
||||
{item.description}
|
||||
</MarkdownContent>
|
||||
)}
|
||||
{item.defaultValue && (
|
||||
<p className="mt-0.5 mb-0">
|
||||
Default: <InlineCode>{item.defaultValue}</InlineCode>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
expandable={(item.children?.length || 0) > 0}
|
||||
hideExpandableIcon={true}
|
||||
className={clsx(
|
||||
getItemClassNames(false),
|
||||
"py-1 pr-1",
|
||||
level === 1 && "pl-1",
|
||||
level === 2 && "pl-3",
|
||||
level === 3 && "pl-[120px]",
|
||||
level === 4 && "pl-[160px]",
|
||||
!nested && "cursor-auto",
|
||||
isSelected && "animate-flash animate-bg-surface"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
const targetElm = e.target as HTMLElement
|
||||
if (targetElm.tagName.toLowerCase() === "a") {
|
||||
window.location.href =
|
||||
targetElm.getAttribute("href") || window.location.href
|
||||
return
|
||||
}
|
||||
}}
|
||||
summaryRef={!nested ? ref : undefined}
|
||||
id={!nested && typeId ? typeId : ""}
|
||||
>
|
||||
<div className="flex gap-0.5">
|
||||
{nested && (
|
||||
<TriangleRightMini
|
||||
className={clsx(
|
||||
"text-medusa-fg-subtle transition-transform",
|
||||
rotateForGroupName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!nested && level > 1 && (
|
||||
<ArrowDownLeftMini
|
||||
className={clsx("text-medusa-fg-subtle flip-y")}
|
||||
/>
|
||||
)}
|
||||
{level === 1 && typeId.length > 0 && (
|
||||
<CopyButton
|
||||
text={typeUrl}
|
||||
onCopy={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
className={clsx(
|
||||
"text-medusa-fg-interactive hover:text-medusa-fg-interactive-hover"
|
||||
)}
|
||||
/>
|
||||
</CopyButton>
|
||||
)}
|
||||
<div className="flex gap-0.75 flex-wrap">
|
||||
<InlineCode>{decodeStr(item.name)}</InlineCode>
|
||||
<span className="font-monospace text-compact-small-plus text-medusa-fg-subtle">
|
||||
<MarkdownContent allowedElements={["a"]} unwrapDisallowed={true}>
|
||||
{item.type}
|
||||
</MarkdownContent>
|
||||
</span>
|
||||
{item.optional === false && (
|
||||
<span
|
||||
className={clsx(
|
||||
"text-compact-x-small-plus uppercase",
|
||||
"text-medusa-fg-error"
|
||||
)}
|
||||
>
|
||||
Required
|
||||
</span>
|
||||
)}
|
||||
{item.featureFlag && (
|
||||
<FeatureFlagNotice
|
||||
featureFlag={item.featureFlag}
|
||||
type="type"
|
||||
badgeClassName="!p-0 leading-none"
|
||||
badgeContent={
|
||||
<FlagMini className="!text-medusa-tag-green-text" />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{item.expandable && (
|
||||
<ExpandableNotice
|
||||
type="method"
|
||||
link={expandUrl || "#"}
|
||||
badgeClassName="!p-0 leading-none"
|
||||
badgeContent={<ArrowsPointingOutMini />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DetailsSummary>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{(typeItem.children?.length || 0) > 0 && (
|
||||
<Details
|
||||
summaryElm={getSummary(typeItem)}
|
||||
className={clsx(getItemClassNames())}
|
||||
heightAnimation={true}
|
||||
id={typeId ? typeId : ""}
|
||||
>
|
||||
{typeItem.children && (
|
||||
<TypeListItems
|
||||
types={typeItem.children}
|
||||
level={level + 1}
|
||||
expandUrl={expandUrl}
|
||||
/>
|
||||
)}
|
||||
</Details>
|
||||
)}
|
||||
{(typeItem.children?.length || 0) === 0 && getSummary(typeItem, false)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type TypeListItemsProps = {
|
||||
types: Type[]
|
||||
} & CommonProps
|
||||
|
||||
const TypeListItems = ({ types, ...rest }: TypeListItemsProps) => {
|
||||
return (
|
||||
<div>
|
||||
{types.map((typeItem, key) => (
|
||||
<TypeListItem
|
||||
typeItem={typeItem}
|
||||
key={key}
|
||||
elementKey={key}
|
||||
{...rest}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TypeListItems
|
||||
@@ -0,0 +1,60 @@
|
||||
import clsx from "clsx"
|
||||
import React, { Suspense, lazy } from "react"
|
||||
import { Loading } from "@/components"
|
||||
|
||||
export type CommonProps = {
|
||||
expandUrl?: string
|
||||
sectionTitle?: string
|
||||
siteUrl?: string
|
||||
// TODO replace this with inferring pathname when using Next.js only
|
||||
pathname?: string
|
||||
}
|
||||
|
||||
export type Type = {
|
||||
name: string
|
||||
type: string
|
||||
optional?: boolean
|
||||
defaultValue?: string
|
||||
description?: string
|
||||
featureFlag?: string
|
||||
expandable: boolean
|
||||
children?: Type[]
|
||||
}
|
||||
|
||||
type ParameterTypesType = {
|
||||
types: Type[]
|
||||
sectionTitle?: string
|
||||
} & CommonProps &
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const TypeListItems = lazy(async () => import("./Items"))
|
||||
|
||||
export const TypeList = ({
|
||||
types,
|
||||
className,
|
||||
siteUrl,
|
||||
sectionTitle,
|
||||
expandUrl,
|
||||
pathname,
|
||||
...props
|
||||
}: ParameterTypesType) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-docs-bg-surface shadow-card-rest rounded my-docs_1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<TypeListItems
|
||||
types={types}
|
||||
expandUrl={expandUrl}
|
||||
sectionTitle={sectionTitle}
|
||||
siteUrl={siteUrl}
|
||||
pathname={pathname}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import Zoom from "react-medium-image-zoom"
|
||||
import "react-medium-image-zoom/dist/styles.css"
|
||||
|
||||
type ZoomImgProps = React.DetailedHTMLProps<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
HTMLImageElement
|
||||
>
|
||||
|
||||
export const ZoomImg = (props: ZoomImgProps) => {
|
||||
return (
|
||||
<Zoom wrapElement="span">
|
||||
<img {...props} />
|
||||
</Zoom>
|
||||
)
|
||||
}
|
||||
@@ -5,26 +5,39 @@ export * from "./Bordered"
|
||||
export * from "./BorderedIcon"
|
||||
export * from "./Button"
|
||||
export * from "./Card"
|
||||
export * from "./CardList"
|
||||
export * from "./ChildDocs"
|
||||
export * from "./CodeBlock"
|
||||
export * from "./CodeMdx"
|
||||
export * from "./CodeTabs"
|
||||
export * from "./CodeTabs/Item"
|
||||
export * from "./CopyButton"
|
||||
export * from "./Details"
|
||||
export * from "./Details/Summary"
|
||||
export * from "./DetailsList"
|
||||
export * from "./EditButton"
|
||||
export * from "./ExpandableNotice"
|
||||
export * from "./FeatureFlagNotice"
|
||||
export * from "./Feedback"
|
||||
export * from "./Feedback/Solutions"
|
||||
export * from "./HooksLoader"
|
||||
export * from "./Icons"
|
||||
export * from "./InlineIcon"
|
||||
export * from "./InlineThemeImage"
|
||||
export * from "./InlineCode"
|
||||
export * from "./Input/Text"
|
||||
export * from "./Kbd"
|
||||
export * from "./Label"
|
||||
export * from "./LearningPath"
|
||||
export * from "./LegacyCodeTabs"
|
||||
export * from "./LegacyLink"
|
||||
export * from "./Link"
|
||||
export * from "./Loading"
|
||||
export * from "./Loading/Dots"
|
||||
export * from "./Loading/Spinner"
|
||||
export * from "./MarkdownContent"
|
||||
export * from "./MDXComponents"
|
||||
export * from "./MermaidDiagram"
|
||||
export * from "./Modal"
|
||||
export * from "./Modal/Header"
|
||||
export * from "./Modal/Footer"
|
||||
@@ -36,10 +49,11 @@ export * from "./Navbar/Logo"
|
||||
export * from "./Navbar/MobileMenu"
|
||||
export * from "./Navbar/MobileMenu/Button"
|
||||
export * from "./Navbar/SearchModalOpener"
|
||||
export { Link as NextLink, type LinkProps as NextLinkProps } from "./NextLink"
|
||||
export * from "./Note"
|
||||
export * from "./Notification"
|
||||
export * from "./Notification/Item"
|
||||
export * from "./Notification/Item/Layout/Default"
|
||||
export * from "./Pagination"
|
||||
export * from "./Rating"
|
||||
export * from "./Search"
|
||||
export * from "./Search/EmptyQueryBoundary"
|
||||
@@ -52,5 +66,11 @@ export * from "./Search/Suggestions/Item"
|
||||
export * from "./Select"
|
||||
export * from "./Sidebar"
|
||||
export * from "./Sidebar/Item"
|
||||
export * from "./Table"
|
||||
export * from "./Tabs"
|
||||
export * from "./TextArea"
|
||||
export * from "./ThemeImage"
|
||||
export * from "./Toggle"
|
||||
export * from "./Tooltip"
|
||||
export * from "./TypeList"
|
||||
export * from "./ZoomImg"
|
||||
@@ -1,2 +1,104 @@
|
||||
import { NavbarLinkProps } from "@/components"
|
||||
import { OptionType } from "./hooks"
|
||||
import { SidebarItemType } from "types"
|
||||
|
||||
export const GITHUB_ISSUES_PREFIX = `https://github.com/medusajs/medusa/issues/new?assignees=&labels=type%3A+docs&template=docs.yml`
|
||||
export const GITHUB_UI_ISSUES_PREFIX = `https://github.com/medusajs/ui/issues/new?labels=documentation`
|
||||
|
||||
export const navbarItems: NavbarLinkProps[] = [
|
||||
{
|
||||
label: "Docs",
|
||||
target: "_blank",
|
||||
rel: "noreferrer",
|
||||
href: `/`,
|
||||
},
|
||||
{
|
||||
label: "User Guide",
|
||||
target: "_blank",
|
||||
rel: "noreferrer",
|
||||
href: `/user-guide`,
|
||||
},
|
||||
{
|
||||
label: "Store API",
|
||||
target: "_blank",
|
||||
rel: "noreferrer",
|
||||
href: `/api/store`,
|
||||
},
|
||||
{
|
||||
label: "Admin API",
|
||||
target: "_blank",
|
||||
rel: "noreferrer",
|
||||
href: `/api/admin`,
|
||||
},
|
||||
{
|
||||
label: "UI",
|
||||
target: "_blank",
|
||||
rel: "noreferrer",
|
||||
href: `/ui`,
|
||||
},
|
||||
]
|
||||
|
||||
export const mobileSidebarItems: SidebarItemType[] = [
|
||||
{
|
||||
title: "Docs",
|
||||
path: `/`,
|
||||
loaded: true,
|
||||
isPathHref: true,
|
||||
},
|
||||
{
|
||||
title: "User Guide",
|
||||
path: `/user-guide`,
|
||||
loaded: true,
|
||||
isPathHref: true,
|
||||
},
|
||||
{
|
||||
title: "Store API",
|
||||
path: `/api/store`,
|
||||
loaded: true,
|
||||
isPathHref: true,
|
||||
},
|
||||
{
|
||||
title: "Admin API",
|
||||
path: `/api/admin`,
|
||||
loaded: true,
|
||||
isPathHref: true,
|
||||
},
|
||||
{
|
||||
title: "UI",
|
||||
path: `/ui`,
|
||||
loaded: true,
|
||||
isPathHref: true,
|
||||
},
|
||||
]
|
||||
|
||||
// TODO add resources once we create index
|
||||
export const searchFilters: OptionType[] = [
|
||||
{
|
||||
value: "admin",
|
||||
label: "Admin API",
|
||||
},
|
||||
{
|
||||
value: "store",
|
||||
label: "Store API",
|
||||
},
|
||||
{
|
||||
value: "docs",
|
||||
label: "Docs",
|
||||
},
|
||||
{
|
||||
value: "user-guide",
|
||||
label: "User Guide",
|
||||
},
|
||||
{
|
||||
value: "plugins",
|
||||
label: "Plugins",
|
||||
},
|
||||
{
|
||||
value: "reference",
|
||||
label: "References",
|
||||
},
|
||||
{
|
||||
value: "ui",
|
||||
label: "UI",
|
||||
},
|
||||
]
|
||||
@@ -1,6 +1,9 @@
|
||||
export * from "./use-copy"
|
||||
export * from "./use-current-learning-path"
|
||||
export * from "./use-is-browser"
|
||||
export * from "./use-keyboard-shortcut"
|
||||
export * from "./use-page-scroll-manager"
|
||||
export * from "./use-request-runner"
|
||||
export * from "./use-scroll-utils"
|
||||
export * from "./use-search-navigation"
|
||||
export * from "./use-select"
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect } from "react"
|
||||
import { useLearningPath, useNotifications } from "@/providers"
|
||||
import { LearningPathIcon } from "../../components/LearningPath/Icon"
|
||||
import { LearningPathFinish } from "../../components/LearningPath/Finish"
|
||||
import { LearningPathSteps } from "../../components/LearningPath/Steps"
|
||||
|
||||
export const useCurrentLearningPath = () => {
|
||||
const { path, currentStep, updatePath, endPath } = useLearningPath()
|
||||
const step = path?.steps[currentStep]
|
||||
const {
|
||||
addNotification,
|
||||
generateId,
|
||||
removeNotification,
|
||||
updateNotification,
|
||||
} = useNotifications() || {}
|
||||
|
||||
// used when a notification closed (finished or not)
|
||||
const handleClose = (notificationId: string, shouldEndPath = true) => {
|
||||
if (shouldEndPath) {
|
||||
setTimeout(() => {
|
||||
endPath()
|
||||
}, 500)
|
||||
}
|
||||
removeNotification?.(notificationId)
|
||||
}
|
||||
|
||||
// used when the learning path is completely finished
|
||||
// shows the finish step, if the path has any
|
||||
const handleFinish = (notificationId: string) => {
|
||||
if (path?.finish) {
|
||||
updateNotification?.(notificationId, {
|
||||
title: path.finish.step.title,
|
||||
text: path.finish.step.description,
|
||||
type: "custom",
|
||||
layout: "default",
|
||||
CustomIcon: (
|
||||
<LearningPathIcon
|
||||
className="!w-2 !h-2"
|
||||
imgClassName="!w-1.5 !h-1.5"
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<LearningPathFinish
|
||||
{...path.finish}
|
||||
onRating={() =>
|
||||
setTimeout(() => {
|
||||
handleClose(notificationId, false)
|
||||
}, 1500)
|
||||
}
|
||||
/>
|
||||
),
|
||||
})
|
||||
endPath()
|
||||
} else {
|
||||
handleClose(notificationId)
|
||||
}
|
||||
}
|
||||
|
||||
const LearningStep = (notificationId: string) => {
|
||||
return <LearningPathSteps onFinish={() => handleFinish(notificationId)} />
|
||||
}
|
||||
|
||||
// create a notification when a path is initialized
|
||||
useEffect(() => {
|
||||
if (path && !path.notificationId) {
|
||||
const id = generateId?.()
|
||||
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
||||
addNotification?.({
|
||||
title: path.label,
|
||||
text: step?.description,
|
||||
onClose: () => handleClose(id),
|
||||
layout: "empty",
|
||||
id,
|
||||
children: LearningStep(id),
|
||||
className: "flex flex-col",
|
||||
})
|
||||
updatePath({
|
||||
notificationId: id,
|
||||
})
|
||||
}
|
||||
}, [path])
|
||||
|
||||
// update an existing notification when the step changes
|
||||
useEffect(() => {
|
||||
if (path && path.notificationId && step) {
|
||||
updateNotification?.(path.notificationId, {
|
||||
text: step?.description,
|
||||
children: LearningStep(path.notificationId),
|
||||
})
|
||||
}
|
||||
}, [step])
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useScrollController } from "../use-scroll-utils"
|
||||
import { useEffect, useState } from "react"
|
||||
import { getScrolledTop, isElmWindow } from "../../utils"
|
||||
|
||||
export const usePageScrollManager = () => {
|
||||
const pathname = usePathname()
|
||||
const { scrollableElement } = useScrollController()
|
||||
const [checkedPageReload, setCheckedPageReload] = useState(false)
|
||||
|
||||
const isPageReloaded = () =>
|
||||
(window.performance.navigation &&
|
||||
window.performance.navigation.type === 1) ||
|
||||
window.performance
|
||||
.getEntriesByType("navigation")
|
||||
.map((nav) => (nav as PerformanceNavigationTiming).type)
|
||||
.includes("reload")
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollableElement || !checkedPageReload) {
|
||||
return
|
||||
}
|
||||
|
||||
if (getScrolledTop(scrollableElement) !== 0 && !location.hash) {
|
||||
scrollableElement?.scrollTo({
|
||||
top: 0,
|
||||
})
|
||||
} else if (location.hash) {
|
||||
// retrieve and scroll to element
|
||||
const targetElm =
|
||||
scrollableElement && typeof document !== "undefined"
|
||||
? document.getElementById(location.hash.replace("#", ""))
|
||||
: undefined
|
||||
|
||||
targetElm?.scrollIntoView()
|
||||
}
|
||||
}, [pathname, scrollableElement])
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollableElement || checkedPageReload) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isPageReloaded()) {
|
||||
const loadedScrollPosition = localStorage.getItem("scrollPos")
|
||||
if (loadedScrollPosition) {
|
||||
scrollableElement?.scrollTo({
|
||||
top: parseInt(loadedScrollPosition),
|
||||
})
|
||||
}
|
||||
localStorage.removeItem("scrollPos")
|
||||
}
|
||||
|
||||
setCheckedPageReload(true)
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
const scrollPos = isElmWindow(scrollableElement)
|
||||
? scrollableElement.screenY
|
||||
: scrollableElement?.scrollTop
|
||||
|
||||
if (scrollPos) {
|
||||
localStorage.setItem(
|
||||
"scrollPos",
|
||||
`${
|
||||
isElmWindow(scrollableElement)
|
||||
? scrollableElement.screenY
|
||||
: scrollableElement?.scrollTop
|
||||
}`
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [scrollableElement])
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ApiTestingOptions } from "types"
|
||||
|
||||
export type useRequestRunnerProps = {
|
||||
pushLog: (...message: string[]) => void
|
||||
onFinish: (message: string) => void
|
||||
replaceLog?: (message: string) => void
|
||||
}
|
||||
|
||||
export const useRequestRunner = ({
|
||||
pushLog,
|
||||
onFinish,
|
||||
replaceLog,
|
||||
}: useRequestRunnerProps) => {
|
||||
const runRequest = (apiRequestOptions: ApiTestingOptions) => {
|
||||
let requestUrl = apiRequestOptions.url
|
||||
if (apiRequestOptions.pathData) {
|
||||
Object.entries(apiRequestOptions.pathData).forEach(([key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
requestUrl = requestUrl.replace(`{${key}}`, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// check if there are query params
|
||||
if (apiRequestOptions.queryData) {
|
||||
requestUrl += `?${new URLSearchParams(
|
||||
apiRequestOptions.queryData as Record<string, string>
|
||||
).toString()}`
|
||||
}
|
||||
|
||||
fetch(requestUrl, {
|
||||
method: apiRequestOptions.method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: apiRequestOptions.bodyData
|
||||
? JSON.stringify(apiRequestOptions.bodyData)
|
||||
: undefined,
|
||||
})
|
||||
.then(async (response) => response.json())
|
||||
.then((data) => {
|
||||
const stringifiedData = JSON.stringify(data, undefined, 2)
|
||||
replaceLog ? replaceLog(stringifiedData) : pushLog(stringifiedData)
|
||||
})
|
||||
.catch((error) => {
|
||||
pushLog(
|
||||
`An error ocurred: ${JSON.stringify(error, undefined, 2)}`,
|
||||
`\nThis could be a CORS error. You can resolve it by adding\nthe docs' URLto your CORS configurations:\n`,
|
||||
`STORE_CORS=http://localhost:8000,https://docs.medusajs.com`,
|
||||
`ADMIN_CORS=http://localhost:7001,https://docs.medusajs.com`
|
||||
)
|
||||
})
|
||||
.finally(() => onFinish(`Finished running request.`))
|
||||
}
|
||||
|
||||
return {
|
||||
runRequest,
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import React, {
|
||||
type ReactNode,
|
||||
useState,
|
||||
} from "react"
|
||||
import { getScrolledTop as getScrolledTopUtil } from "../../utils"
|
||||
import { getScrolledTop as getScrolledTopUtil, isElmWindow } from "../../utils"
|
||||
|
||||
type EventFunc = (...args: never[]) => unknown
|
||||
|
||||
@@ -61,9 +61,12 @@ type ScrollController = {
|
||||
getScrolledTop: () => number
|
||||
}
|
||||
|
||||
function useScrollControllerContextValue(
|
||||
function useScrollControllerContextValue({
|
||||
scrollableSelector,
|
||||
}: {
|
||||
scrollableSelector: string
|
||||
): ScrollController {
|
||||
restoreScrollOnReload?: boolean
|
||||
}): ScrollController {
|
||||
const scrollEventsEnabledRef = useRef(true)
|
||||
|
||||
const [scrollableElement, setScrollableElement] = useState<
|
||||
@@ -106,8 +109,11 @@ export function ScrollControllerProvider({
|
||||
}: {
|
||||
children: ReactNode
|
||||
scrollableSelector?: string
|
||||
restoreScrollOnReload?: boolean
|
||||
}): JSX.Element {
|
||||
const value = useScrollControllerContextValue(scrollableSelector)
|
||||
const value = useScrollControllerContextValue({
|
||||
scrollableSelector,
|
||||
})
|
||||
return (
|
||||
<ScrollMonitorContext.Provider value={value}>
|
||||
{children}
|
||||
|
||||
@@ -2,6 +2,6 @@ export * from "./constants"
|
||||
|
||||
export * from "./components"
|
||||
export * from "./hooks"
|
||||
export * from "./layouts"
|
||||
export * from "./providers"
|
||||
export * from "./types"
|
||||
export * from "./utils"
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./root"
|
||||
export * from "./tight"
|
||||
export * from "./wide"
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { Sidebar, SidebarProps } from "@/components"
|
||||
|
||||
export type RootLayoutProps = {
|
||||
children: React.ReactNode
|
||||
ProvidersComponent: React.FC<{ children: React.ReactNode }>
|
||||
NavbarComponent: React.FC
|
||||
sidebarProps?: SidebarProps
|
||||
htmlClassName?: string
|
||||
bodyClassName?: string
|
||||
mainWrapperClasses?: string
|
||||
showPagination?: boolean
|
||||
}
|
||||
|
||||
export const RootLayout = ({
|
||||
ProvidersComponent,
|
||||
NavbarComponent,
|
||||
children,
|
||||
sidebarProps,
|
||||
htmlClassName,
|
||||
bodyClassName,
|
||||
mainWrapperClasses,
|
||||
}: RootLayoutProps) => {
|
||||
return (
|
||||
<html lang="en" className={clsx("h-full w-full", htmlClassName)}>
|
||||
<head />
|
||||
<body
|
||||
className={clsx(
|
||||
"bg-docs-bg font-base text-medium w-full",
|
||||
"text-medusa-fg-subtle",
|
||||
"h-screen overflow-hidden",
|
||||
bodyClassName
|
||||
)}
|
||||
>
|
||||
<ProvidersComponent>
|
||||
<NavbarComponent />
|
||||
<div
|
||||
className="w-full h-[calc(100%-57px)] overflow-y-scroll overflow-x-hidden"
|
||||
id="main"
|
||||
>
|
||||
<div className={clsx("max-w-xxl w-full", mainWrapperClasses)}>
|
||||
<Sidebar {...sidebarProps} />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</ProvidersComponent>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from "react"
|
||||
import { RootLayout, RootLayoutProps } from "./root"
|
||||
import clsx from "clsx"
|
||||
import { Pagination } from ".."
|
||||
|
||||
export const TightLayout = ({
|
||||
children,
|
||||
showPagination,
|
||||
...props
|
||||
}: RootLayoutProps) => {
|
||||
return (
|
||||
<RootLayout
|
||||
{...props}
|
||||
mainWrapperClasses={clsx(
|
||||
props.mainWrapperClasses,
|
||||
"grid grid-cols-1 px-docs_1.5 lg:mx-auto lg:grid-cols-[280px_1fr]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"relative flex w-full flex-1 items-start justify-center",
|
||||
" px-docs_1 pb-docs_2 pt-docs_7 md:px-docs_2 lg:px-docs_4 lg:py-docs_7"
|
||||
)}
|
||||
>
|
||||
<main className="h-full w-full lg:max-w-[720px] lg:px-px">
|
||||
{children}
|
||||
{showPagination && <Pagination />}
|
||||
</main>
|
||||
</div>
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react"
|
||||
import { RootLayout, RootLayoutProps } from "./root"
|
||||
import clsx from "clsx"
|
||||
import { Pagination } from ".."
|
||||
|
||||
export const WideLayout = ({
|
||||
children,
|
||||
showPagination,
|
||||
...props
|
||||
}: RootLayoutProps) => {
|
||||
return (
|
||||
<RootLayout
|
||||
{...props}
|
||||
mainWrapperClasses={clsx(props.mainWrapperClasses, "mx-auto flex px-1.5")}
|
||||
>
|
||||
<main className="lg:w-ref-main relative mt-4 w-full flex-1 lg:mt-7">
|
||||
{children}
|
||||
{showPagination && <Pagination />}
|
||||
</main>
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,264 @@
|
||||
"use client"
|
||||
|
||||
import { useIsBrowser } from "@/hooks"
|
||||
import { getLearningPath } from "@/utils/learning-paths"
|
||||
import React, { createContext, useContext, useEffect, useState } from "react"
|
||||
import { LearningPathFinishType } from "@/components/LearningPath/Finish"
|
||||
import { useAnalytics } from "docs-ui"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
|
||||
export type LearningPathType = {
|
||||
name: string
|
||||
label: string
|
||||
description?: string
|
||||
steps: LearningPathStepType[]
|
||||
finish?: LearningPathFinishType
|
||||
notificationId?: string
|
||||
}
|
||||
|
||||
export type LearningPathStepType = {
|
||||
title?: string
|
||||
description?: string
|
||||
descriptionJSX?: JSX.Element
|
||||
path?: string
|
||||
}
|
||||
|
||||
export type LearningPathContextType = {
|
||||
path: LearningPathType | null
|
||||
setPath: (value: LearningPathType) => void
|
||||
currentStep: number
|
||||
setCurrentStep: (value: number) => void
|
||||
startPath: (path: LearningPathType) => void
|
||||
updatePath: (data: Pick<LearningPathType, "notificationId">) => void
|
||||
endPath: () => void
|
||||
nextStep: () => void
|
||||
hasNextStep: () => boolean
|
||||
previousStep: () => void
|
||||
hasPreviousStep: () => boolean
|
||||
goToStep: (stepIndex: number) => void
|
||||
isCurrentPath: () => boolean
|
||||
goToCurrentPath: () => void
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
type LearningPathProviderProps = {
|
||||
children?: React.ReactNode
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
const LearningPathContext = createContext<LearningPathContextType | null>(null)
|
||||
|
||||
export const LearningPathProvider: React.FC<LearningPathProviderProps> = ({
|
||||
children,
|
||||
baseUrl,
|
||||
}) => {
|
||||
const [path, setPath] = useState<LearningPathType | null>(null)
|
||||
const [currentStep, setCurrentStep] = useState(-1)
|
||||
const isBrowser = useIsBrowser()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const { track } = useAnalytics()
|
||||
|
||||
const startPath = (path: LearningPathType) => {
|
||||
setPath(path)
|
||||
setCurrentStep(-1)
|
||||
if (isBrowser) {
|
||||
localStorage.setItem(
|
||||
"learning-path",
|
||||
JSON.stringify({
|
||||
pathName: path.name,
|
||||
currentStep: -1,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
track(`learning_path_${path.name}`, {
|
||||
url: pathname,
|
||||
state: `start`,
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (path && currentStep === -1) {
|
||||
nextStep()
|
||||
}
|
||||
}, [path])
|
||||
|
||||
const endPath = () => {
|
||||
const didFinish = currentStep === (path?.steps.length || 0) - 1
|
||||
const reachedIndex = currentStep === -1 ? 0 : currentStep
|
||||
track(`learning_path_${path?.name}`, {
|
||||
url: pathname,
|
||||
state: !didFinish ? `closed` : `end`,
|
||||
reachedStep:
|
||||
path?.steps[reachedIndex]?.title ||
|
||||
path?.steps[reachedIndex]?.description ||
|
||||
path?.steps[reachedIndex]?.descriptionJSX ||
|
||||
reachedIndex,
|
||||
})
|
||||
setPath(null)
|
||||
setCurrentStep(-1)
|
||||
if (isBrowser) {
|
||||
localStorage.removeItem("learning-path")
|
||||
}
|
||||
}
|
||||
|
||||
const hasNextStep = () => currentStep !== (path?.steps.length || 0) - 1
|
||||
|
||||
const nextStep = () => {
|
||||
if (!path || !hasNextStep()) {
|
||||
return
|
||||
}
|
||||
const nextStepIndex = currentStep + 1
|
||||
setCurrentStep(nextStepIndex)
|
||||
const newPath = path.steps[nextStepIndex].path
|
||||
if (isBrowser) {
|
||||
localStorage.setItem(
|
||||
"learning-path",
|
||||
JSON.stringify({
|
||||
pathName: path.name,
|
||||
currentStep: nextStepIndex,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (pathname !== newPath && newPath) {
|
||||
router.push(newPath)
|
||||
}
|
||||
}
|
||||
|
||||
const hasPreviousStep = () => currentStep > 0
|
||||
|
||||
const previousStep = () => {
|
||||
if (!path || !hasPreviousStep()) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousStepIndex = currentStep - 1
|
||||
setCurrentStep(previousStepIndex)
|
||||
const newPath = path.steps[previousStepIndex].path
|
||||
if (isBrowser) {
|
||||
localStorage.setItem(
|
||||
"learning-path",
|
||||
JSON.stringify({
|
||||
pathName: path.name,
|
||||
currentStep: previousStepIndex,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (pathname !== newPath && newPath) {
|
||||
router.push(newPath)
|
||||
}
|
||||
}
|
||||
|
||||
const goToStep = (stepIndex: number) => {
|
||||
if (!path || stepIndex >= path.steps.length) {
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentStep(stepIndex)
|
||||
const newPath = path.steps[stepIndex].path
|
||||
if (isBrowser) {
|
||||
localStorage.setItem(
|
||||
"learning-path",
|
||||
JSON.stringify({
|
||||
pathName: path.name,
|
||||
currentStep: stepIndex,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (pathname !== newPath && newPath) {
|
||||
router.push(newPath)
|
||||
}
|
||||
}
|
||||
|
||||
const isCurrentPath = () => {
|
||||
if (!path || currentStep === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
return pathname === path.steps[currentStep].path
|
||||
}
|
||||
|
||||
const goToCurrentPath = () => {
|
||||
if (!path || currentStep === -1 || !path.steps[currentStep].path) {
|
||||
return
|
||||
}
|
||||
|
||||
router.push(path.steps[currentStep].path!)
|
||||
}
|
||||
|
||||
const updatePath = (data: Pick<LearningPathType, "notificationId">) => {
|
||||
if (!path) {
|
||||
return
|
||||
}
|
||||
setPath({
|
||||
...path,
|
||||
...data,
|
||||
})
|
||||
}
|
||||
|
||||
const initPath = () => {
|
||||
if (isBrowser) {
|
||||
// give query parameters higher precedence over local storage
|
||||
const queryPathName = new URLSearchParams(location.search).get("path")
|
||||
const queryPath = queryPathName
|
||||
? getLearningPath(queryPathName)
|
||||
: undefined
|
||||
if (queryPath) {
|
||||
startPath(queryPath)
|
||||
} else {
|
||||
const storedPath = localStorage.getItem("learning-path")
|
||||
if (storedPath) {
|
||||
const storedPathParsed = JSON.parse(storedPath)
|
||||
const currentPath = getLearningPath(storedPathParsed?.pathName)
|
||||
if (currentPath) {
|
||||
setPath(currentPath)
|
||||
setCurrentStep(storedPathParsed?.currentStep || 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isBrowser && !path) {
|
||||
initPath()
|
||||
}
|
||||
}, [isBrowser])
|
||||
|
||||
return (
|
||||
<LearningPathContext.Provider
|
||||
value={{
|
||||
path,
|
||||
setPath,
|
||||
currentStep,
|
||||
setCurrentStep,
|
||||
startPath,
|
||||
updatePath,
|
||||
endPath,
|
||||
nextStep,
|
||||
hasNextStep,
|
||||
previousStep,
|
||||
hasPreviousStep,
|
||||
goToStep,
|
||||
isCurrentPath,
|
||||
goToCurrentPath,
|
||||
baseUrl,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LearningPathContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useLearningPath = () => {
|
||||
const context = useContext(LearningPathContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useLearningPath must be used within a LearningPathProvider"
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client"
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react"
|
||||
import { useSidebar } from "../Sidebar"
|
||||
import { usePrevious } from "@uidotdev/usehooks"
|
||||
import { SidebarItemType } from "types"
|
||||
|
||||
export type Page = {
|
||||
title: string
|
||||
description?: string
|
||||
link: string
|
||||
}
|
||||
|
||||
export type PaginationContextType = {
|
||||
nextPage?: Page
|
||||
previousPage?: Page
|
||||
}
|
||||
|
||||
export const PaginationContext = createContext<PaginationContextType | null>(
|
||||
null
|
||||
)
|
||||
|
||||
type SearchItemsResult = {
|
||||
foundActive: boolean
|
||||
prevItem?: SidebarItemType
|
||||
nextItem?: SidebarItemType
|
||||
}
|
||||
|
||||
export type PaginationProviderProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const PaginationProvider = ({ children }: PaginationProviderProps) => {
|
||||
const { items, activePath } = useSidebar()
|
||||
const combinedItems = useMemo(() => [...items.top, ...items.bottom], [items])
|
||||
const previousActivePath = usePrevious(activePath)
|
||||
const [nextPage, setNextPage] = useState<Page | undefined>()
|
||||
const [prevPage, setPrevPage] = useState<Page | undefined>()
|
||||
|
||||
const getFirstChild = (
|
||||
item: SidebarItemType
|
||||
): SidebarItemType | undefined => {
|
||||
const children = getChildrenWithPages(item)
|
||||
if (!children?.length) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return children[0].path ? children[0] : getFirstChild(children[0])
|
||||
}
|
||||
|
||||
const getChildrenWithPages = (
|
||||
item: SidebarItemType
|
||||
): SidebarItemType[] | undefined => {
|
||||
return item.children?.filter(
|
||||
(childItem) =>
|
||||
childItem.path !== undefined || getChildrenWithPages(childItem)?.length
|
||||
)
|
||||
}
|
||||
|
||||
const getPrevItem = (
|
||||
items: SidebarItemType[],
|
||||
index: number
|
||||
): SidebarItemType | undefined => {
|
||||
let foundItem: SidebarItemType | undefined
|
||||
items
|
||||
.slice(0, index)
|
||||
.reverse()
|
||||
.some((item) => {
|
||||
if (item.children?.length) {
|
||||
foundItem = getPrevItem(item.children, item.children.length)
|
||||
} else if (item.path) {
|
||||
foundItem = item
|
||||
}
|
||||
|
||||
return foundItem !== undefined
|
||||
})
|
||||
|
||||
return foundItem
|
||||
}
|
||||
|
||||
const getNextItem = (
|
||||
items: SidebarItemType[],
|
||||
index: number
|
||||
): SidebarItemType | undefined => {
|
||||
let foundItem: SidebarItemType | undefined
|
||||
items.slice(index + 1).some((item) => {
|
||||
if (item.path) {
|
||||
foundItem = item
|
||||
} else if (item.children?.length) {
|
||||
foundItem = getNextItem(item.children, -1)
|
||||
}
|
||||
|
||||
return foundItem !== undefined
|
||||
})
|
||||
|
||||
return foundItem
|
||||
}
|
||||
|
||||
const searchItems = (currentItems: SidebarItemType[]): SearchItemsResult => {
|
||||
const result: SearchItemsResult = {
|
||||
foundActive: false,
|
||||
}
|
||||
|
||||
result.foundActive = currentItems.some((item, index) => {
|
||||
if (item.path === activePath) {
|
||||
if (index !== 0) {
|
||||
result.prevItem = getPrevItem(currentItems, index)
|
||||
}
|
||||
|
||||
if (item.children?.length) {
|
||||
result.nextItem = getFirstChild(item)
|
||||
}
|
||||
|
||||
if (!result.nextItem && index !== currentItems.length - 1) {
|
||||
result.nextItem = getNextItem(currentItems, index)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (item.children?.length) {
|
||||
const childrenResult = searchItems(item.children)
|
||||
|
||||
if (childrenResult.foundActive) {
|
||||
result.prevItem = childrenResult.prevItem
|
||||
result.nextItem = childrenResult.nextItem
|
||||
if (!result.prevItem) {
|
||||
result.prevItem = item.path
|
||||
? item
|
||||
: getPrevItem(currentItems, index)
|
||||
}
|
||||
|
||||
if (!result.nextItem && index !== currentItems.length - 1) {
|
||||
result.nextItem = getNextItem(currentItems, index)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activePath !== previousActivePath) {
|
||||
const result = searchItems(combinedItems)
|
||||
setPrevPage(
|
||||
result.prevItem
|
||||
? {
|
||||
title: result.prevItem.title,
|
||||
link: result.prevItem.path || "",
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
setNextPage(
|
||||
result.nextItem
|
||||
? {
|
||||
title: result.nextItem.title,
|
||||
link: result.nextItem.path || "",
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
}, [activePath, previousActivePath])
|
||||
|
||||
return (
|
||||
<PaginationContext.Provider
|
||||
value={{
|
||||
previousPage: prevPage,
|
||||
nextPage,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PaginationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePagination = (): PaginationContextType => {
|
||||
const context = useContext(PaginationContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("usePagination must be used inside a PaginationProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -7,34 +7,30 @@ import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { getScrolledTop } from "../../utils"
|
||||
import { useIsBrowser } from "../../hooks"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { getScrolledTop } from "@/utils"
|
||||
import { useIsBrowser } from "@/hooks"
|
||||
import {
|
||||
SidebarItemSections,
|
||||
SidebarItemType,
|
||||
SidebarSectionItemsType,
|
||||
} from "types"
|
||||
|
||||
export enum SidebarItemSections {
|
||||
TOP = "top",
|
||||
BOTTOM = "bottom",
|
||||
MOBILE = "mobile",
|
||||
export type CurrentItemsState = SidebarSectionItemsType & {
|
||||
previousSidebar?: CurrentItemsState
|
||||
}
|
||||
|
||||
export type SidebarItemType = {
|
||||
path?: string
|
||||
title: string
|
||||
additionalElms?: React.ReactNode
|
||||
children?: SidebarItemType[]
|
||||
loaded?: boolean
|
||||
isPathHref?: boolean
|
||||
linkProps?: React.AllHTMLAttributes<HTMLAnchorElement>
|
||||
}
|
||||
|
||||
export type SidebarSectionItemsType = {
|
||||
[k in SidebarItemSections]: SidebarItemType[]
|
||||
export type SidebarStyleOptions = {
|
||||
disableActiveTransition?: boolean
|
||||
noTitleStyling?: boolean
|
||||
}
|
||||
|
||||
export type SidebarContextType = {
|
||||
items: SidebarSectionItemsType
|
||||
currentItems: CurrentItemsState | undefined
|
||||
activePath: string | null
|
||||
getActiveItem: () => SidebarItemType | undefined
|
||||
setActivePath: (path: string | null) => void
|
||||
@@ -61,7 +57,11 @@ export type SidebarContextType = {
|
||||
isSidebarEmpty: () => boolean
|
||||
desktopSidebarOpen: boolean
|
||||
setDesktopSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
staticSidebarItems?: boolean
|
||||
shouldHandleHashChange: boolean
|
||||
sidebarRef: React.RefObject<HTMLUListElement>
|
||||
goBack: () => void
|
||||
} & SidebarStyleOptions
|
||||
|
||||
export const SidebarContext = createContext<SidebarContextType | null>(null)
|
||||
|
||||
@@ -86,16 +86,21 @@ const findItem = (
|
||||
item: Partial<SidebarItemType>,
|
||||
checkChildren = true
|
||||
): SidebarItemType | undefined => {
|
||||
return section.find((i) => {
|
||||
if (!item.path) {
|
||||
return !i.path && i.title === item.title
|
||||
} else {
|
||||
return (
|
||||
i.path === item.path ||
|
||||
(checkChildren && i.children && findItem(i.children, item))
|
||||
)
|
||||
let foundItem: SidebarItemType | undefined
|
||||
section.some((i) => {
|
||||
if (
|
||||
(!item.path && !i.path && i.title === item.title) ||
|
||||
i.path === item.path
|
||||
) {
|
||||
foundItem = i
|
||||
} else if (checkChildren && i.children) {
|
||||
foundItem = findItem(i.children, item)
|
||||
}
|
||||
|
||||
return foundItem !== undefined
|
||||
})
|
||||
|
||||
return foundItem
|
||||
}
|
||||
|
||||
export const reducer = (
|
||||
@@ -160,7 +165,8 @@ export type SidebarProviderProps = {
|
||||
shouldHandleHashChange?: boolean
|
||||
shouldHandlePathChange?: boolean
|
||||
scrollableElement?: Element | Window
|
||||
}
|
||||
staticSidebarItems?: boolean
|
||||
} & SidebarStyleOptions
|
||||
|
||||
export const SidebarProvider = ({
|
||||
children,
|
||||
@@ -170,16 +176,25 @@ export const SidebarProvider = ({
|
||||
shouldHandleHashChange = false,
|
||||
shouldHandlePathChange = false,
|
||||
scrollableElement,
|
||||
staticSidebarItems = false,
|
||||
disableActiveTransition = false,
|
||||
noTitleStyling = false,
|
||||
}: SidebarProviderProps) => {
|
||||
const [items, dispatch] = useReducer(reducer, {
|
||||
top: initialItems?.top || [],
|
||||
bottom: initialItems?.bottom || [],
|
||||
mobile: initialItems?.mobile || [],
|
||||
})
|
||||
const [currentItems, setCurrentItems] = useState<
|
||||
CurrentItemsState | undefined
|
||||
>()
|
||||
const [activePath, setActivePath] = useState<string | null>("")
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState<boolean>(false)
|
||||
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true)
|
||||
const sidebarRef = useRef<HTMLUListElement>(null)
|
||||
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const isBrowser = useIsBrowser()
|
||||
const getResolvedScrollableElement = useCallback(() => {
|
||||
return scrollableElement || window
|
||||
@@ -229,9 +244,13 @@ export const SidebarProvider = ({
|
||||
)
|
||||
|
||||
const isSidebarEmpty = useCallback((): boolean => {
|
||||
return Object.values(items).every(
|
||||
(sectionItems) => sectionItems.length === 0
|
||||
)
|
||||
return Object.values(items).every((sectionItems) => {
|
||||
if (!Array.isArray(sectionItems)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return sectionItems.length === 0
|
||||
})
|
||||
}, [items])
|
||||
|
||||
const init = () => {
|
||||
@@ -241,6 +260,54 @@ export const SidebarProvider = ({
|
||||
}
|
||||
}
|
||||
|
||||
const getCurrentSidebar = useCallback(
|
||||
(searchItems: SidebarItemType[]): SidebarItemType | undefined => {
|
||||
let currentSidebar: SidebarItemType | undefined
|
||||
searchItems.some((item) => {
|
||||
if (item.isChildSidebar) {
|
||||
if (isItemActive(item)) {
|
||||
currentSidebar = item
|
||||
} else if (item.children?.length) {
|
||||
const childSidebar =
|
||||
getCurrentSidebar(item.children) ||
|
||||
findItem(item.children, { path: activePath || undefined })
|
||||
|
||||
if (childSidebar) {
|
||||
currentSidebar = childSidebar.isChildSidebar ? childSidebar : item
|
||||
}
|
||||
}
|
||||
} else if (item.children?.length) {
|
||||
currentSidebar = getCurrentSidebar(item.children)
|
||||
}
|
||||
|
||||
return currentSidebar !== undefined
|
||||
})
|
||||
|
||||
return currentSidebar
|
||||
},
|
||||
[isItemActive, activePath]
|
||||
)
|
||||
|
||||
const goBack = () => {
|
||||
if (!currentItems) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousSidebar = currentItems.previousSidebar || items
|
||||
|
||||
const backItem =
|
||||
previousSidebar.top.find((item) => item.path && !item.isChildSidebar) ||
|
||||
previousSidebar.bottom.find((item) => item.path && !item.isChildSidebar)
|
||||
|
||||
if (!backItem) {
|
||||
return
|
||||
}
|
||||
|
||||
setActivePath(backItem.path!)
|
||||
setCurrentItems(currentItems.previousSidebar)
|
||||
router.replace(backItem.path!)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldHandleHashChange) {
|
||||
init()
|
||||
@@ -297,15 +364,50 @@ export const SidebarProvider = ({
|
||||
}, [items, isLoading, setIsLoading])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldHandlePathChange && pathname !== activePath) {
|
||||
if (!shouldHandlePathChange) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pathname !== activePath) {
|
||||
setActivePath(pathname)
|
||||
}
|
||||
}, [shouldHandlePathChange, pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activePath?.length) {
|
||||
setCurrentItems(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const currentSidebar =
|
||||
getCurrentSidebar(items.top) || getCurrentSidebar(items.bottom)
|
||||
|
||||
if (!currentSidebar) {
|
||||
setCurrentItems(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
currentSidebar.isChildSidebar &&
|
||||
currentSidebar.children &&
|
||||
currentItems?.parentItem?.path !== currentSidebar.path
|
||||
) {
|
||||
const { children, ...parentItem } = currentSidebar
|
||||
setCurrentItems({
|
||||
top: children,
|
||||
bottom: [],
|
||||
mobile: items.mobile,
|
||||
parentItem: parentItem,
|
||||
previousSidebar: currentItems,
|
||||
})
|
||||
}
|
||||
}, [getCurrentSidebar, activePath])
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider
|
||||
value={{
|
||||
items,
|
||||
currentItems,
|
||||
addItems,
|
||||
activePath,
|
||||
setActivePath,
|
||||
@@ -317,6 +419,12 @@ export const SidebarProvider = ({
|
||||
getActiveItem,
|
||||
desktopSidebarOpen,
|
||||
setDesktopSidebarOpen,
|
||||
staticSidebarItems,
|
||||
disableActiveTransition,
|
||||
noTitleStyling,
|
||||
shouldHandleHashChange,
|
||||
sidebarRef,
|
||||
goBack,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
export * from "./AiAssistant"
|
||||
export * from "./Analytics"
|
||||
export * from "./ColorMode"
|
||||
export * from "./LearningPath"
|
||||
export * from "./Mobile"
|
||||
export * from "./Modal"
|
||||
export * from "./Navbar"
|
||||
export * from "./Notification"
|
||||
export * from "./PageLoading"
|
||||
export * from "./Pagination"
|
||||
export * from "./Search"
|
||||
export * from "./Sidebar"
|
||||
@@ -1,3 +0,0 @@
|
||||
export type Ref<T> =
|
||||
| React.MutableRefObject<T | null>
|
||||
| ((instance: T | null) => void)
|
||||
@@ -1,21 +1,42 @@
|
||||
const TOP_MARGIN = 57
|
||||
|
||||
export function checkSidebarItemVisibility(
|
||||
item: Element,
|
||||
options?: {
|
||||
topMargin?: number
|
||||
}
|
||||
item: HTMLElement,
|
||||
withTransition = false
|
||||
) {
|
||||
return withTransition
|
||||
? checkSidebarItemVisibilityTransition(item)
|
||||
: checkSidebarItemVisibilityRelative(item)
|
||||
}
|
||||
|
||||
function checkSidebarItemVisibilityRelative(item: HTMLElement) {
|
||||
const sidebar = document.getElementById("sidebar")
|
||||
if (!sidebar) {
|
||||
return false
|
||||
}
|
||||
const sidebarBoundingRect = sidebar.getBoundingClientRect()
|
||||
const sidebarTop = sidebarBoundingRect.top - TOP_MARGIN
|
||||
const sidebarBottom = sidebarTop + sidebarBoundingRect.height
|
||||
const itemBoundingRect = item.getBoundingClientRect()
|
||||
const itemTop =
|
||||
item.offsetParent === sidebar ? item.offsetTop : itemBoundingRect.top
|
||||
const itemBottom = itemTop + itemBoundingRect.height
|
||||
|
||||
return itemTop >= sidebarTop && itemBottom <= sidebarBottom
|
||||
}
|
||||
|
||||
function checkSidebarItemVisibilityTransition(item: HTMLElement) {
|
||||
const sidebar = document.getElementById("sidebar")
|
||||
if (!sidebar) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { topMargin = 0 } = options || {}
|
||||
const sidebarBoundingRect = sidebar.getBoundingClientRect()
|
||||
const activeItemBoundingRect = item.getBoundingClientRect()
|
||||
|
||||
return (
|
||||
activeItemBoundingRect.top >= topMargin &&
|
||||
activeItemBoundingRect.top - sidebarBoundingRect.height + topMargin < 0 &&
|
||||
activeItemBoundingRect.top >= TOP_MARGIN &&
|
||||
activeItemBoundingRect.top - sidebarBoundingRect.height + TOP_MARGIN < 0 &&
|
||||
activeItemBoundingRect.bottom > 0
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function decodeStr(str: string) {
|
||||
return str
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll("{", "{")
|
||||
.replaceAll("}", "}")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll("\\|", "|")
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function getLinkWithBasePath(path: string, basePath?: string): string {
|
||||
return `${basePath || ""}${path}`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NavbarLinkProps, navbarItems } from ".."
|
||||
|
||||
type Options = {
|
||||
basePath: string
|
||||
activePath: string
|
||||
}
|
||||
|
||||
export function getNavbarItems({
|
||||
basePath,
|
||||
activePath,
|
||||
}: Options): NavbarLinkProps[] {
|
||||
return navbarItems.map((item) => ({
|
||||
...item,
|
||||
isActive: activePath === item.href,
|
||||
href: `${basePath}${item.href}`,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function getOsShortcut() {
|
||||
const isMacOs =
|
||||
typeof navigator !== "undefined"
|
||||
? navigator.userAgent.toLowerCase().indexOf("mac") !== 0
|
||||
: true
|
||||
|
||||
return isMacOs ? "⌘" : "Ctrl"
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
export * from "./array-same-elms"
|
||||
export * from "./capitalize"
|
||||
export * from "./check-sidebar-item-visibility"
|
||||
export * from "./decode-str"
|
||||
export * from "./dom-utils"
|
||||
export * from "./format-report-link"
|
||||
export * from "./get-link-with-base-path"
|
||||
export * from "./get-navbar-items"
|
||||
export * from "./get-os-shortcut"
|
||||
export * from "./get-scrolled-top"
|
||||
export * from "./is-elm-window"
|
||||
export * from "./is-in-view"
|
||||
export * from "./learning-paths"
|
||||
export * from "./sidebar-attach-href-common-options"
|
||||
export * from "./swr-fetcher"
|
||||
@@ -0,0 +1,10 @@
|
||||
export function isInView(element: Element): boolean {
|
||||
const rect = element.getBoundingClientRect()
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <=
|
||||
(window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,861 @@
|
||||
import React from "react"
|
||||
import { LearningPathType } from "../providers/LearningPath"
|
||||
import { Link } from "@/components"
|
||||
|
||||
const paths: LearningPathType[] = [
|
||||
{
|
||||
name: "simple-quickstart",
|
||||
label: "Start Selling in 3 Steps",
|
||||
description: "Create and deploy a full-fledged ecommerce store.",
|
||||
steps: [
|
||||
{
|
||||
title: "Create a Next.js Starter Template",
|
||||
description:
|
||||
"Create a Next.js Starter Template and connect it to your Medusa backend.",
|
||||
path: "#",
|
||||
},
|
||||
{
|
||||
title: "Deploy the backend",
|
||||
path: "#",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Deploy your backend to Railway. You can alternatively check out{" "}
|
||||
<Link href="/deployments/server">other deployment guides</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Deploy the storefront",
|
||||
description: "Deploy your storefront to your preferred hosting.",
|
||||
path: "#",
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on building your store!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_simple-quickstart",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "marketplace",
|
||||
label: "Build a marketplace",
|
||||
description:
|
||||
"Customize the backend and handle events to build a marketplace.",
|
||||
steps: [
|
||||
{
|
||||
title: "Extend entities",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Extend entities, such as <code>User</code> or <code>Product</code>{" "}
|
||||
entities, to associate them with the <code>Store</code> entity.
|
||||
</>
|
||||
),
|
||||
path: "/development/entities/extend-entity",
|
||||
},
|
||||
{
|
||||
title: "Access logged-in user",
|
||||
description:
|
||||
"Create a middleware that registers the logged-in user in the dependency container.",
|
||||
path: "/development/api-routes/example-logged-in-user",
|
||||
},
|
||||
{
|
||||
title: "Extend services",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Extend services, such as <code>ProductService</code> to customize
|
||||
data management functionalities
|
||||
</>
|
||||
),
|
||||
path: "/development/services/extend-service",
|
||||
},
|
||||
{
|
||||
title: "Handle events",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Listen to events like <code>order.placed</code> and handle them with
|
||||
subscribers
|
||||
</>
|
||||
),
|
||||
path: "/development/events/create-subscriber",
|
||||
},
|
||||
{
|
||||
title: "Add Payment Provider",
|
||||
path: "/plugins/payment",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Add a payment provider to your Medusa backend. You can choose to
|
||||
install a plugin or{" "}
|
||||
<Link href="/modules/carts-and-checkout/backend/add-payment-provider">
|
||||
create your own provider
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Customize Admin",
|
||||
path: "/admin/widgets",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
As you add marketplace features to your store, you'll most
|
||||
likely need to customize the admin to provide an interface to manage
|
||||
these features.
|
||||
<br />
|
||||
You can extend the admin plugin to add widgets,{" "}
|
||||
<Link href="/admin/routes">UI routes</Link>, or{" "}
|
||||
<Link href="/admin/setting-pages">setting pages</Link>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Implement Role-Based Access Control",
|
||||
path: "/modules/users/backend/rbac",
|
||||
description:
|
||||
"In your marketplace, you may need to implement role-based access control (RBAC) within stores. This will restrict some users' permissions to specified functionalities or API Routes.",
|
||||
},
|
||||
{
|
||||
title: "Create a storefront",
|
||||
path: "/starters/nextjs-medusa-starter",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Build a storefront either using the Next.js Starter Template or{" "}
|
||||
<Link href="/storefront/roadmap">from scratch</Link>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Deploy the backend",
|
||||
path: "/deployments/server/deploying-on-railway",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Deploy your backend to Railway. You can alternatively check out{" "}
|
||||
<Link href="/deployments/server">other deployment guides</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on building your marketplace!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_marketplace",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "subscriptions",
|
||||
label: "Build Subscription-based Purchases",
|
||||
description:
|
||||
"Customize the backend and handle events to implement subscriptions",
|
||||
steps: [
|
||||
{
|
||||
title: "Extend entities",
|
||||
path: "/development/entities/extend-entity",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Extend entities, such as the <code>Order</code> entity, to associate
|
||||
them with the <code>Store</code> entity. You can also{" "}
|
||||
<Link href="/development/entities/create">
|
||||
Create a custom entity
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Handle events",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Create a subscriber that listens to the <code>order.placed</code>{" "}
|
||||
event, or other{" "}
|
||||
<Link href="/development/events/events-list">events</Link>, and
|
||||
handles creating the subscription in Medusa.
|
||||
</>
|
||||
),
|
||||
path: "/development/events/create-subscriber",
|
||||
},
|
||||
{
|
||||
title: "Create a Scheduled Job",
|
||||
description:
|
||||
"Create a scheduled job that checks daily for subscriptions that needs renewal.",
|
||||
path: "/development/scheduled-jobs/create",
|
||||
},
|
||||
{
|
||||
title: "Customize Admin",
|
||||
path: "/admin/widgets",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
As you add subscription features to your store, you may need to
|
||||
customize the admin to provide an interface to manage these
|
||||
features.
|
||||
<br />
|
||||
You can extend the admin plugin to add widgets,{" "}
|
||||
<Link href="/admin/routes">UI routes</Link>, or{" "}
|
||||
<Link href="/admin/setting-pages">setting pages</Link>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create a storefront",
|
||||
path: "/starters/nextjs-medusa-starter",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Build a storefront either using the Next.js Starter Template or{" "}
|
||||
<Link href="/storefront/roadmap">from scratch</Link>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Deploy the backend",
|
||||
path: "/deployments/server/deploying-on-railway",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Deploy your backend to Railway. You can alternatively check out{" "}
|
||||
<Link href="/deployments/server">other deployment guides</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on implementing subscription-based purchases!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_subscriptions",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "b2b",
|
||||
label: "Build a B2B store",
|
||||
description:
|
||||
"Utilize Medusa's features and customization capabilities to build a B2B store.",
|
||||
steps: [
|
||||
{
|
||||
title: "Create a B2B Sales Channel",
|
||||
path: "/user-guide/sales-channels/manage",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
You can create a B2B sales channel that will include only your
|
||||
wholesale products.
|
||||
<br />
|
||||
You can either use the Medusa admin, or the{" "}
|
||||
<Link href="/modules/sales-channels/admin/manage">
|
||||
Admin REST APIs
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create a Publishable API Key",
|
||||
path: "/user-guide/settings/publishable-api-keys",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Publishable API keys can be associated with one or more sales
|
||||
channels. You can then use the publishable API key in your
|
||||
storefront or client.
|
||||
<br />
|
||||
You can either use the Medusa admin, or the{" "}
|
||||
<Link href="/development/publishable-api-keys/admin/manage-publishable-api-keys">
|
||||
Admin REST APIs
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Add Wholesale Products",
|
||||
path: "/user-guide/products/manage",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
You can add your wholesale products and make them only available in
|
||||
the B2B sales channel.
|
||||
<br />
|
||||
You can use the Medusa admin to add the products. Other alternatives
|
||||
are:
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/modules/products/admin/manage-products">
|
||||
Add Products Using REST APIs
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/user-guide/products/import">
|
||||
Import Products Using Medusa Admin
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/modules/products/admin/import-products">
|
||||
Import Products Using REST APIs
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create a B2B Customer Group",
|
||||
path: "/user-guide/customers/groups",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Customer groups can be used to apply different prices for different
|
||||
subsets of customers, in this case B2B customers.
|
||||
<br />
|
||||
You can either use the Medusa admin, or the{" "}
|
||||
<Link href="/modules/customers/admin/manage-customer-groups">
|
||||
Admin REST APIs
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Add B2B Customers",
|
||||
path: "/user-guide/customers/manage",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
You can now add B2B customers and assign them to the B2B customer
|
||||
group. Alternatively, if you want to allow B2B customers to register
|
||||
themselves, you can implement that logic within your storefront.
|
||||
<br />
|
||||
You can either use the Medusa admin, or the{" "}
|
||||
<Link href="/modules/customers/admin/manage-customers">
|
||||
Admin REST APIs
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create B2B Price Lists",
|
||||
path: "/user-guide/price-lists/manage",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
A price list allows you to set different prices on a set of products
|
||||
for different conditions. You can use this when building a B2B store
|
||||
to assign different prices for B2B customer groups.
|
||||
<br />
|
||||
You can use the Medusa admin to add the price list. Other
|
||||
alternatives are:
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/modules/price-lists/admin/manage-price-lists">
|
||||
Add Price List Using REST APIs
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/user-guide/price-lists/import">
|
||||
Import Prices Using Medusa Admin
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/modules/price-lists/admin/import-prices">
|
||||
Import Prices Using REST APIs
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create Custom Entities",
|
||||
path: "/development/entities/create",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Your use case may be more elaborate than what is shown in this
|
||||
recipe.
|
||||
<br />
|
||||
Medusa can be customized to add custom entities, API Routes,
|
||||
services, and more.
|
||||
<br />
|
||||
You can find additional development resources in the{" "}
|
||||
<Link href="/development/overview">Medusa development section</Link>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create an API Route to Check Customers",
|
||||
path: "/development/entities/create",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
On the clients communicating with your store, such as the
|
||||
storefront, you’ll need to check if the currently logged-in customer
|
||||
is a normal customer or a B2B customer.
|
||||
<br />
|
||||
To do that, you need to create a custom API Route that handles the
|
||||
checking based on the custom logic you've chosen to indicate a
|
||||
customer is a B2B customer.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Customize Admin",
|
||||
path: "/admin/widgets",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
As you add B2B features to your store, you may need to customize the
|
||||
admin to provide an interface to manage these features.
|
||||
<br />
|
||||
You can extend the admin plugin to add widgets,{" "}
|
||||
<Link href="/admin/routes">UI routes</Link>, or{" "}
|
||||
<Link href="/admin/setting-pages">setting pages</Link>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Customize Storefront",
|
||||
path: "/starters/nextjs-medusa-starter",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
You may need to customize your storefront to add different
|
||||
interfaces for B2B and regular customers, or show products
|
||||
differently.
|
||||
<br />
|
||||
You can customize the Next.js storefront, or you can{" "}
|
||||
<Link href="/storefront/roadmap">build a custom storefront</Link>.
|
||||
<br />
|
||||
In your storefront, make sure to{" "}
|
||||
<Link href="/development/publishable-api-keys/storefront/use-in-requests">
|
||||
use publishable API keys
|
||||
</Link>{" "}
|
||||
in your requests.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Deploy the B2B store",
|
||||
path: "/deployments/server",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Once you finish your development, you can deploy your B2B backend to
|
||||
your preferred hosting provider. You can also{" "}
|
||||
<Link href="/deployments/storefront">deploy your storefront</Link>{" "}
|
||||
to your preferred hosting provider.
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on building a B2B store!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_b2b",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "integrate-ecommerce-stack",
|
||||
label: "Integrate Ecommerce Stack",
|
||||
description:
|
||||
"Use Medusa’s architecture and functionalities to integrate third-party systems and build flows around them.",
|
||||
steps: [
|
||||
{
|
||||
title: "Connect to External Systems with Services",
|
||||
path: "/development/services/create-service",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Medusa’s Services let you implement a client that connects and
|
||||
performs functionalities with your third-party system.
|
||||
<br />
|
||||
<br />
|
||||
You can then use the service to connect to your third-party system
|
||||
in other resources, such as a Workflow or an API Route.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Build Flows Across Systems",
|
||||
path: "/development/workflows",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
With Medusa’s workflows, you can build flows with steps that may
|
||||
perform actions on different systems. Workflows can be executed from
|
||||
anywhere.
|
||||
<br />
|
||||
<br />
|
||||
For example, you can create a workflow that updates the product’s
|
||||
details in integrated systems like ERPs, WMSs, and CMSs. Then, you
|
||||
can listen to the
|
||||
<code>product.updated</code> event using a{" "}
|
||||
<Link href="/development/events/create-subscriber">Subscriber</Link>{" "}
|
||||
and execute the workflow whenever the event is triggered.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create Webhook Listeners",
|
||||
path: "/development/api-routes/create",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
You can provide webhook listeners that your external systems call
|
||||
when their data is updated. This lets you synchronize data between
|
||||
your systems.
|
||||
<br />
|
||||
<br />
|
||||
Webhook listeners can be created in Medusa using API Routes.
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on integrating your ecommerce stack!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_integrate-ecommerce-stack",
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: Eventually remove these learning paths
|
||||
{
|
||||
name: "rbac",
|
||||
label: "Role-based access control (RBAC)",
|
||||
description: "Implement roles and permissions for admin users in Medusa",
|
||||
steps: [
|
||||
{
|
||||
title: "Create Role and Permission Entities",
|
||||
path: "/development/entities/create",
|
||||
description:
|
||||
"When implementing RBAC, you typically require the availability of roles and permissions, both of which would require new entities. A role would include different permissions, such as the ability to access the products’ route, and it can be assigned to one or more users.",
|
||||
},
|
||||
{
|
||||
title: "Extend Entities",
|
||||
path: "/development/entities/extend-entity",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
To associate roles with users, you need to extend the{" "}
|
||||
<code>User</code> entity to add the relation between it and the new{" "}
|
||||
<code>Role</code> entity. You can also extend other entities that
|
||||
are associated with your custom one, such as the <code>Store</code>{" "}
|
||||
entity.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create Guard Middleware",
|
||||
path: "/development/api-routes/add-middleware",
|
||||
description:
|
||||
"To ensure that users who have the privilege can access an API Route, you must create a middleware that guards admin routes. This middleware will run on all authenticated admin requests to ensure that only allowed users can access an API Route.",
|
||||
},
|
||||
{
|
||||
title: "Create Services",
|
||||
path: "/development/services/create-service",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
For every entity you create, such as the <code>Role</code> and{" "}
|
||||
<code>Permission</code> entities, you must create a service that
|
||||
provides create, read, update, and delete (CRUD) functionalities at
|
||||
the very least.
|
||||
<br />
|
||||
If you also extended entities, such as the <code>User</code> entity,
|
||||
you may need to{" "}
|
||||
<Link href="/development/services/extend-service">
|
||||
extend its core service
|
||||
</Link>{" "}
|
||||
<code>UserService</code> as well to perform custom functionalities
|
||||
related to your implementation.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Create API Routes",
|
||||
path: "/development/api-routes/create",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
To manage the roles and permissions, you’ll need to create custom
|
||||
API Routes, typically for Create, Read, Update, and Delete (CRUD)
|
||||
operations.
|
||||
<br />
|
||||
After creating the API Routes, you may test adding roles and
|
||||
permissions, and how they provide different access for different
|
||||
roles and users.
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on implementing RBAC!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_rbac",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "entity-and-api",
|
||||
label: "Create Entity and Expose it with API Routes",
|
||||
description:
|
||||
"Learn how to create a new table in your database, then create API Routes to expose and manipulate its data.",
|
||||
steps: [
|
||||
{
|
||||
title: "Create entity",
|
||||
path: "/development/entities/create",
|
||||
description: "Create your entity, its migration, and repository.",
|
||||
},
|
||||
{
|
||||
title: "Create service",
|
||||
path: "/development/services/create-service",
|
||||
description:
|
||||
"A service is a class that defines helper methods for your entity. The service will be used by the API Routes to access or modify the entity's data.",
|
||||
},
|
||||
{
|
||||
title: "Create API Routes",
|
||||
path: "/development/api-routes/create",
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on creating your entity and API Routes!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_entity-and-api",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "storefront",
|
||||
label: "Create a Custom Storefront",
|
||||
description:
|
||||
"Learn how to create a custom storefront with your preferred language or framework.",
|
||||
steps: [
|
||||
{
|
||||
title: "Choose your client",
|
||||
path: "/medusa-react/overview",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
As your storefront connect to the Medusa backend, you need a way to
|
||||
interact with the backend's REST APIs. There are three ways to
|
||||
do so, based on your type of project:
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/medusa-react/overview">Medusa React</Link>: Can be
|
||||
used in any React-based project. For example, in a Next.js
|
||||
storefront.
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/js-client/overview">Medusa JS Client</Link>: Can be
|
||||
used in any JavaScript and NPM based project. For example, in a
|
||||
Nuxt storefront.
|
||||
</li>
|
||||
<li>
|
||||
<Link href={`https://docs.medusajs.com/api/store`}>
|
||||
Store REST APIs
|
||||
</Link>
|
||||
: You can send requests directly to the API Routes without using
|
||||
Medusa's clients.
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Set CORS configurations in Backend",
|
||||
path: "/development/backend/configurations#admin_cors-and-store_cors",
|
||||
description:
|
||||
"To ensure your storefront can connect to the backend, make sure to configure the backend's CORS configuration based on your storefront's local or remote URL.",
|
||||
},
|
||||
{
|
||||
title: "Create a Publishable API Key",
|
||||
path: "/user-guide/settings/publishable-api-keys",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
A publishable API key allows you to associate a key with a sales
|
||||
channel. Then, you can include that key in the headers of all your
|
||||
requests.
|
||||
<br />
|
||||
You can create the publishable API key from the dashboard.
|
||||
Alternatively, you can create it using the{" "}
|
||||
<Link href="/development/publishable-api-keys/admin/manage-publishable-api-keys">
|
||||
Admin APIs
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Use Publishable API Key",
|
||||
path: "/development/publishable-api-keys/storefront/use-in-requests",
|
||||
description:
|
||||
"After creating the publishable API key and associating it with sales channels, you can pass it in the header of your requests to Store API Routes.",
|
||||
},
|
||||
{
|
||||
title: "Add Region Selection",
|
||||
path: "/modules/regions-and-currencies/storefront/use-regions",
|
||||
description:
|
||||
"In your storefront, you can allow your customers to view available regions and select their current region. This can affect the prices, discounts, and shipping and payment providers available to the customer.",
|
||||
},
|
||||
{
|
||||
title: "Display Products",
|
||||
path: "/modules/products/storefront/show-products",
|
||||
description: "Display products to your customers in the storefront.",
|
||||
},
|
||||
{
|
||||
title: "Implement Cart Functionalities",
|
||||
path: "/modules/carts-and-checkout/storefront/implement-cart",
|
||||
description:
|
||||
"Allow your customers to add items to their cart, update them, and more in preparation for checkout.",
|
||||
},
|
||||
{
|
||||
title: "Implement Checkout Flow",
|
||||
path: "/modules/carts-and-checkout/storefront/implement-checkout-flow",
|
||||
description:
|
||||
"Implement the checkout flow that allows customers to handle shipping and payment, then place their orders.",
|
||||
},
|
||||
{
|
||||
title: "Implement Customer Profiles",
|
||||
path: "/modules/customers/storefront/implement-customer-profiles",
|
||||
description:
|
||||
"Allow customers to register, login, edit their profile information, and more.",
|
||||
},
|
||||
{
|
||||
title: "More Commerce Functionalities",
|
||||
path: "/modules/overview",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
This recipe guided you to create a storefront with basic
|
||||
functionalities. You can add more functionalities to your storefront
|
||||
based on your use case.
|
||||
<ul>
|
||||
<li>
|
||||
The <Link href="/modules/overview">Commerce Modules</Link>{" "}
|
||||
documentation holds various storefront-related how-to guides to
|
||||
help you implement different features.
|
||||
</li>
|
||||
<li>
|
||||
You can also checkout the{" "}
|
||||
<Link href={`https://docs.medusajs.com/api/store`}>
|
||||
Store REST APIs
|
||||
</Link>{" "}
|
||||
for a full list of available REST APIs.
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on creating your storefront!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_storefront",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plugin",
|
||||
label: "Create a Plugin",
|
||||
description:
|
||||
"Learn how to create a plugin that can be re-used across Medusa backends.",
|
||||
steps: [
|
||||
{
|
||||
title: "Setup plugin project",
|
||||
path: "/development/backend/install",
|
||||
description:
|
||||
"A plugin is initially a Medusa backend with customizations. If you don't have a project ready, you can create one using Medusa's CLI tool.",
|
||||
},
|
||||
{
|
||||
title: "Implement Customizations",
|
||||
path: "/development/entities/create",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Your plugin can hold backend and admin customizations. Those
|
||||
include:
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/development/entities/create">Create Entity</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/development/services/create-service">
|
||||
Create Service
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/development/api-routes/create">
|
||||
Create an API Route
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/development/events/create-subscriber">
|
||||
Create Subscriber
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/admin/widgets">Create Admin Widgets</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/admin/routes">Create Admin Routes</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/admin/setting-pages">
|
||||
Create Admin Setting Pages
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/development/search/create">
|
||||
Create Search Service
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/development/file-service/create-file-service">
|
||||
Create File Service
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/development/notification/create-notification-provider">
|
||||
Create Notification Service
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
If you've already made your custom development, you can skip to
|
||||
the next step.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Change your package.json",
|
||||
path: "/development/plugins/create#changes-to-packagejson",
|
||||
descriptionJSX: (
|
||||
<>
|
||||
Once you're done making your customizations and you're
|
||||
ready to publish your plugin, make changes to your{" "}
|
||||
<code>package.json</code> in preparation for publishing.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Optionally test locally",
|
||||
path: "/development/plugins/create#test-your-plugin",
|
||||
description:
|
||||
"If necessary, you can test your plugin in a separate local Medusa backend. It's recommended, however, to do your plugin testing within the plugin project.",
|
||||
},
|
||||
{
|
||||
title: "Publish plugin",
|
||||
path: "/development/plugins/publish",
|
||||
description: "Publish your plugin on NPM.",
|
||||
},
|
||||
],
|
||||
finish: {
|
||||
type: "rating",
|
||||
step: {
|
||||
title: "Congratulations on creating your plugin!",
|
||||
description: "Please rate your experience using this recipe.",
|
||||
eventName: "rating_path_plugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// get a shallow copy
|
||||
export const getLearningPaths = () => [...paths]
|
||||
|
||||
export const getLearningPath = (
|
||||
pathName: string
|
||||
): LearningPathType | undefined => paths.find((path) => path.name === pathName)
|
||||
@@ -0,0 +1,16 @@
|
||||
import { RawSidebarItemType } from "types"
|
||||
|
||||
const commonOptions: Partial<RawSidebarItemType> = {
|
||||
loaded: true,
|
||||
isPathHref: true,
|
||||
}
|
||||
|
||||
export function sidebarAttachHrefCommonOptions(
|
||||
sidebar: RawSidebarItemType[]
|
||||
): RawSidebarItemType[] {
|
||||
return sidebar.map((item) => ({
|
||||
...commonOptions,
|
||||
...item,
|
||||
children: sidebarAttachHrefCommonOptions(item.children || []),
|
||||
}))
|
||||
}
|
||||
Reference in new issue
Block a user