import { useRouter } from 'next/router' import { NodeApi, SimpleTree, Tree, TreeApi } from 'react-arborist' import useResizeObserver from 'use-resize-observer' import { Shortcut, useGetShortcuts, useSetShortcuts, } from '../../lib/networking/shortcuts/useShortcuts' import { usePersistedState } from '../../lib/hooks/usePersistedState' import { CSSProperties, useCallback, useMemo, useState } from 'react' import { v4 as uuidv4 } from 'uuid' import { Box, HStack, SpanBox } from '../elements/LayoutPrimitives' import { Dropdown, DropdownOption } from '../elements/DropdownElements' import { DotsThree, ListMagnifyingGlass, Tag } from '@phosphor-icons/react' import { ShortcutFolderClosed } from '../elements/icons/ShortcutFolderClosed' import { theme } from '../tokens/stitches.config' import { ShortcutFolderOpen } from '../elements/icons/ShortcutFolderOpen' import { CoverImage } from '../elements/CoverImage' import { NewsletterIcon } from '../elements/icons/NewsletterIcon' import { FollowingIcon } from '../elements/icons/FollowingIcon' import { StyledText } from '../elements/StyledText' import { OpenMap } from 'react-arborist/dist/module/state/open-slice' type ShortcutsTreeProps = { treeRef: React.MutableRefObject | undefined> } export const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => { const router = useRouter() const { ref, width, height } = useResizeObserver() const { data, isLoading } = useGetShortcuts() const setShorcuts = useSetShortcuts() const [folderOpenState, setFolderOpenState] = usePersistedState< Record >({ key: 'nav-menu-open-state', isSessionStorage: false, initialValue: {}, }) const tree = useMemo(() => { const result = new SimpleTree((data ?? []) as Shortcut[]) return result }, [data]) const syncTreeData = async (data: Shortcut[]) => { await setShorcuts.mutateAsync({ shortcuts: data }) } const onMove = useCallback( async (args: { dragIds: string[] parentId: null | string index: number }) => { for (const id of args.dragIds) { tree?.move({ id, parentId: args.parentId, index: args.index }) } await syncTreeData(tree.data) }, [tree, data] ) const onCreate = useCallback( async (args: { parentId: string | null; index: number; type: string }) => { const data = { id: uuidv4(), name: '', type: 'folder' } as any if (args.type === 'internal') { data.children = [] } tree.create({ parentId: args.parentId, index: args.index, data }) await syncTreeData(tree.data) return data }, [tree, data] ) const onDelete = useCallback( async (args: { ids: string[] }) => { args.ids.forEach((id) => tree.drop({ id })) await syncTreeData(tree.data) }, [tree, data] ) const onRename = useCallback( async (args: { name: string; id: string }) => { tree.update({ id: args.id, changes: { name: args.name } as any }) await syncTreeData(tree.data) }, [tree, data] ) const onToggle = useCallback( (id: string) => { if (id && props.treeRef.current) { const isOpen = props.treeRef.current?.isOpen(id) const newItem: OpenMap = {} newItem[id] = isOpen setFolderOpenState({ ...folderOpenState, ...newItem }) } }, [props, folderOpenState, setFolderOpenState] ) const onActivate = useCallback( (node: NodeApi) => { if (node.data.type == 'folder') { const join = node.data.join if (join == 'or') { const query = node.children ?.map((child) => { return `(${child.data.filter})` }) .join(' OR ') } } else if (node.data.section != null && node.data.filter != null) { router.push(`/${node.data.section}?q=${node.data.filter}`) } }, [tree, router] ) function countTotalShortcuts(shortcuts: Shortcut[]): number { let total = 0 for (const shortcut of shortcuts) { // Count the current shortcut total++ // If the shortcut has children, recursively count them if (shortcut.children && shortcut.children.length > 0) { total += countTotalShortcuts(shortcut.children) } } return total } const maximumHeight = useMemo(() => { if (!data) { return 320 } return countTotalShortcuts(data as Shortcut[]) * 36 }, [data]) return ( {!isLoading && ( {NodeRenderer} )} ) } function NodeRenderer(args: { style: CSSProperties node: NodeApi tree: TreeApi dragHandle?: (el: HTMLDivElement | null) => void preview?: boolean }) { const isSelected = false const [menuVisible, setMenuVisible] = useState(false) const [menuOpened, setMenuOpened] = useState(false) return ( { setMenuVisible(true) }} onMouseLeave={() => { setMenuVisible(false) }} title={args.node.data.name} onClick={(e) => { // router.push(`/` + props.section) }} > } css={{ ml: 'auto' }} onOpenChange={(open) => { setMenuOpened(open) }} > { args.tree.delete(args.node) }} title="Remove" /> {/* {args.node.data.type == 'folder' && ( { args.node.data.join = 'or' }} title="Folder query: OR" /> )} */} ) } type NodeItemContentsProps = { node: NodeApi } const NodeItemContents = (props: NodeItemContentsProps): JSX.Element => { if (props.node.isEditing) { return ( e.currentTarget.select()} onBlur={() => props.node.reset()} onKeyDown={(e) => { if (e.key === 'Escape') { props.node.reset() } if (e.key === 'Enter') { // props.node.data = { // id: 'new-folder', // type: 'folder', // name: e.currentTarget.value, // } props.node.submit(e.currentTarget.value) props.node.activate() } }} /> ) } if (props.node.isLeaf) { const shortcut = props.node.data if (shortcut) { switch (shortcut.type) { case 'feed': case 'newsletter': return ( ) case 'label': return ( ) case 'search': return ( ) } } } else { return ( { props.node.toggle() event.preventDefault() }} > {props.node.isClosed ? ( ) : ( )} {props.node.data.name} ) } return <> } type ShortcutItemProps = { shortcut: Shortcut } const FeedOrNewsletterShortcut = (props: ShortcutItemProps): JSX.Element => { return ( {props.shortcut.icon ? ( ) : props.shortcut.type == 'newsletter' ? ( ) : ( )} {props.shortcut.name} ) } const SearchShortcut = (props: ShortcutItemProps): JSX.Element => { return ( {props.shortcut.name} ) } const LabelShortcut = (props: ShortcutItemProps): JSX.Element => { return ( {props.shortcut.name} ) }