docs: improve UX of recipe learning path (#5477)

* docs: improve UX of recipe learning path

* fix icon colors
This commit is contained in:
Shahed Nasser
2023-10-26 16:05:17 +03:00
committed by GitHub
parent ea2ee343f0
commit 7f9c4dea66
9 changed files with 231 additions and 164 deletions

View File

@@ -1,15 +1,18 @@
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>
const LearningPathStepActions: React.FC<LearningPathStepActionsType> = ({
onFinish,
onClose,
setCollapsed,
}) => {
const { hasNextStep, nextStep, endPath } = useLearningPath()
@@ -22,18 +25,34 @@ const LearningPathStepActions: React.FC<LearningPathStepActionsType> = ({
}
return (
<div className="flex gap-0.5 p-1 justify-end items-center">
<Button onClick={onClose}>Close</Button>
{hasNextStep() && (
<Button onClick={nextStep} variant="primary">
Next
<div className="flex p-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>
)}
{!hasNextStep() && (
<Button onClick={handleFinish} variant="primary">
Finish
</Button>
)}
</div>
<div className="flex gap-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>
)
}

View File

@@ -1,10 +1,12 @@
import { useLearningPath } from "@site/src/providers/LearningPath"
import React from "react"
import React, { useCallback, useEffect, useRef, useState } from "react"
import LearningPathStepActions from "./Actions"
import clsx from "clsx"
import IconCircleDottedLine from "@site/src/theme/Icon/CircleDottedLine"
import Link from "@docusaurus/Link"
import { CheckCircleSolid, CircleMiniSolid } from "@medusajs/icons"
import { CheckCircleSolid, CircleMiniSolid, ListBullet } from "@medusajs/icons"
import { Badge, Button } from "docs-ui"
import { CSSTransition, SwitchTransition } from "react-transition-group"
type LearningPathStepsProps = {
onFinish?: () => void
@@ -13,66 +15,140 @@ type LearningPathStepsProps = {
const LearningPathSteps: React.FC<LearningPathStepsProps> = ({ ...rest }) => {
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 (
<>
<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-1"
)}
key={index}
>
<div className={clsx("flex items-center gap-1")}>
<div className="w-2 flex-none flex items-center justify-center">
{index === currentStep && (
<IconCircleDottedLine
<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(
"shadow-active dark:shadow-active-dark rounded-full",
"!text-ui-fg-interactive"
"border-0 border-b border-solid border-medusa-border-base",
"relative p-1"
)}
/>
)}
{index < currentStep && (
<CheckCircleSolid className="text-ui-fg-interactive" />
)}
{index > currentStep && (
<CircleMiniSolid className="text-ui-fg-subtle" />
)}
key={index}
>
<div className={clsx("flex items-center gap-1")}>
<div className="w-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>
</div>
{index === currentStep && (
<div className={clsx("flex items-center gap-1")}>
<div className="w-2 flex-none"></div>
<div
className={clsx("text-medium text-ui-fg-subtle mt-1")}
>
{step.descriptionJSX ?? step.description}
</div>
</div>
)}
<Link
href={step.path}
className={clsx("absolute top-0 left-0 w-full h-full")}
onClick={(e) => {
e.preventDefault()
goToStep(index)
}}
/>
</div>
))}
</div>
<span
className={clsx("text-compact-medium-plus text-medusa-fg-base")}
>
{step.title}
</span>
<LearningPathStepActions setCollapsed={setCollapsed} {...rest} />
</div>
{index === currentStep && (
<div className={clsx("flex items-center gap-1")}>
<div className="w-2 flex-none"></div>
<div className={clsx("text-medium text-ui-fg-subtle mt-1")}>
{step.descriptionJSX ?? step.description}
</div>
</div>
)}
<Link
href={step.path}
className={clsx("absolute top-0 left-0 w-full h-full")}
onClick={(e) => {
e.preventDefault()
goToStep(index)
}}
/>
</div>
))}
</div>
<LearningPathStepActions {...rest} />
</>
)}
{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-1 max-[767px]:right-1 "
)}
onClick={() => setCollapsed(false)}
buttonRef={buttonRef}
>
<ListBullet />
<Badge
variant="blue"
className={clsx("absolute -top-0.25 -right-0.25")}
>
!
</Badge>
</Button>
)}
</>
</CSSTransition>
</SwitchTransition>
)
}

