diff --git a/web/components/custom/command-palette/command-palette.tsx b/web/components/custom/command-palette/command-palette.tsx index 4986d557..e6a45aa9 100644 --- a/web/components/custom/command-palette/command-palette.tsx +++ b/web/components/custom/command-palette/command-palette.tsx @@ -10,18 +10,21 @@ import { useAccount } from "@/lib/providers/jazz-provider" import { searchSafeRegExp, toTitleCase } from "@/lib/utils" import { GraphNode } from "@/components/routes/public/PublicHomeRoute" import { useCommandActions } from "./hooks/use-command-actions" +import { atom, useAtom } from "jotai" let graph_data_promise = import("@/components/routes/public/graph-data.json").then(a => a.default) const filterItems = (items: CommandItemType[], searchRegex: RegExp) => items.filter(item => searchRegex.test(item.value)).slice(0, 6) +export const commandPaletteOpenAtom = atom(false) + export function CommandPalette() { const { me } = useAccount({ root: { personalLinks: [], personalPages: [] } }) const dialogRef = React.useRef(null) const [inputValue, setInputValue] = React.useState("") const [activePage, setActivePage] = React.useState("home") - const [open, setOpen] = React.useState(false) + const [open, setOpen] = useAtom(commandPaletteOpenAtom) const actions = useCommandActions() const commandGroups = React.useMemo(() => me && createCommandGroups(actions, me), [actions, me]) @@ -38,7 +41,7 @@ export function CommandPalette() { document.addEventListener("keydown", down) return () => document.removeEventListener("keydown", down) - }, []) + }, [setOpen]) const bounce = React.useCallback(() => { if (dialogRef.current) { @@ -177,7 +180,7 @@ export function CommandPalette() { closeDialog() } }, - [bounce] + [bounce, setOpen] ) const filteredCommands = React.useMemo(() => getFilteredCommands(), [getFilteredCommands]) diff --git a/web/components/routes/link/LinkRoute.tsx b/web/components/routes/link/LinkRoute.tsx index 1505b45d..bd88d6ab 100644 --- a/web/components/routes/link/LinkRoute.tsx +++ b/web/components/routes/link/LinkRoute.tsx @@ -4,7 +4,7 @@ import { LinkHeader } from "@/components/routes/link/header" import { LinkList } from "@/components/routes/link/list" import { LinkManage } from "@/components/routes/link/manage" import { useQueryState } from "nuqs" -import { useEffect } from "react" +import { useEffect, useState } from "react" import { useAtom } from "jotai" import { linkEditIdAtom } from "@/store/link" import { LinkBottomBar } from "./bottom-bar" @@ -12,6 +12,7 @@ import { LinkBottomBar } from "./bottom-bar" export function LinkRoute() { const [, setEditId] = useAtom(linkEditIdAtom) const [nuqsEditId] = useQueryState("editId") + const [activeItemIndex, setActiveItemIndex] = useState(null) useEffect(() => { setEditId(nuqsEditId) @@ -21,7 +22,7 @@ export function LinkRoute() {
- +
) diff --git a/web/components/routes/link/list.tsx b/web/components/routes/link/list.tsx index 0173a8cb..3de70727 100644 --- a/web/components/routes/link/list.tsx +++ b/web/components/routes/link/list.tsx @@ -1,5 +1,4 @@ -"use client" - +import React, { useCallback, useEffect, useMemo } from "react" import { DndContext, closestCenter, @@ -11,6 +10,7 @@ import { DragStartEvent, UniqueIdentifier } from "@dnd-kit/core" +import { Primitive } from "@radix-ui/react-primitive" import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable" import { useAccount } from "@/lib/providers/jazz-provider" import { PersonalLinkLists } from "@/lib/schema/personal-link" @@ -18,15 +18,20 @@ import { useAtom } from "jotai" import { linkSortAtom } from "@/store/link" import { useKey } from "react-use" import { LinkItem } from "./partials/link-item" -import { useRef, useState, useCallback, useEffect, useMemo } from "react" -import { learningStateAtom } from "./header" import { useQueryState } from "nuqs" +import { learningStateAtom } from "./header" +import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette" -interface LinkListProps {} +interface LinkListProps { + activeItemIndex: number | null + setActiveItemIndex: React.Dispatch> +} -const LinkList: React.FC = () => { +const LinkList: React.FC = ({ activeItemIndex, setActiveItemIndex }) => { + const [isCommandPalettePpen] = useAtom(commandPaletteOpenAtom) const [editId, setEditId] = useQueryState("editId") const [activeLearningState] = useAtom(learningStateAtom) + const [draggingId, setDraggingId] = React.useState(null) const { me } = useAccount({ root: { personalLinks: [] } @@ -34,9 +39,6 @@ const LinkList: React.FC = () => { const personalLinks = useMemo(() => me?.root?.personalLinks || [], [me?.root?.personalLinks]) const [sort] = useAtom(linkSortAtom) - const [focusedId, setFocusedId] = useState(null) - const [draggingId, setDraggingId] = useState(null) - const linkRefs = useRef<{ [key: string]: HTMLLIElement | null }>({}) const filteredLinks = useMemo( () => @@ -67,16 +69,22 @@ const LinkList: React.FC = () => { }) ) - const registerRef = useCallback((id: string, ref: HTMLLIElement | null) => { - linkRefs.current[id] = ref - }, []) - useKey("Escape", () => { if (editId) { setEditId(null) } }) + // on mounted, if editId is set, set activeItemIndex to the index of the item with the editId + useEffect(() => { + if (editId) { + const index = sortedLinks.findIndex(link => link?.id === editId) + if (index !== -1) { + setActiveItemIndex(index) + } + } + }, [editId, sortedLinks, setActiveItemIndex]) + const updateSequences = useCallback((links: PersonalLinkLists) => { links.forEach((link, index) => { if (link) { @@ -87,57 +95,56 @@ const LinkList: React.FC = () => { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (!me?.root?.personalLinks || sortedLinks.length === 0 || editId !== null) return - - const currentIndex = sortedLinks.findIndex(link => link?.id === focusedId) + if (isCommandPalettePpen || !me?.root?.personalLinks || sortedLinks.length === 0 || editId !== null) return if (e.key === "ArrowUp" || e.key === "ArrowDown") { e.preventDefault() - const newIndex = - e.key === "ArrowUp" ? Math.max(0, currentIndex - 1) : Math.min(sortedLinks.length - 1, currentIndex + 1) + setActiveItemIndex(prevIndex => { + if (prevIndex === null) return 0 + const newIndex = + e.key === "ArrowUp" ? Math.max(0, prevIndex - 1) : Math.min(sortedLinks.length - 1, prevIndex + 1) - if (e.metaKey && sort === "manual") { - const currentLink = me.root.personalLinks[currentIndex] - if (!currentLink) return + if (e.metaKey && sort === "manual") { + const linksArray = [...me.root.personalLinks] + const newLinks = arrayMove(linksArray, prevIndex, newIndex) - const linksArray = [...me.root.personalLinks] - const newLinks = arrayMove(linksArray, currentIndex, newIndex) - - while (me.root.personalLinks.length > 0) { - me.root.personalLinks.pop() - } - - newLinks.forEach(link => { - if (link) { - me.root.personalLinks.push(link) + while (me.root.personalLinks.length > 0) { + me.root.personalLinks.pop() } - }) - updateSequences(me.root.personalLinks) - - const newFocusedLink = me.root.personalLinks[newIndex] - if (newFocusedLink) { - setFocusedId(newFocusedLink.id) - - requestAnimationFrame(() => { - linkRefs.current[newFocusedLink.id]?.focus() - }) - } - } else { - const newFocusedLink = sortedLinks[newIndex] - if (newFocusedLink) { - setFocusedId(newFocusedLink.id) - requestAnimationFrame(() => { - linkRefs.current[newFocusedLink.id]?.focus() + newLinks.forEach(link => { + if (link) { + me.root.personalLinks.push(link) + } }) + + updateSequences(me.root.personalLinks) } + + return newIndex + }) + } else if (e.key === "Enter" && activeItemIndex !== null) { + e.preventDefault() + const activeLink = sortedLinks[activeItemIndex] + if (activeLink) { + setEditId(activeLink.id) } } } window.addEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown) - }, [me?.root?.personalLinks, sortedLinks, focusedId, editId, sort, updateSequences]) + }, [ + me?.root?.personalLinks, + sortedLinks, + editId, + sort, + updateSequences, + isCommandPalettePpen, + activeItemIndex, + setEditId, + setActiveItemIndex + ]) const handleDragStart = useCallback( (event: DragStartEvent) => { @@ -185,6 +192,7 @@ const LinkList: React.FC = () => { }) updateSequences(me.root.personalLinks) + setActiveItemIndex(newIndex) } catch (error) { console.error("Error during link reordering:", error) } @@ -194,7 +202,10 @@ const LinkList: React.FC = () => { } return ( -
+ = () => { item?.id || "") || []} strategy={verticalListSortingStrategy}>
    {sortedLinks.map( - linkItem => + (linkItem, index) => linkItem && ( = () => { setEditId={setEditId} personalLink={linkItem} disabled={sort !== "manual" || editId !== null} - registerRef={registerRef} isDragging={draggingId === linkItem.id} - isFocused={focusedId === linkItem.id} - setFocusedId={setFocusedId} + isActive={activeItemIndex === index} + setActiveItemIndex={setActiveItemIndex} + index={index} /> ) )}
-
+ ) } diff --git a/web/components/routes/link/partials/link-item.tsx b/web/components/routes/link/partials/link-item.tsx index 6f1a0a09..ffab129e 100644 --- a/web/components/routes/link/partials/link-item.tsx +++ b/web/components/routes/link/partials/link-item.tsx @@ -1,5 +1,3 @@ -"use client" - import React, { useCallback, useMemo } from "react" import Image from "next/image" import Link from "next/link" @@ -15,7 +13,7 @@ import { PersonalLink } from "@/lib/schema/personal-link" import { LinkForm } from "./form/link-form" import { cn } from "@/lib/utils" import { LEARNING_STATES, LearningStateValue } from "@/lib/constants" -import { linkOpenPopoverForIdAtom, linkShowCreateAtom } from "@/store/link" +import { linkOpenPopoverForIdAtom } from "@/store/link" interface LinkItemProps { personalLink: PersonalLink @@ -23,9 +21,9 @@ interface LinkItemProps { isEditing: boolean setEditId: (id: string | null) => void isDragging: boolean - isFocused: boolean - setFocusedId: (id: string | null) => void - registerRef: (id: string, ref: HTMLLIElement | null) => void + isActive: boolean + setActiveItemIndex: (index: number | null) => void + index: number } export const LinkItem: React.FC = ({ @@ -34,9 +32,9 @@ export const LinkItem: React.FC = ({ personalLink, disabled = false, isDragging, - isFocused, - setFocusedId, - registerRef + isActive, + setActiveItemIndex, + index }) => { const [openPopoverForId, setOpenPopoverForId] = useAtom(linkOpenPopoverForIdAtom) const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled }) @@ -50,14 +48,6 @@ export const LinkItem: React.FC = ({ [transform, transition, isDragging] ) - const refCallback = useCallback( - (node: HTMLLIElement | null) => { - setNodeRef(node) - registerRef(personalLink.id, node) - }, - [setNodeRef, registerRef, personalLink.id] - ) - const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter") { @@ -92,90 +82,84 @@ export const LinkItem: React.FC = ({ return (
  • setFocusedId(personalLink.id)} - onBlur={() => setFocusedId(null)} + onFocus={() => setActiveItemIndex(index)} + onBlur={() => setActiveItemIndex(null)} onKeyDown={handleKeyDown} - className={cn("relative flex h-14 cursor-default items-center outline-none xl:h-11", { - "bg-muted-foreground/10": isFocused, - "hover:bg-muted/50": !isFocused - })} + className={cn( + "relative cursor-default outline-none", + "grid grid-cols-[auto_1fr_auto] items-center gap-x-2 px-2 py-2 sm:px-4 sm:py-2", + { + "bg-muted-foreground/10": isActive, + "hover:bg-muted/50": !isActive + } + )} onDoubleClick={handleRowDoubleClick} > -
    -
    - setOpenPopoverForId(open ? personalLink.id : null)} - > - - - - e.preventDefault()} - > - - - + setOpenPopoverForId(open ? personalLink.id : null)} + > + + + + e.preventDefault()} + > + + + - {personalLink.icon && ( - {personalLink.title} - )} -
    -
    -

    - {personalLink.title} -

    - {personalLink.url && ( -
    -
    - )} +
    + {personalLink.icon && ( + {personalLink.title} + )} +
    +

    {personalLink.title}

    + {personalLink.url && ( +
    +
    -
    + )}
    +
    -
    - {personalLink.topic && {personalLink.topic.prettyName}} -
    +
    + {personalLink.topic && {personalLink.topic.prettyName}}
  • )