docs: expanded rating form + UI docs improvement (#5211)
* docs: expanded rating form + UI docs improvement * added props table + changed example
This commit is contained in:
1 parent
dee3419fe8
commit
987dd8f568
15 files changed
+230
-31
No files matched your search
@@ -22,9 +22,12 @@ const useOnboarding = () => {
|
|||||||
children: (
|
children: (
|
||||||
<Rating
|
<Rating
|
||||||
event="rating_onboarding"
|
event="rating_onboarding"
|
||||||
onRating={() => {
|
onRating={(rating?: number) => {
|
||||||
|
if (rating >= 4) {
|
||||||
setTimeout(() => removeNotification(id), 1500)
|
setTimeout(() => removeNotification(id), 1500)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
parentNotificationId={id}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const Feedback = ({ title, ...props }: FeedbackProps) => {
|
|||||||
<UiFeedback
|
<UiFeedback
|
||||||
event="survey"
|
event="survey"
|
||||||
pathName={absoluteUrl(pathname)}
|
pathName={absoluteUrl(pathname)}
|
||||||
reportLink={formatReportLink("UI Docs", title)}
|
reportLink={formatReportLink("UI Docs", title, "ui")}
|
||||||
extraData={{
|
extraData={{
|
||||||
section: title,
|
section: title,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -91,6 +91,20 @@ const components = {
|
|||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
ol: ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLOListElement>) => {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
{...props}
|
||||||
|
className={clsx("list-decimal px-docs_1 mb-docs_1.5", className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
},
|
||||||
li: ({
|
li: ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import { FocusModal } from "@medusajs/ui"
|
|||||||
|
|
||||||
This component is based on the [Radix UI Dialog](https://www.radix-ui.com/primitives/docs/components/dialog) primitives.
|
This component is based on the [Radix UI Dialog](https://www.radix-ui.com/primitives/docs/components/dialog) primitives.
|
||||||
|
|
||||||
|
<ComponentProps component="focus-modal" />
|
||||||
|
|
||||||
### FocusModal.Header
|
### FocusModal.Header
|
||||||
|
|
||||||
This component is based on the `div` element and supports all props of this element.
|
This component is based on the `div` element and supports all props of this element.
|
||||||
@@ -37,3 +39,28 @@ This component is based on the `div` element and supports all props of this elem
|
|||||||
### FocusModal.Body
|
### FocusModal.Body
|
||||||
|
|
||||||
This component is based on the `div` element and supports all props of this element.
|
This component is based on the `div` element and supports all props of this element.
|
||||||
|
|
||||||
|
## Example: Control Open State
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
const MyModal = () => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusModal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
>
|
||||||
|
<FocusModal.Trigger>Trigger</FocusModal.Trigger>
|
||||||
|
<FocusModal.Content>
|
||||||
|
<FocusModal.Header>Title</FocusModal.Header>
|
||||||
|
<FocusModal.Body>Content</FocusModal.Body>
|
||||||
|
</FocusModal.Content>
|
||||||
|
</FocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { PropTable } from "@/components/props-table"
|
||||||
|
import { PropDataMap } from "@/types/props"
|
||||||
|
|
||||||
|
const focusModalProps: PropDataMap = [
|
||||||
|
{
|
||||||
|
prop: "defaultOpen",
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "open",
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: "onOpenChange",
|
||||||
|
type: {
|
||||||
|
type: "function",
|
||||||
|
signature: `(open: boolean) => void`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const Props = () => {
|
||||||
|
return <PropTable props={focusModalProps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Props
|
||||||
@@ -29,6 +29,9 @@ export const PropRegistry: Record<string, PropRegistryItem> = {
|
|||||||
"date-picker": {
|
"date-picker": {
|
||||||
table: React.lazy(async () => import("../props/date-picker")),
|
table: React.lazy(async () => import("../props/date-picker")),
|
||||||
},
|
},
|
||||||
|
"focus-modal": {
|
||||||
|
table: React.lazy(async () => import("../props/focus-modal")),
|
||||||
|
},
|
||||||
heading: {
|
heading: {
|
||||||
table: React.lazy(async () => import("../props/heading")),
|
table: React.lazy(async () => import("../props/heading")),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Button } from "@/components"
|
|||||||
|
|
||||||
export type NotificationItemLayoutDefaultProps = NotificationItemProps & {
|
export type NotificationItemLayoutDefaultProps = NotificationItemProps & {
|
||||||
handleClose: () => void
|
handleClose: () => void
|
||||||
|
closeButtonText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotificationItemLayoutDefault: React.FC<
|
export const NotificationItemLayoutDefault: React.FC<
|
||||||
@@ -23,6 +24,7 @@ export const NotificationItemLayoutDefault: React.FC<
|
|||||||
isClosable = true,
|
isClosable = true,
|
||||||
handleClose,
|
handleClose,
|
||||||
CustomIcon,
|
CustomIcon,
|
||||||
|
closeButtonText = "Close",
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -74,7 +76,7 @@ export const NotificationItemLayoutDefault: React.FC<
|
|||||||
)}
|
)}
|
||||||
{isClosable && (
|
{isClosable && (
|
||||||
<div className={clsx("p-docs_1 flex justify-end items-center")}>
|
<div className={clsx("p-docs_1 flex justify-end items-center")}>
|
||||||
<Button onClick={handleClose}>Close</Button>
|
<Button onClick={handleClose}>{closeButtonText}</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type NotificationItemProps = {
|
|||||||
show?: boolean
|
show?: boolean
|
||||||
setShow?: (value: boolean) => void
|
setShow?: (value: boolean) => void
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
|
closeButtonText?: string
|
||||||
} & React.HTMLAttributes<HTMLDivElement>
|
} & React.HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
export const NotificationItem = ({
|
export const NotificationItem = ({
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { NotificationItemType, useNotifications } from "@/providers"
|
import {
|
||||||
|
NotificationContextType,
|
||||||
|
NotificationItemType,
|
||||||
|
useNotifications,
|
||||||
|
} from "@/providers"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { NotificationItem } from "./Item"
|
import { NotificationItem } from "./Item"
|
||||||
import { CSSTransition, TransitionGroup } from "react-transition-group"
|
import { CSSTransition, TransitionGroup } from "react-transition-group"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
|
|
||||||
export const NotificationContainer = () => {
|
export const NotificationContainer = () => {
|
||||||
const { notifications, removeNotification } = useNotifications()
|
const { notifications, removeNotification } =
|
||||||
|
useNotifications() as NotificationContextType
|
||||||
|
|
||||||
const handleClose = (notification: NotificationItemType) => {
|
const handleClose = (notification: NotificationItemType) => {
|
||||||
notification.onClose?.()
|
notification.onClose?.()
|
||||||
@@ -25,8 +30,8 @@ export const NotificationContainer = () => {
|
|||||||
key={notification.id}
|
key={notification.id}
|
||||||
timeout={200}
|
timeout={200}
|
||||||
classNames={{
|
classNames={{
|
||||||
enter: "animate__animated animate__slideInRight animate__fastest",
|
enter: "animate-slideInRight animate-fast",
|
||||||
exit: "animate__animated animate__slideOutRight animate__fastest",
|
exit: "animate-slideOutRight animate-fast",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<NotificationItem
|
<NotificationItem
|
||||||
|
|||||||
@@ -1,27 +1,47 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { useRef, useState } from "react"
|
import React, { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import { Star, StarSolid } from "@medusajs/icons"
|
import { Star, StarSolid } from "@medusajs/icons"
|
||||||
import { Button } from "@/components"
|
import { Button, Label, TextArea } from "@/components"
|
||||||
import { useAnalytics } from "@/providers"
|
import { useAnalytics, useNotifications } from "@/providers"
|
||||||
|
|
||||||
export type RatingProps = {
|
export type RatingProps = {
|
||||||
event?: string
|
event?: string
|
||||||
className?: string
|
className?: string
|
||||||
onRating?: () => void
|
onRating?: (rating?: number) => void
|
||||||
|
additionalQuestion?: string
|
||||||
|
parentNotificationId?: string
|
||||||
} & React.HTMLAttributes<HTMLDivElement>
|
} & React.HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
export const Rating: React.FC<RatingProps> = ({
|
export const Rating: React.FC<RatingProps> = ({
|
||||||
event = "rating",
|
event = "rating",
|
||||||
className = "",
|
className = "",
|
||||||
onRating,
|
onRating,
|
||||||
|
additionalQuestion = "What should we improve?",
|
||||||
|
parentNotificationId,
|
||||||
}) => {
|
}) => {
|
||||||
const [rating, setRating] = useState(0)
|
const [rating, setRating] = useState(0)
|
||||||
|
const [additionalFeedback, setAdditionalFeedback] = useState("")
|
||||||
const [hoverRating, setHoverRating] = useState(0)
|
const [hoverRating, setHoverRating] = useState(0)
|
||||||
const starElms = useRef<HTMLElement[]>([])
|
const starElms = useRef<HTMLElement[]>([])
|
||||||
const starArr = Array.from(Array(5).keys())
|
const starArr = Array.from(Array(5).keys())
|
||||||
const { track } = useAnalytics()
|
const { track } = useAnalytics()
|
||||||
|
const { updateNotification } = useNotifications(true) || {}
|
||||||
|
|
||||||
|
const submitTracking = useCallback(
|
||||||
|
(selectedRating?: number, feedback?: string) => {
|
||||||
|
track(
|
||||||
|
event,
|
||||||
|
{
|
||||||
|
rating: selectedRating || rating,
|
||||||
|
additionalFeedback: feedback || additionalFeedback,
|
||||||
|
},
|
||||||
|
() => onRating?.(selectedRating || rating)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[rating, additionalFeedback]
|
||||||
|
)
|
||||||
|
|
||||||
const handleRating = (selectedRating: number) => {
|
const handleRating = (selectedRating: number) => {
|
||||||
if (rating) {
|
if (rating) {
|
||||||
@@ -29,19 +49,31 @@ export const Rating: React.FC<RatingProps> = ({
|
|||||||
}
|
}
|
||||||
setHoverRating(0)
|
setHoverRating(0)
|
||||||
setRating(selectedRating)
|
setRating(selectedRating)
|
||||||
|
if (selectedRating >= 4) {
|
||||||
for (let i = 0; i < selectedRating; i++) {
|
for (let i = 0; i < selectedRating; i++) {
|
||||||
starElms.current[i].classList.add("animate-tada")
|
starElms.current[i].classList.add("animate-tada")
|
||||||
}
|
}
|
||||||
track(
|
submitTracking(selectedRating)
|
||||||
event,
|
}
|
||||||
{
|
|
||||||
rating: selectedRating,
|
|
||||||
},
|
|
||||||
() => onRating?.()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
rating > 0 &&
|
||||||
|
rating < 4 &&
|
||||||
|
parentNotificationId &&
|
||||||
|
updateNotification
|
||||||
|
) {
|
||||||
|
// update parent notification ID
|
||||||
|
updateNotification(parentNotificationId, {
|
||||||
|
closeButtonText: "Submit",
|
||||||
|
onClose: () => submitTracking(rating, additionalFeedback),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [additionalFeedback, rating])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={clsx("flex flex-col gap-docs_1")}>
|
||||||
<div className={clsx("flex gap-docs_0.5", className)}>
|
<div className={clsx("flex gap-docs_0.5", className)}>
|
||||||
{starArr.map((i) => {
|
{starArr.map((i) => {
|
||||||
const isSelected =
|
const isSelected =
|
||||||
@@ -76,5 +108,23 @@ export const Rating: React.FC<RatingProps> = ({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{rating !== 0 && rating < 4 && (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"text-medusa-fg-subtle",
|
||||||
|
"flex flex-col gap-docs_0.5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Label>{additionalQuestion}</Label>
|
||||||
|
<TextArea
|
||||||
|
placeholder="I didn't like..."
|
||||||
|
rows={4}
|
||||||
|
onChange={(e) => setAdditionalFeedback(e.target.value)}
|
||||||
|
value={additionalFeedback}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export const GITHUB_ISSUES_PREFIX = `https://github.com/medusajs/medusa/issues/new?assignees=&labels=type%3A+docs&template=docs.yml`
|
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`
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useState } from "react"
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
import { Analytics, AnalyticsBrowser } from "@segment/analytics-next"
|
import { Analytics, AnalyticsBrowser } from "@segment/analytics-next"
|
||||||
|
|
||||||
export type ExtraData = {
|
export type ExtraData = {
|
||||||
@@ -18,6 +24,11 @@ export type AnalyticsContextType = {
|
|||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TrackedEvent = {
|
||||||
|
event: string
|
||||||
|
options?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
const AnalyticsContext = createContext<AnalyticsContextType | null>(null)
|
const AnalyticsContext = createContext<AnalyticsContextType | null>(null)
|
||||||
|
|
||||||
export type AnalyticsProviderProps = {
|
export type AnalyticsProviderProps = {
|
||||||
@@ -37,8 +48,9 @@ export const AnalyticsProvider = ({
|
|||||||
const [loaded, setLoaded] = useState<boolean>(false)
|
const [loaded, setLoaded] = useState<boolean>(false)
|
||||||
const [analytics, setAnalytics] = useState<Analytics | null>(null)
|
const [analytics, setAnalytics] = useState<Analytics | null>(null)
|
||||||
const analyticsBrowser = new AnalyticsBrowser()
|
const analyticsBrowser = new AnalyticsBrowser()
|
||||||
|
const [queue, setQueue] = useState<TrackedEvent[]>([])
|
||||||
|
|
||||||
const init = () => {
|
const init = useCallback(() => {
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
analyticsBrowser
|
analyticsBrowser
|
||||||
.load(
|
.load(
|
||||||
@@ -60,9 +72,10 @@ export const AnalyticsProvider = ({
|
|||||||
)
|
)
|
||||||
.finally(() => setLoaded(true))
|
.finally(() => setLoaded(true))
|
||||||
}
|
}
|
||||||
}
|
}, [loaded, writeKey])
|
||||||
|
|
||||||
const track = async (
|
const track = useCallback(
|
||||||
|
async (
|
||||||
event: string,
|
event: string,
|
||||||
options?: Record<string, any>,
|
options?: Record<string, any>,
|
||||||
callback?: () => void
|
callback?: () => void
|
||||||
@@ -76,17 +89,39 @@ export const AnalyticsProvider = ({
|
|||||||
},
|
},
|
||||||
callback
|
callback
|
||||||
)
|
)
|
||||||
} else if (callback) {
|
} else {
|
||||||
|
// push the event into the queue
|
||||||
|
setQueue((prevQueue) => [
|
||||||
|
...prevQueue,
|
||||||
|
{
|
||||||
|
event,
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
if (callback) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Segment is either not installed or not configured. Simulating success..."
|
"Segment is either not installed or not configured. Simulating success..."
|
||||||
)
|
)
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[analytics, loaded]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
init()
|
init()
|
||||||
}, [])
|
}, [init])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (analytics && queue.length) {
|
||||||
|
// track stuff in queue
|
||||||
|
queue.forEach(async (trackEvent) =>
|
||||||
|
track(trackEvent.event, trackEvent.options)
|
||||||
|
)
|
||||||
|
setQueue([])
|
||||||
|
}
|
||||||
|
}, [analytics, queue])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnalyticsContext.Provider
|
<AnalyticsContext.Provider
|
||||||
|
|||||||
@@ -122,10 +122,12 @@ export const NotificationProvider = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotifications = (): NotificationContextType => {
|
export const useNotifications = (
|
||||||
|
suppressError?: boolean
|
||||||
|
): NotificationContextType | null => {
|
||||||
const context = useContext(NotificationContext)
|
const context = useContext(NotificationContext)
|
||||||
|
|
||||||
if (!context) {
|
if (!context && !suppressError) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"useNotifications must be used within a NotificationProvider"
|
"useNotifications must be used within a NotificationProvider"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { GITHUB_ISSUES_PREFIX } from "../constants"
|
import { GITHUB_ISSUES_PREFIX, GITHUB_UI_ISSUES_PREFIX } from "../constants"
|
||||||
|
|
||||||
export function formatReportLink(title: string, sectionTitle: string) {
|
export type ReportLinkType = "default" | "ui"
|
||||||
return `${GITHUB_ISSUES_PREFIX}&title=${encodeURI(
|
|
||||||
title
|
export function formatReportLink(
|
||||||
)}%3A%20Issue%20in%20${encodeURI(sectionTitle)}`
|
title: string,
|
||||||
|
sectionTitle: string,
|
||||||
|
type?: ReportLinkType
|
||||||
|
) {
|
||||||
|
let prefix = GITHUB_ISSUES_PREFIX
|
||||||
|
if (type === "ui") {
|
||||||
|
prefix = GITHUB_UI_ISSUES_PREFIX
|
||||||
|
}
|
||||||
|
return `${prefix}&title=${encodeURI(title)}%3A%20Issue%20in%20${encodeURI(
|
||||||
|
sectionTitle
|
||||||
|
)}`
|
||||||
}
|
}
|
||||||
@@ -459,11 +459,31 @@ module.exports = {
|
|||||||
transform: "scale3d(1, 1, 1)",
|
transform: "scale3d(1, 1, 1)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
slideInRight: {
|
||||||
|
from: {
|
||||||
|
transform: "translate3d(100%, 0, 0)",
|
||||||
|
visibility: "visible",
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: "translate3d(0, 0, 0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slideOutRight: {
|
||||||
|
from: {
|
||||||
|
transform: "translate3d(0, 0, 0)",
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
visibility: "hidden",
|
||||||
|
transform: "translate3d(100%, 0, 0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
fadeIn: "fadeIn 500ms",
|
fadeIn: "fadeIn 500ms",
|
||||||
fadeOut: "fadeOut 500ms",
|
fadeOut: "fadeOut 500ms",
|
||||||
tada: "tada 1s",
|
tada: "tada 1s",
|
||||||
|
slideInRight: "slideInRight 500ms",
|
||||||
|
slideOutRight: "slideOutRight 500ms",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
|
|||||||
Reference in new issue
Block a user