View File

@@ -1,93 +0,0 @@
import { useCallback, useMemo } from "react"
export type OptionType = {
value: string
label: string
index?: string
isAllOption?: boolean
}
export type SelectOptions = {
value: string | string[]
multiple?: boolean
options: OptionType[]
setSelected?: (value: string | string[]) => void
addSelected?: (value: string) => void
removeSelected?: (value: string) => void
handleAddAll?: (isAllSelected: boolean) => void
}
const useSelect = ({
value,
options,
multiple = false,
setSelected,
addSelected,
removeSelected,
handleAddAll,
}: SelectOptions) => {
const isValueSelected = useCallback(
(val: string) => {
return (
(typeof value === "string" && val === value) ||
(Array.isArray(value) && value.includes(val))
)
},
[value]
)
// checks if there are multiple selected values
const hasSelectedValues = useMemo(() => {
return multiple && Array.isArray(value) && value.length > 0
}, [value, multiple])
// checks if there are any selected values,
// whether multiple or one
const hasSelectedValue = useMemo(() => {
return hasSelectedValues || (typeof value === "string" && value.length)
}, [hasSelectedValues, value])
const selectedValues: OptionType[] = useMemo(() => {
if (typeof value === "string") {
const selectedValue = options.find((option) => option.value === value)
return selectedValue ? [selectedValue] : []
} else if (Array.isArray(value)) {
return options.filter((option) => value.includes(option.value))
}
return []
}, [options, value])
const isAllSelected = useMemo(() => {
return Array.isArray(value) && value.length === options.length
}, [options, value])
const handleChange = (selectedValue: string, wasSelected: boolean) => {
if (multiple) {
wasSelected
? removeSelected?.(selectedValue)
: addSelected?.(selectedValue)
} else {
setSelected?.(selectedValue)
}
}
const handleSelectAll = () => {
if (handleAddAll) {
handleAddAll(isAllSelected)
} else {
setSelected?.(options.map((option) => option.value))
}
}
return {
isValueSelected,
hasSelectedValue,
hasSelectedValues,
selectedValues,
isAllSelected,
handleChange,
handleSelectAll,
}
}
export default useSelect

View File

@@ -9,6 +9,7 @@ import { useThemeConfig } from "@docusaurus/theme-common"
import { ThemeConfig } from "@medusajs/docs"
import SearchProvider from "../Search"
import LearningPathProvider from "../LearningPath"
import SkipToContent from "@theme/SkipToContent"
type DocsProvidersProps = {
children?: React.ReactNode
@@ -25,7 +26,10 @@ const DocsProviders = ({ children }: DocsProvidersProps) => {
<ModalProvider>
<SearchProvider>
<LearningPathProvider>
<NotificationProvider>{children}</NotificationProvider>
<NotificationProvider>
<SkipToContent />
{children}
</NotificationProvider>
</LearningPathProvider>
</SearchProvider>
</ModalProvider>

View File

@@ -0,0 +1,33 @@
import React from "react"
import clsx from "clsx"
import { translate } from "@docusaurus/Translate"
import { useBackToTopButton } from "@docusaurus/theme-common/internal"
import { Button, useNotifications } from "docs-ui"
import { ArrowUpMini } from "@medusajs/icons"
export default function BackToTopButton(): JSX.Element {
const { shown, scrollToTop } = useBackToTopButton({ threshold: 300 })
const { notifications } = useNotifications()
return (
<Button
aria-label={translate({
id: "theme.BackToTopButton.buttonAriaLabel",
message: "Scroll back to top",
description: "The ARIA label for the back to top button",
})}
className={clsx(
"fixed right-1 rounded-full !p-[10px] !border-0",
"shadow-flyout dark:shadow-flyout-dark !text-medusa-fg-subtle",
"!transition-all opacity-0 scale-0 invisible",
shown && "!opacity-100 !scale-100 !visible",
notifications.length && "bottom-4",
!notifications.length && "bottom-1"
)}
variant="secondary"
onClick={scrollToTop}
>
<ArrowUpMini />
</Button>
)
}

View File

@@ -27,7 +27,7 @@ export const NotificationItemLayoutDefault: React.FC<
closeButtonText = "Close",
}) => {
return (
<>
<div className="bg-medusa-bg-base w-full h-full shadow-flyout dark:shadow-flyout-dark rounded-docs_DEFAULT">
<div className={clsx("flex gap-docs_1 p-docs_1")}>
{type !== "none" && (
<div
@@ -79,6 +79,6 @@ export const NotificationItemLayoutDefault: React.FC<
<Button onClick={handleClose}>{closeButtonText}</Button>
</div>
)}
</>
</div>
)
}

