docs: added callouts in search and AI assistant (#14421)
* docs: added callouts in search and AI assistant * added more suggestions * fix search tests * add mock for links
This commit is contained in:
1 parent
1ed6e6b308
commit
499dec6d31
23 files changed
+674
-7
No files matched your search
+119
@@ -0,0 +1,119 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||||
|
import { cleanup, fireEvent, render } from "@testing-library/react"
|
||||||
|
import * as AiAssistantMocks from "../../../__mocks__"
|
||||||
|
|
||||||
|
// mock functions
|
||||||
|
const mockUseMedusaSuggestions = vi.fn((options) => null as unknown)
|
||||||
|
const mockTrack = vi.fn()
|
||||||
|
|
||||||
|
// mock components and hooks
|
||||||
|
vi.mock("@/providers/AiAssistant", () => ({
|
||||||
|
useAiAssistant: () => AiAssistantMocks.mockUseAiAssistant(),
|
||||||
|
}))
|
||||||
|
vi.mock("@kapaai/react-sdk", () => ({
|
||||||
|
useChat: () => AiAssistantMocks.mockUseChat(),
|
||||||
|
}))
|
||||||
|
vi.mock("@/hooks/use-medusa-suggestions", () => ({
|
||||||
|
useMedusaSuggestions: (options: unknown) => mockUseMedusaSuggestions(options),
|
||||||
|
}))
|
||||||
|
vi.mock("@/components/Card", () => ({
|
||||||
|
Card: (props: { title: string, onClick: () => void }) => (
|
||||||
|
<div data-testid="card" onClick={props.onClick}>{props.title}</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
vi.mock("@/providers/Analytics", () => ({
|
||||||
|
useAnalytics: () => ({
|
||||||
|
track: mockTrack,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { AiAssistantChatWindowCallout } from "../index"
|
||||||
|
import { DocsTrackingEvents } from "../../../../../constants"
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("render", () => {
|
||||||
|
test("should not render when there is no matched suggestion", () => {
|
||||||
|
mockUseMedusaSuggestions.mockReturnValueOnce(null)
|
||||||
|
|
||||||
|
const { container } = render(<AiAssistantChatWindowCallout />)
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should render when there is a matched suggestion", () => {
|
||||||
|
const mockCardProps = {
|
||||||
|
title: "Test Card",
|
||||||
|
text: "This is a test card.",
|
||||||
|
href: "https://example.com",
|
||||||
|
icon: () => <div>Icon</div>,
|
||||||
|
}
|
||||||
|
mockUseMedusaSuggestions.mockReturnValueOnce(mockCardProps)
|
||||||
|
|
||||||
|
const { getByTestId } = render(<AiAssistantChatWindowCallout />)
|
||||||
|
|
||||||
|
expect(getByTestId("card")).toBeInTheDocument()
|
||||||
|
expect(getByTestId("card")).toHaveTextContent("Test Card")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should not render when loading is true", () => {
|
||||||
|
AiAssistantMocks.mockUseAiAssistant.mockReturnValueOnce({
|
||||||
|
...AiAssistantMocks.defaultUseAiAssistantReturn,
|
||||||
|
loading: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
mockUseMedusaSuggestions.mockReturnValueOnce({
|
||||||
|
title: "Test Card",
|
||||||
|
text: "This is a test card.",
|
||||||
|
href: "https://example.com",
|
||||||
|
icon: () => <div>Icon</div>,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { container } = render(<AiAssistantChatWindowCallout />)
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should pass correct keywords to useMedusaSuggestions", () => {
|
||||||
|
render(<AiAssistantChatWindowCallout />)
|
||||||
|
|
||||||
|
expect(mockUseMedusaSuggestions).toHaveBeenCalledWith({
|
||||||
|
keywords: AiAssistantMocks.mockConversation.getLatestCompleted()?.question || "",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("interactions", () => {
|
||||||
|
test("should track event on card click", () => {
|
||||||
|
const mockCardProps = {
|
||||||
|
title: "Test Card",
|
||||||
|
text: "This is a test card.",
|
||||||
|
href: "https://example.com",
|
||||||
|
icon: () => <div>Icon</div>,
|
||||||
|
}
|
||||||
|
mockUseMedusaSuggestions.mockReturnValueOnce(mockCardProps)
|
||||||
|
|
||||||
|
const { getByTestId } = render(<AiAssistantChatWindowCallout />)
|
||||||
|
|
||||||
|
const cardElement = getByTestId("card")
|
||||||
|
expect(cardElement).toBeInTheDocument()
|
||||||
|
|
||||||
|
// Simulate click
|
||||||
|
fireEvent.click(cardElement!)
|
||||||
|
|
||||||
|
expect(mockTrack).toHaveBeenCalledWith({
|
||||||
|
event: {
|
||||||
|
event: DocsTrackingEvents.AI_ASSISTANT_CALLOUT_CLICK,
|
||||||
|
options: {
|
||||||
|
user_keywords: AiAssistantMocks.mockConversation.getLatestCompleted()?.question || "",
|
||||||
|
callout_title: mockCardProps.title,
|
||||||
|
callout_href: mockCardProps.href,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React from "react"
|
||||||
|
import { Card } from "../../../Card"
|
||||||
|
import { useChat } from "@kapaai/react-sdk"
|
||||||
|
import { useAiAssistant } from "../../../../providers/AiAssistant"
|
||||||
|
import { useMedusaSuggestions } from "../../../../hooks/use-medusa-suggestions"
|
||||||
|
import { useAnalytics } from "../../../../providers/Analytics"
|
||||||
|
import { DocsTrackingEvents } from "../../../../constants"
|
||||||
|
|
||||||
|
export const AiAssistantChatWindowCallout = () => {
|
||||||
|
const { conversation } = useChat()
|
||||||
|
const { loading } = useAiAssistant()
|
||||||
|
const { track } = useAnalytics()
|
||||||
|
|
||||||
|
const lastQuestion = conversation.getLatestCompleted()?.question
|
||||||
|
|
||||||
|
const matchedCallout = useMedusaSuggestions({
|
||||||
|
keywords: lastQuestion || "",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loading || !matchedCallout) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="m-docs_1 flex justify-center items-center">
|
||||||
|
<Card
|
||||||
|
{...matchedCallout}
|
||||||
|
onClick={() => {
|
||||||
|
track({
|
||||||
|
event: {
|
||||||
|
event: DocsTrackingEvents.AI_ASSISTANT_CALLOUT_CLICK,
|
||||||
|
options: {
|
||||||
|
user_keywords: lastQuestion || "",
|
||||||
|
callout_title: matchedCallout.title || "",
|
||||||
|
callout_href: matchedCallout.href || "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -52,6 +52,9 @@ vi.mock("@/components/AiAssistant/ThreadItem", () => ({
|
|||||||
<div className="thread-item">ThreadItem - type: {item.type}</div>
|
<div className="thread-item">ThreadItem - type: {item.type}</div>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
vi.mock("@/components/AiAssistant/ChatWindow/Callout", () => ({
|
||||||
|
AiAssistantChatWindowCallout: () => <div data-testid="callout">Callout</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
import { AiAssistantChatWindow } from "../../ChatWindow"
|
import { AiAssistantChatWindow } from "../../ChatWindow"
|
||||||
|
|
||||||
@@ -81,6 +84,7 @@ describe("rendering", () => {
|
|||||||
expect(container).toHaveTextContent("Header")
|
expect(container).toHaveTextContent("Header")
|
||||||
expect(container).toHaveTextContent("Input")
|
expect(container).toHaveTextContent("Input")
|
||||||
expect(container).toHaveTextContent("Footer")
|
expect(container).toHaveTextContent("Footer")
|
||||||
|
expect(container).toHaveTextContent("Callout")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("chat is hidden when chatOpened is false", () => {
|
test("chat is hidden when chatOpened is false", () => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { AiAssistantChatWindowInput } from "./Input"
|
|||||||
import { useKeyboardShortcut } from "../../../hooks/use-keyboard-shortcut"
|
import { useKeyboardShortcut } from "../../../hooks/use-keyboard-shortcut"
|
||||||
import { AiAssistantChatWindowFooter } from "./Footer"
|
import { AiAssistantChatWindowFooter } from "./Footer"
|
||||||
import { useChat } from "@kapaai/react-sdk"
|
import { useChat } from "@kapaai/react-sdk"
|
||||||
|
import { AiAssistantChatWindowCallout } from "./Callout"
|
||||||
|
|
||||||
const DEFAULT_HEIGHT = "calc(100% - 8px)"
|
const DEFAULT_HEIGHT = "calc(100% - 8px)"
|
||||||
|
|
||||||
@@ -204,6 +205,7 @@ export const AiAssistantChatWindow = () => {
|
|||||||
)}
|
)}
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
|
<AiAssistantChatWindowCallout />
|
||||||
<AiAssistantChatWindowInput chatWindowRef={chatWindowRef} />
|
<AiAssistantChatWindowInput chatWindowRef={chatWindowRef} />
|
||||||
<AiAssistantChatWindowFooter />
|
<AiAssistantChatWindowFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const createMockConversation = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getLatest: mockGetLatest,
|
getLatest: mockGetLatest,
|
||||||
|
getLatestCompleted: mockGetLatest,
|
||||||
map(callback: (item: unknown, index: number) => React.ReactNode) {
|
map(callback: (item: unknown, index: number) => React.ReactNode) {
|
||||||
return conversationItems.map(callback)
|
return conversationItems.map(callback)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { describe, expect, test, vi } from "vitest"
|
import { describe, expect, test, vi } from "vitest"
|
||||||
import { render } from "@testing-library/react"
|
import { fireEvent, render } from "@testing-library/react"
|
||||||
import { CardDefaultLayout } from "../index"
|
import { CardDefaultLayout } from "../index"
|
||||||
import { IconProps } from "@medusajs/icons/dist/types"
|
import { IconProps } from "@medusajs/icons/dist/types"
|
||||||
import { LinkProps } from "../../../../Link"
|
import { LinkProps } from "../../../../Link"
|
||||||
@@ -108,6 +108,8 @@ describe("rendering", () => {
|
|||||||
const linkElement = container.querySelector("a")
|
const linkElement = container.querySelector("a")
|
||||||
expect(linkElement).toBeInTheDocument()
|
expect(linkElement).toBeInTheDocument()
|
||||||
expect(linkElement).toHaveAttribute("href", href)
|
expect(linkElement).toHaveAttribute("href", href)
|
||||||
|
expect(linkElement).toHaveAttribute("target", "_blank")
|
||||||
|
expect(linkElement).toHaveAttribute("rel", "noopener noreferrer")
|
||||||
const arrowUpRightOnBoxElement = container.querySelector(
|
const arrowUpRightOnBoxElement = container.querySelector(
|
||||||
"[data-testid='external-icon']"
|
"[data-testid='external-icon']"
|
||||||
)
|
)
|
||||||
@@ -126,6 +128,8 @@ describe("rendering", () => {
|
|||||||
const linkElement = container.querySelector("a")
|
const linkElement = container.querySelector("a")
|
||||||
expect(linkElement).toBeInTheDocument()
|
expect(linkElement).toBeInTheDocument()
|
||||||
expect(linkElement).toHaveAttribute("href", href)
|
expect(linkElement).toHaveAttribute("href", href)
|
||||||
|
expect(linkElement).not.toHaveAttribute("target")
|
||||||
|
expect(linkElement).not.toHaveAttribute("rel")
|
||||||
const internalIconElement = container.querySelector(
|
const internalIconElement = container.querySelector(
|
||||||
"[data-testid='internal-icon']"
|
"[data-testid='internal-icon']"
|
||||||
)
|
)
|
||||||
@@ -213,3 +217,17 @@ describe("highlight text", () => {
|
|||||||
expect(highlightTextElement).not.toBeInTheDocument()
|
expect(highlightTextElement).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("interaction", () => {
|
||||||
|
test("onClick is called when card's link is clicked", () => {
|
||||||
|
const handleClick = vi.fn()
|
||||||
|
const { container } = render(
|
||||||
|
<CardDefaultLayout onClick={handleClick} href="#">Click me</CardDefaultLayout>
|
||||||
|
)
|
||||||
|
expect(container).toBeInTheDocument()
|
||||||
|
const link = container.querySelector("a")
|
||||||
|
expect(link).toBeInTheDocument()
|
||||||
|
fireEvent.click(link!)
|
||||||
|
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -20,6 +20,7 @@ export const CardDefaultLayout = ({
|
|||||||
badge,
|
badge,
|
||||||
rightIcon: RightIconComponent,
|
rightIcon: RightIconComponent,
|
||||||
highlightText = [],
|
highlightText = [],
|
||||||
|
onClick,
|
||||||
}: CardProps) => {
|
}: CardProps) => {
|
||||||
const isExternal = useIsExternalLink({ href })
|
const isExternal = useIsExternalLink({ href })
|
||||||
|
|
||||||
@@ -113,6 +114,10 @@ export const CardDefaultLayout = ({
|
|||||||
href={href}
|
href={href}
|
||||||
className="absolute left-0 top-0 h-full w-full rounded"
|
className="absolute left-0 top-0 h-full w-full rounded"
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
|
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||||
|
target={isExternal ? "_blank" : undefined}
|
||||||
|
aria-label={title}
|
||||||
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,44 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { describe, expect, test, vi } from "vitest"
|
import { describe, expect, test, vi } from "vitest"
|
||||||
import { render } from "@testing-library/react"
|
import { fireEvent, render } from "@testing-library/react"
|
||||||
import { CardLargeLayout } from "../index"
|
import { CardLargeLayout } from "../index"
|
||||||
|
|
||||||
|
// mock components
|
||||||
|
vi.mock("next/link", () => ({
|
||||||
|
default: ({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
target,
|
||||||
|
rel,
|
||||||
|
onClick,
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
}: {
|
||||||
|
href: string
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
target?: string
|
||||||
|
rel?: string
|
||||||
|
onClick?: () => void
|
||||||
|
"aria-label"?: string
|
||||||
|
}) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className={className}
|
||||||
|
target={target}
|
||||||
|
rel={rel}
|
||||||
|
onClick={(e) => {
|
||||||
|
// can't perform actual navigation in tests
|
||||||
|
e.preventDefault()
|
||||||
|
onClick?.()
|
||||||
|
}}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
describe("rendering", () => {
|
describe("rendering", () => {
|
||||||
test("renders card large layout with title", () => {
|
test("renders card large layout with title", () => {
|
||||||
const title = "Title"
|
const title = "Title"
|
||||||
@@ -33,6 +69,8 @@ describe("rendering", () => {
|
|||||||
const linkElement = container.querySelector("a")
|
const linkElement = container.querySelector("a")
|
||||||
expect(linkElement).toBeInTheDocument()
|
expect(linkElement).toBeInTheDocument()
|
||||||
expect(linkElement).toHaveAttribute("href", href)
|
expect(linkElement).toHaveAttribute("href", href)
|
||||||
|
expect(linkElement).toHaveAttribute("rel", "noopener noreferrer")
|
||||||
|
expect(linkElement).toHaveAttribute("target", "_blank")
|
||||||
const arrowUpRightOnBoxElement = container.querySelector(
|
const arrowUpRightOnBoxElement = container.querySelector(
|
||||||
"[data-testid='external-icon']"
|
"[data-testid='external-icon']"
|
||||||
)
|
)
|
||||||
@@ -51,6 +89,8 @@ describe("rendering", () => {
|
|||||||
const linkElement = container.querySelector("a")
|
const linkElement = container.querySelector("a")
|
||||||
expect(linkElement).toBeInTheDocument()
|
expect(linkElement).toBeInTheDocument()
|
||||||
expect(linkElement).toHaveAttribute("href", href)
|
expect(linkElement).toHaveAttribute("href", href)
|
||||||
|
expect(linkElement).not.toHaveAttribute("target")
|
||||||
|
expect(linkElement).not.toHaveAttribute("rel")
|
||||||
const internalIconElement = container.querySelector(
|
const internalIconElement = container.querySelector(
|
||||||
"[data-testid='internal-icon']"
|
"[data-testid='internal-icon']"
|
||||||
)
|
)
|
||||||
@@ -79,3 +119,17 @@ describe("rendering", () => {
|
|||||||
expect(imageElement).toBeInTheDocument()
|
expect(imageElement).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("interaction", () => {
|
||||||
|
test("calls onClick when card with link is clicked", () => {
|
||||||
|
const handleClick = vi.fn()
|
||||||
|
const { container } = render(
|
||||||
|
<CardLargeLayout onClick={handleClick} href="#">Click me</CardLargeLayout>
|
||||||
|
)
|
||||||
|
expect(container).toBeInTheDocument()
|
||||||
|
const linkElement = container.querySelector("a")
|
||||||
|
expect(linkElement).toBeInTheDocument()
|
||||||
|
fireEvent.click(linkElement!)
|
||||||
|
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -12,6 +12,7 @@ export const CardLargeLayout = ({
|
|||||||
icon,
|
icon,
|
||||||
href,
|
href,
|
||||||
className,
|
className,
|
||||||
|
onClick,
|
||||||
}: CardProps) => {
|
}: CardProps) => {
|
||||||
const isExternal = useIsExternalLink({ href })
|
const isExternal = useIsExternalLink({ href })
|
||||||
const IconComponent = icon
|
const IconComponent = icon
|
||||||
@@ -76,6 +77,10 @@ export const CardLargeLayout = ({
|
|||||||
href={href}
|
href={href}
|
||||||
className="absolute left-0 top-0 h-full w-full rounded"
|
className="absolute left-0 top-0 h-full w-full rounded"
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
|
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||||
|
target={isExternal ? "_blank" : undefined}
|
||||||
|
aria-label={title}
|
||||||
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { describe, expect, test, vi } from "vitest"
|
import { describe, expect, test, vi } from "vitest"
|
||||||
import { fireEvent, render } from "@testing-library/react"
|
import { fireEvent, render } from "@testing-library/react"
|
||||||
import { BorderedIconProps } from "../../../../BorderedIcon"
|
import { BorderedIconProps } from "../../../../BorderedIcon"
|
||||||
import { LinkProps } from "../../../../Link"
|
|
||||||
|
|
||||||
// mock data
|
// mock data
|
||||||
const exampleDataImageUrl =
|
const exampleDataImageUrl =
|
||||||
@@ -16,8 +15,39 @@ vi.mock("@/components/BorderedIcon", () => ({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
vi.mock("@/components/Link", () => ({
|
vi.mock("next/link", () => ({
|
||||||
Link: (props: LinkProps) => <a {...props} />,
|
default: ({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
target,
|
||||||
|
rel,
|
||||||
|
onClick,
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
}: {
|
||||||
|
href: string
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
target?: string
|
||||||
|
rel?: string
|
||||||
|
onClick?: () => void
|
||||||
|
"aria-label"?: string
|
||||||
|
}) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className={className}
|
||||||
|
target={target}
|
||||||
|
rel={rel}
|
||||||
|
onClick={(e) => {
|
||||||
|
// can't perform actual navigation in tests
|
||||||
|
e.preventDefault()
|
||||||
|
onClick?.()
|
||||||
|
}}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
vi.mock("@/components/ThemeImage", () => ({
|
vi.mock("@/components/ThemeImage", () => ({
|
||||||
ThemeImage: (props: ThemeImageProps) => (
|
ThemeImage: (props: ThemeImageProps) => (
|
||||||
@@ -26,7 +56,32 @@ vi.mock("@/components/ThemeImage", () => ({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
vi.mock("next/image", () => ({
|
||||||
|
default: ({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
src: string
|
||||||
|
alt?: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
className?: string
|
||||||
|
style?: React.CSSProperties
|
||||||
|
}) => (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt || ""}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}))
|
||||||
import { CardLayoutMini } from "../../Mini"
|
import { CardLayoutMini } from "../../Mini"
|
||||||
import { ThemeImageProps } from "../../../../ThemeImage"
|
import { ThemeImageProps } from "../../../../ThemeImage"
|
||||||
|
|
||||||
@@ -60,6 +115,8 @@ describe("rendering", () => {
|
|||||||
const linkElement = container.querySelector("a")
|
const linkElement = container.querySelector("a")
|
||||||
expect(linkElement).toBeInTheDocument()
|
expect(linkElement).toBeInTheDocument()
|
||||||
expect(linkElement).toHaveAttribute("href", href)
|
expect(linkElement).toHaveAttribute("href", href)
|
||||||
|
expect(linkElement).toHaveAttribute("rel", "noopener noreferrer")
|
||||||
|
expect(linkElement).toHaveAttribute("target", "_blank")
|
||||||
})
|
})
|
||||||
test("renders card mini layout with internal href", () => {
|
test("renders card mini layout with internal href", () => {
|
||||||
const href = "/example"
|
const href = "/example"
|
||||||
@@ -70,6 +127,8 @@ describe("rendering", () => {
|
|||||||
const linkElement = container.querySelector("a")
|
const linkElement = container.querySelector("a")
|
||||||
expect(linkElement).toBeInTheDocument()
|
expect(linkElement).toBeInTheDocument()
|
||||||
expect(linkElement).toHaveAttribute("href", href)
|
expect(linkElement).toHaveAttribute("href", href)
|
||||||
|
expect(linkElement).not.toHaveAttribute("target")
|
||||||
|
expect(linkElement).not.toHaveAttribute("rel")
|
||||||
})
|
})
|
||||||
test("renders card mini layout with icon", () => {
|
test("renders card mini layout with icon", () => {
|
||||||
const icon = () => <div data-testid="icon">Icon</div>
|
const icon = () => <div data-testid="icon">Icon</div>
|
||||||
@@ -202,3 +261,17 @@ describe("rendering", () => {
|
|||||||
expect(themeImageElement).toHaveClass(iconClassName)
|
expect(themeImageElement).toHaveClass(iconClassName)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("interaction", () => {
|
||||||
|
test("calls onClick when card mini layout with link is clicked", () => {
|
||||||
|
const onClick = vi.fn()
|
||||||
|
const { container } = render(
|
||||||
|
<CardLayoutMini onClick={onClick} href="#">Click me</CardLayoutMini>
|
||||||
|
)
|
||||||
|
expect(container).toBeInTheDocument()
|
||||||
|
const link = container.querySelector("a")
|
||||||
|
expect(link).toBeInTheDocument()
|
||||||
|
fireEvent.click(link!)
|
||||||
|
expect(onClick).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -25,6 +25,7 @@ export const CardLayoutMini = ({
|
|||||||
imageDimensions = { width: 45, height: 36 },
|
imageDimensions = { width: 45, height: 36 },
|
||||||
iconClassName,
|
iconClassName,
|
||||||
cardRef,
|
cardRef,
|
||||||
|
onClick,
|
||||||
}: CardProps) => {
|
}: CardProps) => {
|
||||||
const isExternal = useIsExternalLink({ href })
|
const isExternal = useIsExternalLink({ href })
|
||||||
|
|
||||||
@@ -120,6 +121,10 @@ export const CardLayoutMini = ({
|
|||||||
className="absolute left-0 top-0 w-full h-full z-[1]"
|
className="absolute left-0 top-0 w-full h-full z-[1]"
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
{...hrefProps}
|
{...hrefProps}
|
||||||
|
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||||
|
target={isExternal ? "_blank" : undefined}
|
||||||
|
aria-label={title}
|
||||||
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{closeable && (
|
{closeable && (
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type CardProps = {
|
|||||||
highlightText?: string[]
|
highlightText?: string[]
|
||||||
closeable?: boolean
|
closeable?: boolean
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
|
onClick?: () => void
|
||||||
hrefProps?: Partial<LinkProps & React.AllHTMLAttributes<HTMLAnchorElement>>
|
hrefProps?: Partial<LinkProps & React.AllHTMLAttributes<HTMLAnchorElement>>
|
||||||
cardRef?: React.Ref<HTMLDivElement>
|
cardRef?: React.Ref<HTMLDivElement>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Medusa } from "@medusajs/icons"
|
||||||
|
import { IconProps } from "@medusajs/icons/dist/types"
|
||||||
|
import clsx from "clsx"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export const ColoredMedusaIcon = ({ className, ...props }: IconProps) => {
|
||||||
|
return (
|
||||||
|
<Medusa
|
||||||
|
{...props}
|
||||||
|
className={clsx(className, "[&_path]:fill-medusa-fg-subtle")}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ export * from "./CalendarRefresh"
|
|||||||
export * from "./ChefHat"
|
export * from "./ChefHat"
|
||||||
export * from "./CircleDottedLine"
|
export * from "./CircleDottedLine"
|
||||||
export * from "./CloudSolid"
|
export * from "./CloudSolid"
|
||||||
|
export * from "./ColoredMedusa"
|
||||||
export * from "./DecisionProcess"
|
export * from "./DecisionProcess"
|
||||||
export * from "./Erp"
|
export * from "./Erp"
|
||||||
export * from "./ImageBinary"
|
export * from "./ImageBinary"
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||||
|
import { cleanup, fireEvent, render } from "@testing-library/react"
|
||||||
|
|
||||||
|
// mock functions
|
||||||
|
const mockUseMedusaSuggestions = vi.fn((options) => null as unknown)
|
||||||
|
const mockUseInstantSearch = vi.fn(() => ({
|
||||||
|
results: {
|
||||||
|
query: "test",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const mockTrack = vi.fn()
|
||||||
|
|
||||||
|
// mock components and hooks
|
||||||
|
vi.mock("@/hooks/use-medusa-suggestions", () => ({
|
||||||
|
useMedusaSuggestions: (options: unknown) => mockUseMedusaSuggestions(options),
|
||||||
|
}))
|
||||||
|
vi.mock("@/components/Card", () => ({
|
||||||
|
Card: (props: { title: string, onClick: () => void }) => (
|
||||||
|
<div data-testid="card" onClick={props.onClick}>{props.title}</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
vi.mock("react-instantsearch", () => ({
|
||||||
|
useInstantSearch: () => mockUseInstantSearch(),
|
||||||
|
}))
|
||||||
|
vi.mock("@/providers/Analytics", () => ({
|
||||||
|
useAnalytics: () => ({
|
||||||
|
track: mockTrack,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { SearchCallout } from "../index"
|
||||||
|
import { DocsTrackingEvents } from "../../../../constants"
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("render", () => {
|
||||||
|
test("should not render when there is no matched suggestion", () => {
|
||||||
|
mockUseMedusaSuggestions.mockReturnValueOnce(null)
|
||||||
|
|
||||||
|
const { container } = render(<SearchCallout />)
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should render when there is a matched suggestion", () => {
|
||||||
|
const mockCardProps = {
|
||||||
|
title: "Test Card",
|
||||||
|
text: "This is a test card.",
|
||||||
|
href: "https://example.com",
|
||||||
|
icon: () => <div>Icon</div>,
|
||||||
|
}
|
||||||
|
mockUseMedusaSuggestions.mockReturnValueOnce(mockCardProps)
|
||||||
|
|
||||||
|
const { getByTestId } = render(<SearchCallout />)
|
||||||
|
|
||||||
|
expect(getByTestId("card")).toBeInTheDocument()
|
||||||
|
expect(getByTestId("card")).toHaveTextContent("Test Card")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should not render when there is no query", () => {
|
||||||
|
mockUseInstantSearch.mockReturnValueOnce({
|
||||||
|
results: {
|
||||||
|
query: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
mockUseMedusaSuggestions.mockReturnValueOnce(null)
|
||||||
|
|
||||||
|
const { container } = render(<SearchCallout />)
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should call useMedusaSuggestions with the correct query", () => {
|
||||||
|
render(<SearchCallout />)
|
||||||
|
|
||||||
|
expect(mockUseMedusaSuggestions).toHaveBeenCalledWith({
|
||||||
|
keywords: "test",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("interaction", () => {
|
||||||
|
test("should track click event when card is clicked", () => {
|
||||||
|
const mockCardProps = {
|
||||||
|
title: "Test Card",
|
||||||
|
text: "This is a test card.",
|
||||||
|
href: "https://example.com",
|
||||||
|
icon: () => <div>Icon</div>,
|
||||||
|
}
|
||||||
|
mockUseMedusaSuggestions.mockReturnValueOnce(mockCardProps)
|
||||||
|
|
||||||
|
const { getByTestId } = render(<SearchCallout />)
|
||||||
|
|
||||||
|
const cardElement = getByTestId("card")
|
||||||
|
expect(cardElement).toBeInTheDocument()
|
||||||
|
|
||||||
|
// Simulate click
|
||||||
|
fireEvent.click(cardElement!)
|
||||||
|
|
||||||
|
expect(mockTrack).toHaveBeenCalledWith({
|
||||||
|
event: {
|
||||||
|
event: DocsTrackingEvents.SEARCH_CALLOUT_CLICK,
|
||||||
|
options: {
|
||||||
|
user_keywords: "test",
|
||||||
|
callout_title: mockCardProps.title,
|
||||||
|
callout_href: mockCardProps.href,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React from "react"
|
||||||
|
import { useInstantSearch } from "react-instantsearch"
|
||||||
|
import { useMedusaSuggestions } from "../../../hooks/use-medusa-suggestions"
|
||||||
|
import { Card } from "../../Card"
|
||||||
|
import { useAnalytics } from "../../../providers/Analytics"
|
||||||
|
import { DocsTrackingEvents } from "../../../constants"
|
||||||
|
|
||||||
|
export const SearchCallout = () => {
|
||||||
|
const { results } = useInstantSearch()
|
||||||
|
const query = results?.query || ""
|
||||||
|
const matchedCallout = useMedusaSuggestions({ keywords: query })
|
||||||
|
const { track } = useAnalytics()
|
||||||
|
|
||||||
|
if (!matchedCallout) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="m-docs_1 flex justify-center items-center">
|
||||||
|
<Card
|
||||||
|
{...matchedCallout}
|
||||||
|
onClick={() => {
|
||||||
|
track({
|
||||||
|
event: {
|
||||||
|
event: DocsTrackingEvents.SEARCH_CALLOUT_CLICK,
|
||||||
|
options: {
|
||||||
|
user_keywords: query,
|
||||||
|
callout_title: matchedCallout.title,
|
||||||
|
callout_href: matchedCallout.href,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -109,6 +109,9 @@ vi.mock("@/components/Search/Footer", () => ({
|
|||||||
vi.mock("@/components/Loading/Spinner", () => ({
|
vi.mock("@/components/Loading/Spinner", () => ({
|
||||||
SpinnerLoading: () => <div data-testid="spinner-loading">Loading</div>,
|
SpinnerLoading: () => <div data-testid="spinner-loading">Loading</div>,
|
||||||
}))
|
}))
|
||||||
|
vi.mock("@/components/Search/Callout", () => ({
|
||||||
|
SearchCallout: () => <div data-testid="search-callout">Callout</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
import { Search } from "../../Search"
|
import { Search } from "../../Search"
|
||||||
|
|
||||||
@@ -209,6 +212,15 @@ describe("rendering", () => {
|
|||||||
expect(footer).toBeInTheDocument()
|
expect(footer).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("renders SearchCallout", () => {
|
||||||
|
const suggestions: SearchSuggestionType[] = []
|
||||||
|
const { container } = render(
|
||||||
|
<Search algolia={defaultAlgoliaProps} suggestions={suggestions} />
|
||||||
|
)
|
||||||
|
const callout = container.querySelector("[data-testid='search-callout']")
|
||||||
|
expect(callout).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
test("passes checkInternalPattern to SearchHitsWrapper", () => {
|
test("passes checkInternalPattern to SearchHitsWrapper", () => {
|
||||||
const suggestions: SearchSuggestionType[] = []
|
const suggestions: SearchSuggestionType[] = []
|
||||||
const pattern = /\/docs\//
|
const pattern = /\/docs\//
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useSearchNavigation } from "@/hooks/use-search-navigation"
|
|||||||
import { OptionType } from "@/hooks/use-select"
|
import { OptionType } from "@/hooks/use-select"
|
||||||
import { SearchFooter } from "./Footer"
|
import { SearchFooter } from "./Footer"
|
||||||
import { SearchFilters } from "./Filters"
|
import { SearchFilters } from "./Filters"
|
||||||
|
import { SearchCallout } from "./Callout"
|
||||||
|
|
||||||
export type SearchProps = {
|
export type SearchProps = {
|
||||||
algolia: AlgoliaProps
|
algolia: AlgoliaProps
|
||||||
@@ -112,6 +113,7 @@ export const Search = ({
|
|||||||
<SearchEmptyQueryBoundary
|
<SearchEmptyQueryBoundary
|
||||||
fallback={<SearchSuggestions suggestions={suggestions} />}
|
fallback={<SearchSuggestions suggestions={suggestions} />}
|
||||||
>
|
>
|
||||||
|
<SearchCallout />
|
||||||
<SearchHitsWrapper
|
<SearchHitsWrapper
|
||||||
configureProps={{}}
|
configureProps={{}}
|
||||||
checkInternalPattern={checkInternalPattern}
|
checkInternalPattern={checkInternalPattern}
|
||||||
|
|||||||
@@ -510,4 +510,6 @@ export enum DocsTrackingEvents {
|
|||||||
SURVEY_API = "survey_api-ref",
|
SURVEY_API = "survey_api-ref",
|
||||||
CODE_BLOCK_COPY = "code_block_copy",
|
CODE_BLOCK_COPY = "code_block_copy",
|
||||||
AI_ASSISTANT_START_CHAT = "ai_assistant_start_chat",
|
AI_ASSISTANT_START_CHAT = "ai_assistant_start_chat",
|
||||||
|
AI_ASSISTANT_CALLOUT_CLICK = "ai_assistant_callout_click",
|
||||||
|
SEARCH_CALLOUT_CLICK = "search_callout_click",
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ export * from "./use-current-learning-path"
|
|||||||
export * from "./use-is-external-link"
|
export * from "./use-is-external-link"
|
||||||
export * from "./use-mutation-observer"
|
export * from "./use-mutation-observer"
|
||||||
export * from "./use-keyboard-shortcut"
|
export * from "./use-keyboard-shortcut"
|
||||||
|
export * from "./use-medusa-suggestions"
|
||||||
export * from "./use-page-scroll-manager"
|
export * from "./use-page-scroll-manager"
|
||||||
export * from "./use-request-runner"
|
export * from "./use-request-runner"
|
||||||
export * from "./use-scroll-utils"
|
export * from "./use-scroll-utils"
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
import { beforeEach, describe, expect, test, vi } from "vitest"
|
||||||
|
import { cleanup, render } from "@testing-library/react"
|
||||||
|
|
||||||
|
// mock components
|
||||||
|
vi.mock("@/componets/Card", () => ({
|
||||||
|
Card: (props: { title: string }) => <div data-testid="card">{props.title}</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { useMedusaSuggestions } from "../index"
|
||||||
|
|
||||||
|
const TestComponent = ({ keywords }: { keywords: string | string[] }) => {
|
||||||
|
const suggestion = useMedusaSuggestions({ keywords })
|
||||||
|
return suggestion ? (
|
||||||
|
<div data-testid="suggestion">{suggestion.title}</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("keywords", () => {
|
||||||
|
test("should handle empty keyword string", () => {
|
||||||
|
const { container } = render(<TestComponent keywords="" />)
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle empty keyword array", () => {
|
||||||
|
const { container } = render(<TestComponent keywords={[]} />)
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return null for no matches", () => {
|
||||||
|
const { container } = render(<TestComponent keywords="nonexistentkeyword" />)
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return suggestion for matching keyword string", () => {
|
||||||
|
const { getByTestId } = render(<TestComponent keywords="railway" />)
|
||||||
|
|
||||||
|
const suggestion = getByTestId("suggestion")
|
||||||
|
|
||||||
|
expect(suggestion).toBeInTheDocument()
|
||||||
|
expect(suggestion).toHaveTextContent("Deploy to Cloud")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return suggestion for matching keyword in array", () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<TestComponent keywords={["nonexistent", "railway", "anothernonexistent"]} />
|
||||||
|
)
|
||||||
|
|
||||||
|
const suggestion = getByTestId("suggestion")
|
||||||
|
|
||||||
|
expect(suggestion).toBeInTheDocument()
|
||||||
|
expect(suggestion).toHaveTextContent("Deploy to Cloud")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should be case insensitive and ignore punctuation", () => {
|
||||||
|
const { getByTestId } = render(<TestComponent keywords="Railway!" />)
|
||||||
|
|
||||||
|
const suggestion = getByTestId("suggestion")
|
||||||
|
|
||||||
|
expect(suggestion).toBeInTheDocument()
|
||||||
|
expect(suggestion).toHaveTextContent("Deploy to Cloud")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { CardProps } from "../../components/Card"
|
||||||
|
import { medusaSuggestions } from "./suggestions"
|
||||||
|
|
||||||
|
const PUNCTIONATION = /[.,?;!]/g
|
||||||
|
|
||||||
|
type UseSuggestionsProps = {
|
||||||
|
keywords: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMedusaSuggestions = ({ keywords }: UseSuggestionsProps) => {
|
||||||
|
if (!keywords.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const keywordsArray = Array.isArray(keywords)
|
||||||
|
? keywords.map(formatWord)
|
||||||
|
: keywords.split(" ").map(formatWord)
|
||||||
|
|
||||||
|
let matchedSuggestion: CardProps | null = null
|
||||||
|
|
||||||
|
keywordsArray.some((word) => {
|
||||||
|
if (medusaSuggestions.has(word)) {
|
||||||
|
matchedSuggestion = medusaSuggestions.get(word) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedSuggestion !== null
|
||||||
|
})
|
||||||
|
|
||||||
|
return matchedSuggestion as CardProps | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatWord = (word: string) => {
|
||||||
|
return word.toLowerCase().trim().replace(PUNCTIONATION, "")
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { ColoredMedusaIcon } from "../../components/Icons/ColoredMedusa"
|
||||||
|
import { CardProps } from "../../components/Card"
|
||||||
|
|
||||||
|
const CLOUD_SUGGESTION: CardProps = {
|
||||||
|
title: "Deploy to Cloud",
|
||||||
|
text: "Deploy and manage production-ready Medusa applications with zero-configuration deployments, automatic scaling, and GitHub integration, and more.",
|
||||||
|
href: "https://cloud.medusajs.com/signup",
|
||||||
|
icon: ColoredMedusaIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLOUD_MAIL_SUGGESTION: CardProps = {
|
||||||
|
title: "Deploy to Cloud",
|
||||||
|
text: "Deploy to Cloud with email sending support out-of-the-box.",
|
||||||
|
href: "https://cloud.medusajs.com/signup",
|
||||||
|
icon: ColoredMedusaIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLOUD_S3_SUGGESTION: CardProps = {
|
||||||
|
title: "Deploy to Cloud",
|
||||||
|
text: "Deploy to Cloud with S3 storage support out-of-the-box.",
|
||||||
|
href: "https://cloud.medusajs.com/signup",
|
||||||
|
icon: ColoredMedusaIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLOUD_CACHE_SUGGESTION: CardProps = {
|
||||||
|
title: "Deploy to Cloud",
|
||||||
|
text: "Deploy to Cloud with caching support out-of-the-box.",
|
||||||
|
href: "https://cloud.medusajs.com/signup",
|
||||||
|
icon: ColoredMedusaIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Suggestions = Map<string, CardProps>
|
||||||
|
|
||||||
|
export const medusaSuggestions: Suggestions = new Map([
|
||||||
|
["railway", CLOUD_SUGGESTION],
|
||||||
|
["heroku", CLOUD_SUGGESTION],
|
||||||
|
["aws", CLOUD_SUGGESTION],
|
||||||
|
["coolify", CLOUD_SUGGESTION],
|
||||||
|
["resend", CLOUD_MAIL_SUGGESTION],
|
||||||
|
["sendgrid", CLOUD_MAIL_SUGGESTION],
|
||||||
|
["s3", CLOUD_S3_SUGGESTION],
|
||||||
|
["minio", CLOUD_S3_SUGGESTION],
|
||||||
|
["cache", CLOUD_CACHE_SUGGESTION],
|
||||||
|
["caching", CLOUD_CACHE_SUGGESTION],
|
||||||
|
])
|
||||||
Reference in new issue
Block a user