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:
Shahed Nasser authored and GitHub committed 2024-03-18 07:47:35 +00:00
1 parent 56a6ec0227
commit bb87db8342
2008 files changed
+15716 -10536

No files matched your search

+6 -1
View File
@@ -39,7 +39,7 @@
"clsx": "^2.0.0",
"cpy-cli": "^5.0.0",
"eslint-config-docs": "*",
"next": "latest",
"next": "14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^5.0.1",
@@ -47,6 +47,7 @@
"tailwindcss": "^3.3.3",
"tsc-alias": "^1.8.7",
"tsup": "^5.10.1",
"types": "*",
"typescript": "^5.1.6"
},
"peerDependencies": {
@@ -62,11 +63,15 @@
"@octokit/request": "^8.1.1",
"@react-hook/resize-observer": "^1.2.6",
"@segment/analytics-next": "^1.55.0",
"@uidotdev/usehooks": "^2.4.1",
"algoliasearch": "^4.20.0",
"mermaid": "^10.9.0",
"npm-to-yarn": "^2.1.0",
"prism-react-renderer": "2.3.1",
"react-google-recaptcha": "^3.1.0",
"react-instantsearch": "^7.0.3",
"react-markdown": "^8.0.7",
"react-medium-image-zoom": "^5.1.10",
"react-tooltip": "^5.21.3",
"react-transition-group": "^4.4.5",
"react-uuid": "^2.0.0"
@@ -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>
)
}
@@ -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>
)
}
+21 -1
View File
@@ -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"
+102
View File
@@ -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}
+1 -1
View File
@@ -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"
+51
View File
@@ -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>
)
}
+22
View File
@@ -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>
)
}
View File
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"
-3
View File
@@ -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("&#60;", "<")
.replaceAll("&#123;", "{")
.replaceAll("&#125;", "}")
.replaceAll("&#62;", ">")
.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"
}
+7
View File
@@ -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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;re done making your customizations and you&apos;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 || []),
}))
}
+1 -5
View File
@@ -11,15 +11,11 @@
"tsBuildInfoFile": "./dist/.tsbuildinfo-client",
"noEmit": false,
"jsx": "react",
"lib": ["dom", "ES2015"],
"lib": ["dom", "ES2015", "es2021"],
"module": "ESNext",
"target": "es6",
"declaration": true,
"declarationMap": true,
"typeRoots": [
"./node_modules/@types",
"./types"
]
},
"include": [
"src/**/*",