docs: create docs workspace (#5174)
* docs: migrate ui docs to docs universe * created yarn workspace * added eslint and tsconfig configurations * fix eslint configurations * fixed eslint configurations * shared tailwind configurations * added shared ui package * added more shared components * migrating more components * made details components shared * move InlineCode component * moved InputText * moved Loading component * Moved Modal component * moved Select components * Moved Tooltip component * moved Search components * moved ColorMode provider * Moved Notification components and providers * used icons package * use UI colors in api-reference * moved Navbar component * used Navbar and Search in UI docs * added Feedback to UI docs * general enhancements * fix color mode * added copy colors file from ui-preset * added features and enhancements to UI docs * move Sidebar component and provider * general fixes and preparations for deployment * update docusaurus version * adjusted versions * fix output directory * remove rootDirectory property * fix yarn.lock * moved code component * added vale for all docs MD and MDX * fix tests * fix vale error * fix deployment errors * change ignore commands * add output directory * fix docs test * general fixes * content fixes * fix announcement script * added changeset * fix vale checks * added nofilter option * fix vale error
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from "react"
|
||||
import { Analytics, AnalyticsBrowser } from "@segment/analytics-next"
|
||||
|
||||
export type ExtraData = {
|
||||
section?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type AnalyticsContextType = {
|
||||
loaded: boolean
|
||||
analytics: Analytics | null
|
||||
track: (
|
||||
event: string,
|
||||
options?: Record<string, any>,
|
||||
callback?: () => void
|
||||
) => void
|
||||
}
|
||||
|
||||
const AnalyticsContext = createContext<AnalyticsContextType | null>(null)
|
||||
|
||||
export type AnalyticsProviderProps = {
|
||||
writeKey?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const LOCAL_STORAGE_KEY = "ajs_anonymous_id"
|
||||
|
||||
export const AnalyticsProvider = ({
|
||||
writeKey = "temp",
|
||||
children,
|
||||
}: AnalyticsProviderProps) => {
|
||||
// loaded is used to ensure that a connection has been made to segment
|
||||
// even if it failed. This is to ensure that the connection isn't
|
||||
// continuously retried
|
||||
const [loaded, setLoaded] = useState<boolean>(false)
|
||||
const [analytics, setAnalytics] = useState<Analytics | null>(null)
|
||||
const analyticsBrowser = new AnalyticsBrowser()
|
||||
|
||||
const init = () => {
|
||||
if (!loaded) {
|
||||
analyticsBrowser
|
||||
.load(
|
||||
{ writeKey },
|
||||
{
|
||||
initialPageview: true,
|
||||
user: {
|
||||
localStorage: {
|
||||
key: LOCAL_STORAGE_KEY,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((instance) => {
|
||||
setAnalytics(instance[0])
|
||||
})
|
||||
.catch((e) =>
|
||||
console.error(`Could not connect to Segment. Error: ${e}`)
|
||||
)
|
||||
.finally(() => setLoaded(true))
|
||||
}
|
||||
}
|
||||
|
||||
const track = async (
|
||||
event: string,
|
||||
options?: Record<string, any>,
|
||||
callback?: () => void
|
||||
) => {
|
||||
if (analytics) {
|
||||
void analytics.track(
|
||||
event,
|
||||
{
|
||||
...options,
|
||||
uuid: analytics.user().anonymousId(),
|
||||
},
|
||||
callback
|
||||
)
|
||||
} else if (callback) {
|
||||
console.warn(
|
||||
"Segment is either not installed or not configured. Simulating success..."
|
||||
)
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
init()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AnalyticsContext.Provider
|
||||
value={{
|
||||
analytics,
|
||||
track,
|
||||
loaded,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AnalyticsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useAnalytics = () => {
|
||||
const context = useContext(AnalyticsContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useAnalytics must be used within a AnalyticsProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from "react"
|
||||
|
||||
export type ColorMode = "light" | "dark"
|
||||
|
||||
export type ColorModeContextType = {
|
||||
colorMode: ColorMode
|
||||
setColorMode: (value: ColorMode) => void
|
||||
toggleColorMode: () => void
|
||||
}
|
||||
|
||||
const ColorModeContext = createContext<ColorModeContextType | null>(null)
|
||||
|
||||
export type ColorModeProviderProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const ColorModeProvider = ({ children }: ColorModeProviderProps) => {
|
||||
const [colorMode, setColorMode] = useState<ColorMode>("light")
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
const toggleColorMode = () =>
|
||||
setColorMode(colorMode === "light" ? "dark" : "light")
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
return
|
||||
}
|
||||
|
||||
const theme = localStorage.getItem("theme")
|
||||
if (theme && (theme === "light" || theme === "dark")) {
|
||||
setColorMode(theme)
|
||||
setLoaded(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.querySelector("html")?.setAttribute("data-theme", colorMode)
|
||||
}, [colorMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded) {
|
||||
return
|
||||
}
|
||||
|
||||
const theme = localStorage.getItem("theme")
|
||||
if (theme !== colorMode) {
|
||||
localStorage.setItem("theme", colorMode)
|
||||
}
|
||||
}, [loaded, colorMode])
|
||||
|
||||
return (
|
||||
<ColorModeContext.Provider
|
||||
value={{
|
||||
colorMode,
|
||||
setColorMode,
|
||||
toggleColorMode,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ColorModeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useColorMode = (): ColorModeContextType => {
|
||||
const context = useContext(ColorModeContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useColorMode must be used inside a ColorModeProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react"
|
||||
|
||||
export type MobileContextType = {
|
||||
isMobile?: boolean
|
||||
}
|
||||
|
||||
const MobileContext = createContext<MobileContextType | null>(null)
|
||||
|
||||
export type MobileProviderProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const MobileProvider = ({ children }: MobileProviderProps) => {
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
if (window.innerWidth < 1025 && !isMobile) {
|
||||
setIsMobile(true)
|
||||
} else if (window.innerWidth >= 1025 && isMobile) {
|
||||
setIsMobile(false)
|
||||
}
|
||||
}, [isMobile])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize)
|
||||
}
|
||||
}, [handleResize])
|
||||
|
||||
useEffect(() => {
|
||||
handleResize()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MobileContext.Provider
|
||||
value={{
|
||||
isMobile,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MobileContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useMobile = () => {
|
||||
const context = useContext(MobileContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useMobile must be used within a MobileProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import React, { useContext, useState } from "react"
|
||||
import { createContext } from "react"
|
||||
import { Modal, type ModalProps } from "@/components"
|
||||
|
||||
export type ModalContextType = {
|
||||
modalProps: ModalProps | null
|
||||
setModalProps: (value: ModalProps | null) => void
|
||||
closeModal: () => void
|
||||
}
|
||||
|
||||
const ModalContext = createContext<ModalContextType | null>(null)
|
||||
|
||||
export type ModalProviderProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const ModalProvider = ({ children }: ModalProviderProps) => {
|
||||
const [modalProps, setModalProps] = useState<ModalProps | null>(null)
|
||||
|
||||
const closeModal = () => {
|
||||
setModalProps(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalContext.Provider
|
||||
value={{
|
||||
modalProps,
|
||||
setModalProps,
|
||||
closeModal,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{modalProps && (
|
||||
<>
|
||||
<div className="bg-medusa-bg-overlay dark:bg-medusa-bg-overlay-dark fixed top-0 left-0 z-[499] h-screen w-screen"></div>
|
||||
<Modal {...modalProps} onClose={closeModal} />
|
||||
</>
|
||||
)}
|
||||
</ModalContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useModal = () => {
|
||||
const context = useContext(ModalContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useModal must be used within a ModalProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
export type NavbarContextType = {
|
||||
activeItem: string | null
|
||||
setActiveItem: (value: string) => void
|
||||
}
|
||||
|
||||
const NavbarContext = createContext<NavbarContextType | null>(null)
|
||||
|
||||
export type NavbarProviderProps = {
|
||||
children: React.ReactNode
|
||||
basePath?: string
|
||||
}
|
||||
|
||||
export const NavbarProvider = ({
|
||||
children,
|
||||
basePath = "",
|
||||
}: NavbarProviderProps) => {
|
||||
const [activeItem, setActiveItem] = useState<string | null>(null)
|
||||
const pathname = usePathname()
|
||||
|
||||
const assemblePathName = (path: string) =>
|
||||
`${basePath}/${path.charAt(0) === "/" ? path.substring(1) : path}`
|
||||
|
||||
useEffect(() => {
|
||||
const newPath = assemblePathName(pathname)
|
||||
if (activeItem !== newPath) {
|
||||
setActiveItem(newPath)
|
||||
}
|
||||
}, [pathname, activeItem])
|
||||
|
||||
return (
|
||||
<NavbarContext.Provider
|
||||
value={{
|
||||
activeItem,
|
||||
setActiveItem,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NavbarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useNavbar = (): NavbarContextType => {
|
||||
const context = useContext(NavbarContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useNavbar must be used inside a NavbarProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useContext, useReducer } from "react"
|
||||
import { NotificationItemProps, NotificationContainer } from "@/components"
|
||||
import uuid from "react-uuid"
|
||||
|
||||
export type NotificationItemType = {
|
||||
id?: string
|
||||
} & NotificationItemProps
|
||||
|
||||
export type NotificationContextType = {
|
||||
notifications: NotificationItemType[]
|
||||
addNotification: (value: NotificationItemType) => void
|
||||
generateId: () => string
|
||||
removeNotification: (id: string) => void
|
||||
updateNotification: (
|
||||
id: string,
|
||||
updatedData: Partial<Omit<NotificationItemType, "id">>
|
||||
) => void
|
||||
}
|
||||
|
||||
export enum NotificationReducerActionTypes {
|
||||
ADD = "add",
|
||||
REMOVE = "remove",
|
||||
UPDATE = "update",
|
||||
}
|
||||
|
||||
export type NotificationReducerAction =
|
||||
| {
|
||||
type: NotificationReducerActionTypes.ADD
|
||||
notification: NotificationItemType
|
||||
}
|
||||
| {
|
||||
type: NotificationReducerActionTypes.REMOVE
|
||||
id: string
|
||||
}
|
||||
| {
|
||||
type: NotificationReducerActionTypes.UPDATE
|
||||
id: string
|
||||
updatedData: Partial<Omit<NotificationItemType, "id">>
|
||||
}
|
||||
|
||||
const notificationReducer = (
|
||||
state: NotificationItemType[],
|
||||
action: NotificationReducerAction
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case NotificationReducerActionTypes.ADD:
|
||||
return [...state, action.notification]
|
||||
case NotificationReducerActionTypes.REMOVE:
|
||||
return state.filter((notification) => notification.id !== action.id)
|
||||
case NotificationReducerActionTypes.UPDATE:
|
||||
return state.map((notification) => {
|
||||
if (notification.id === action.id) {
|
||||
return {
|
||||
...notification,
|
||||
...action.updatedData,
|
||||
}
|
||||
}
|
||||
|
||||
return notification
|
||||
})
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const NotificationContext = createContext<NotificationContextType | null>(null)
|
||||
|
||||
export type NotificationProviderProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const NotificationProvider = ({
|
||||
children,
|
||||
}: NotificationProviderProps) => {
|
||||
const [notifications, dispatch] = useReducer(notificationReducer, [])
|
||||
|
||||
const generateId = () => uuid()
|
||||
|
||||
const addNotification = (notification: NotificationItemType) => {
|
||||
if (!notification.id) {
|
||||
notification.id = generateId()
|
||||
}
|
||||
dispatch({
|
||||
type: NotificationReducerActionTypes.ADD,
|
||||
notification,
|
||||
})
|
||||
}
|
||||
|
||||
const updateNotification = (
|
||||
id: string,
|
||||
updatedData: Partial<Omit<NotificationItemType, "id">>
|
||||
) => {
|
||||
dispatch({
|
||||
type: NotificationReducerActionTypes.UPDATE,
|
||||
id,
|
||||
updatedData,
|
||||
})
|
||||
}
|
||||
|
||||
const removeNotification = (id: string) => {
|
||||
dispatch({
|
||||
type: NotificationReducerActionTypes.REMOVE,
|
||||
id,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider
|
||||
value={{
|
||||
notifications,
|
||||
addNotification,
|
||||
generateId,
|
||||
removeNotification,
|
||||
updateNotification,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<NotificationContainer />
|
||||
</NotificationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useNotifications = (): NotificationContextType => {
|
||||
const context = useContext(NotificationContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useNotifications must be used within a NotificationProvider"
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useContext, useState } from "react"
|
||||
|
||||
export type PageLoadingContextType = {
|
||||
isLoading: boolean
|
||||
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const PageLoadingContext = createContext<PageLoadingContextType | null>(null)
|
||||
|
||||
export type PageLoadingProviderProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const PageLoadingProvider = ({ children }: PageLoadingProviderProps) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
return (
|
||||
<PageLoadingContext.Provider
|
||||
value={{
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PageLoadingContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePageLoading = (): PageLoadingContextType => {
|
||||
const context = useContext(PageLoadingContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("usePageLoading must be used inside a PageLoadingProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
} from "react"
|
||||
import { SearchModal, SearchModalProps } from "@/components"
|
||||
import { checkArraySameElms } from "../../utils"
|
||||
import algoliasearch, { SearchClient } from "algoliasearch/lite"
|
||||
|
||||
export type SearchContextType = {
|
||||
isOpen: boolean
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
defaultFilters: string[]
|
||||
setDefaultFilters: (value: string[]) => void
|
||||
searchClient: SearchClient
|
||||
}
|
||||
|
||||
const SearchContext = createContext<SearchContextType | null>(null)
|
||||
|
||||
export type AlgoliaProps = {
|
||||
appId: string
|
||||
apiKey: string
|
||||
mainIndexName: string
|
||||
indices: string[]
|
||||
}
|
||||
|
||||
export type SearchProviderProps = {
|
||||
children: React.ReactNode
|
||||
initialDefaultFilters?: string[]
|
||||
algolia: AlgoliaProps
|
||||
searchProps: Omit<SearchModalProps, "algolia">
|
||||
}
|
||||
|
||||
export const SearchProvider = ({
|
||||
children,
|
||||
initialDefaultFilters = [],
|
||||
searchProps,
|
||||
algolia,
|
||||
}: SearchProviderProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [defaultFilters, setDefaultFilters] = useState<string[]>(
|
||||
initialDefaultFilters
|
||||
)
|
||||
|
||||
const searchClient: SearchClient = useMemo(() => {
|
||||
const algoliaClient = algoliasearch(algolia.appId, algolia.apiKey)
|
||||
return {
|
||||
...algoliaClient,
|
||||
async search(requests) {
|
||||
if (requests.every(({ params }) => !params?.query)) {
|
||||
return Promise.resolve({
|
||||
results: requests.map(() => ({
|
||||
hits: [],
|
||||
nbHits: 0,
|
||||
nbPages: 0,
|
||||
page: 0,
|
||||
processingTimeMS: 0,
|
||||
hitsPerPage: 0,
|
||||
exhaustiveNbHits: false,
|
||||
query: "",
|
||||
params: "",
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
return algoliaClient.search(requests)
|
||||
},
|
||||
}
|
||||
}, [algolia.appId, algolia.apiKey])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
initialDefaultFilters.length &&
|
||||
!checkArraySameElms(defaultFilters, initialDefaultFilters)
|
||||
) {
|
||||
setDefaultFilters(initialDefaultFilters)
|
||||
}
|
||||
}, [initialDefaultFilters])
|
||||
|
||||
return (
|
||||
<SearchContext.Provider
|
||||
value={{
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
defaultFilters,
|
||||
setDefaultFilters,
|
||||
searchClient,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<SearchModal {...searchProps} algolia={algolia} />
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useSearch = (): SearchContextType => {
|
||||
const context = useContext(SearchContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useSearch must be used inside a SearchProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
"use client"
|
||||
|
||||
import React, {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useReducer,
|
||||
useState,
|
||||
} from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
export enum SidebarItemSections {
|
||||
TOP = "top",
|
||||
BOTTOM = "bottom",
|
||||
MOBILE = "mobile",
|
||||
}
|
||||
|
||||
export type SidebarItemType = {
|
||||
path?: string
|
||||
title: string
|
||||
additionalElms?: React.ReactNode
|
||||
children?: SidebarItemType[]
|
||||
loaded?: boolean
|
||||
isPathHref?: boolean
|
||||
}
|
||||
|
||||
export type SidebarSectionItemsType = {
|
||||
[k in SidebarItemSections]: SidebarItemType[]
|
||||
}
|
||||
|
||||
export type SidebarContextType = {
|
||||
items: SidebarSectionItemsType
|
||||
activePath: string | null
|
||||
getActiveItem: () => SidebarItemType | undefined
|
||||
setActivePath: (path: string | null) => void
|
||||
isItemActive: (item: SidebarItemType, checkChildren?: boolean) => boolean
|
||||
addItems: (
|
||||
item: SidebarItemType[],
|
||||
options?: {
|
||||
section?: SidebarItemSections
|
||||
parent?: {
|
||||
path: string
|
||||
changeLoaded?: boolean
|
||||
}
|
||||
indexPosition?: number
|
||||
ignoreExisting?: boolean
|
||||
}
|
||||
) => void
|
||||
findItemInSection: (
|
||||
section: SidebarItemType[],
|
||||
item: Partial<SidebarItemType>,
|
||||
checkChildren?: boolean
|
||||
) => SidebarItemType | undefined
|
||||
mobileSidebarOpen: boolean
|
||||
setMobileSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
isSidebarEmpty: () => boolean
|
||||
desktopSidebarOpen: boolean
|
||||
setDesktopSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const SidebarContext = createContext<SidebarContextType | null>(null)
|
||||
|
||||
export type ActionOptionsType = {
|
||||
section?: SidebarItemSections
|
||||
parent?: {
|
||||
path: string
|
||||
changeLoaded?: boolean
|
||||
}
|
||||
indexPosition?: number
|
||||
ignoreExisting?: boolean
|
||||
}
|
||||
|
||||
export type ActionType = {
|
||||
type: "add" | "update"
|
||||
items: SidebarItemType[]
|
||||
options?: ActionOptionsType
|
||||
}
|
||||
|
||||
export const reducer = (
|
||||
state: SidebarSectionItemsType,
|
||||
{ type, items, options }: ActionType
|
||||
) => {
|
||||
const {
|
||||
section = SidebarItemSections.TOP,
|
||||
parent,
|
||||
indexPosition,
|
||||
} = options || {}
|
||||
|
||||
switch (type) {
|
||||
case "add":
|
||||
return {
|
||||
...state,
|
||||
[section]:
|
||||
indexPosition !== undefined
|
||||
? [
|
||||
...state[section].slice(0, indexPosition),
|
||||
...items,
|
||||
...state[section].slice(indexPosition),
|
||||
]
|
||||
: [...state[section], ...items],
|
||||
}
|
||||
case "update":
|
||||
// find item index
|
||||
return {
|
||||
...state,
|
||||
[section]: state[section].map((i) => {
|
||||
if (i.path && parent?.path && i.path === parent?.path) {
|
||||
return {
|
||||
...i,
|
||||
children: [...(i.children || []), ...items],
|
||||
loaded: parent.changeLoaded ? true : i.loaded,
|
||||
}
|
||||
}
|
||||
return i
|
||||
}),
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export type SidebarProviderProps = {
|
||||
children?: ReactNode
|
||||
isLoading?: boolean
|
||||
setIsLoading?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
initialItems?: SidebarSectionItemsType
|
||||
shouldHandleHashChange?: boolean
|
||||
shouldHandlePathChange?: boolean
|
||||
}
|
||||
|
||||
export const SidebarProvider = ({
|
||||
children,
|
||||
isLoading = false,
|
||||
setIsLoading,
|
||||
initialItems,
|
||||
shouldHandleHashChange = false,
|
||||
shouldHandlePathChange = false,
|
||||
}: SidebarProviderProps) => {
|
||||
const [items, dispatch] = useReducer(reducer, {
|
||||
top: initialItems?.top || [],
|
||||
bottom: initialItems?.bottom || [],
|
||||
mobile: initialItems?.mobile || [],
|
||||
})
|
||||
const [activePath, setActivePath] = useState<string | null>("")
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState<boolean>(false)
|
||||
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true)
|
||||
const pathname = usePathname()
|
||||
|
||||
const findItemInSection = useCallback(
|
||||
(
|
||||
section: SidebarItemType[],
|
||||
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 && findItemInSection(i.children, item))
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const getActiveItem = useCallback(() => {
|
||||
if (activePath === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return (
|
||||
findItemInSection(items.mobile, { path: activePath }) ||
|
||||
findItemInSection(items.top, { path: activePath }) ||
|
||||
findItemInSection(items.bottom, { path: activePath })
|
||||
)
|
||||
}, [activePath, items, findItemInSection])
|
||||
|
||||
const addItems = (
|
||||
newItems: SidebarItemType[],
|
||||
options?: {
|
||||
section?: SidebarItemSections
|
||||
parent?: {
|
||||
path: string
|
||||
changeLoaded?: boolean
|
||||
}
|
||||
indexPosition?: number
|
||||
ignoreExisting?: boolean
|
||||
}
|
||||
) => {
|
||||
const {
|
||||
section = SidebarItemSections.TOP,
|
||||
parent,
|
||||
ignoreExisting = false,
|
||||
} = options || {}
|
||||
|
||||
if (!ignoreExisting) {
|
||||
const selectedSection =
|
||||
section === SidebarItemSections.BOTTOM ? items.bottom : items.top
|
||||
newItems = newItems.filter(
|
||||
(item) => !findItemInSection(selectedSection, item)
|
||||
)
|
||||
}
|
||||
|
||||
if (!newItems.length) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: parent ? "update" : "add",
|
||||
items: newItems,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
const isItemActive = useCallback(
|
||||
(item: SidebarItemType, checkChildren = false): boolean => {
|
||||
return (
|
||||
item.path === activePath ||
|
||||
(checkChildren && activePath?.split("_")[0] === item.path)
|
||||
)
|
||||
},
|
||||
[activePath]
|
||||
)
|
||||
|
||||
const isSidebarEmpty = useCallback((): boolean => {
|
||||
return Object.values(items).every(
|
||||
(sectionItems) => sectionItems.length === 0
|
||||
)
|
||||
}, [items])
|
||||
|
||||
const init = () => {
|
||||
const currentPath = location.hash.replace("#", "")
|
||||
if (currentPath) {
|
||||
setActivePath(currentPath)
|
||||
}
|
||||
}
|
||||
|
||||
// this is mainly triggered by Algolia
|
||||
const handleHashChange = useCallback(() => {
|
||||
const currentPath = location.hash.replace("#", "")
|
||||
if (currentPath !== activePath) {
|
||||
setActivePath(currentPath)
|
||||
}
|
||||
}, [activePath])
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldHandleHashChange) {
|
||||
return
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY === 0) {
|
||||
setActivePath("")
|
||||
// can't use next router as it doesn't support
|
||||
// changing url without scrolling
|
||||
history.replaceState({}, "", location.pathname)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
window.addEventListener("hashchange", handleHashChange)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll)
|
||||
window.removeEventListener("hashchange", handleHashChange)
|
||||
}
|
||||
}, [handleHashChange, shouldHandleHashChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading && items.top.length && items.bottom.length) {
|
||||
setIsLoading?.(false)
|
||||
}
|
||||
}, [items, isLoading, setIsLoading])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldHandlePathChange && pathname !== activePath) {
|
||||
setActivePath(pathname)
|
||||
}
|
||||
}, [shouldHandlePathChange, pathname])
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider
|
||||
value={{
|
||||
items,
|
||||
addItems,
|
||||
activePath,
|
||||
setActivePath,
|
||||
isItemActive,
|
||||
findItemInSection,
|
||||
mobileSidebarOpen,
|
||||
setMobileSidebarOpen,
|
||||
isSidebarEmpty,
|
||||
getActiveItem,
|
||||
desktopSidebarOpen,
|
||||
setDesktopSidebarOpen,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useSidebar = (): SidebarContextType => {
|
||||
const context = useContext(SidebarContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used inside a SidebarProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "./Analytics"
|
||||
export * from "./ColorMode"
|
||||
export * from "./Mobile"
|
||||
export * from "./Modal"
|
||||
export * from "./Navbar"
|
||||
export * from "./Notification"
|
||||
export * from "./PageLoading"
|
||||
export * from "./Search"
|
||||
export * from "./Sidebar"
|
||||
Reference in New Issue
Block a user