View File

@@ -36,8 +36,7 @@ export const NotificationItem = ({
return (
<div
className={clsx(
"md:max-w-[320px] md:w-[320px] w-full bg-medusa-bg-base rounded-docs_DEFAULT",
"shadow-flyout dark:shadow-flyout-dark max-h-[calc(100vh-90px)]",
"md:max-w-[320px] md:w-[320px] w-full",
"fixed md:right-docs_1 left-0 md:m-docs_1",
placement === "bottom" && "md:bottom-docs_1 bottom-0",
placement === "top" && "md:top-docs_1 top-0",

View File

@@ -24,7 +24,15 @@ export const NotificationContainer = () => {
className?: string
) => {
return (
<TransitionGroup className={className}>
<TransitionGroup
className={clsx(
"flex fixed flex-col gap-docs_0.5 right-0",
"md:w-auto w-full overflow-y-auto",
"max-h-[50%] md:max-h-[calc(100vh-57px)]",
notifications.length && "max-[768px]:h-[50%]",
className
)}
>
{notifications.filter(condition).map((notification) => (
<CSSTransition
key={notification.id}
@@ -52,11 +60,11 @@ export const NotificationContainer = () => {
<>
{renderFilteredNotifications(
(notification) => notification.placement === "top",
"flex fixed flex-col gap-docs_0.5 right-0 top-0 md:w-auto w-full max-h-[calc(100vh-57px)] overflow-y-auto"
"top-0"
)}
{renderFilteredNotifications(
(notification) => notification.placement !== "top",
"flex flex-col gap-docs_0.5 fixed right-0 bottom-0 md:w-auto w-full max-h-[calc(100vh-57px)] overflow-y-auto"
"bottom-0"
)}
</>
)

View File

@@ -560,6 +560,22 @@ module.exports = {
opacity: 0.3,
},
},
minimize: {
from: {
transform: "scale(1)",
},
to: {
transform: "scale(0)",
},
},
maximize: {
from: {
transform: "scale(0)",
},
to: {
transform: "scale(1)",
},
},
}),
animation: {
fadeIn: "fadeIn 500ms",
@@ -576,6 +592,8 @@ module.exports = {
slideInLeft: "slideInLeft 500ms",
slideOutLeft: "slideOutLeft 500ms",
pulsingDots: "pulsingDots 1s alternate infinite",
minimize: "minimize 500ms",
maximize: "maximize 500ms",
},
},
fontFamily: {
@@ -686,6 +704,9 @@ module.exports = {
display: "none",
},
},
".flip-y": {
transform: "rotateY(180deg)",
},
})
addComponents({
".btn-secondary-icon": {