mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
fix(page): Add item scroll, fix display issues, refactor nav, and improve perf (#166)
* feat: add item scroll to active * fix: reset enterkey and scroll to view * fix: link item displayName * refactor: remove keyboard page nav * chore: fix scrolling, perf, keys, highlight active item etc * chore: use new hook for create a page * chore: disabled auto delete page
This commit is contained in:
@@ -3,11 +3,13 @@ import { ensureUrlProtocol } from "@/lib/utils"
|
|||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { LaAccount, PersonalPage } from "@/lib/schema"
|
import { LaAccount } from "@/lib/schema"
|
||||||
|
import { usePageActions } from "@/components/routes/page/hooks/use-page-actions"
|
||||||
|
|
||||||
export const useCommandActions = () => {
|
export const useCommandActions = () => {
|
||||||
const { setTheme } = useTheme()
|
const { setTheme } = useTheme()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { newPage } = usePageActions()
|
||||||
|
|
||||||
const changeTheme = React.useCallback(
|
const changeTheme = React.useCallback(
|
||||||
(theme: string) => {
|
(theme: string) => {
|
||||||
@@ -35,19 +37,10 @@ export const useCommandActions = () => {
|
|||||||
|
|
||||||
const createNewPage = React.useCallback(
|
const createNewPage = React.useCallback(
|
||||||
(me: LaAccount) => {
|
(me: LaAccount) => {
|
||||||
try {
|
const page = newPage(me)
|
||||||
const newPersonalPage = PersonalPage.create(
|
router.push(`/pages/${page.id}`)
|
||||||
{ public: false, createdAt: new Date(), updatedAt: new Date() },
|
|
||||||
{ owner: me._owner }
|
|
||||||
)
|
|
||||||
|
|
||||||
me.root?.personalPages?.push(newPersonalPage)
|
|
||||||
router.push(`/pages/${newPersonalPage.id}`)
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to create page")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[router]
|
[router, newPage]
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { atomWithStorage } from "jotai/utils"
|
|||||||
import { PersonalPage, PersonalPageLists } from "@/lib/schema/personal-page"
|
import { PersonalPage, PersonalPageLists } from "@/lib/schema/personal-page"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { LaIcon } from "@/components/custom/la-icon"
|
import { LaIcon } from "@/components/custom/la-icon"
|
||||||
import { toast } from "sonner"
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { icons } from "lucide-react"
|
import { icons } from "lucide-react"
|
||||||
|
import { usePageActions } from "@/components/routes/page/hooks/use-page-actions"
|
||||||
|
|
||||||
type SortOption = "title" | "recent"
|
type SortOption = "title" | "recent"
|
||||||
type ShowOption = 5 | 10 | 15 | 20 | 0
|
type ShowOption = 5 | 10 | 15 | 20 | 0
|
||||||
@@ -101,20 +101,13 @@ const PageSectionHeader: React.FC<PageSectionHeaderProps> = ({ pageCount, isActi
|
|||||||
const NewPageButton: React.FC = () => {
|
const NewPageButton: React.FC = () => {
|
||||||
const { me } = useAccount()
|
const { me } = useAccount()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { newPage } = usePageActions()
|
||||||
|
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
try {
|
const page = newPage(me)
|
||||||
const newPersonalPage = PersonalPage.create(
|
router.push(`/pages/${page.id}`)
|
||||||
{ public: false, createdAt: new Date(), updatedAt: new Date() },
|
|
||||||
{ owner: me._owner }
|
|
||||||
)
|
|
||||||
me.root?.personalPages?.push(newPersonalPage)
|
|
||||||
router.push(`/pages/${newPersonalPage.id}`)
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to create page")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ import { usePageActions } from "../hooks/use-page-actions"
|
|||||||
|
|
||||||
const TITLE_PLACEHOLDER = "Untitled"
|
const TITLE_PLACEHOLDER = "Untitled"
|
||||||
|
|
||||||
const emptyPage = (page: PersonalPage): boolean => {
|
const isPageEmpty = (page: PersonalPage): boolean => {
|
||||||
return (!page.title || page.title.trim() === "") && (!page.content || Object.keys(page.content).length === 0)
|
return (!page.title || page.title.trim() === "") && (!page.content || Object.keys(page.content).length === 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteEmptyPage = (currentPageId: string | null) => {
|
const useDeleteEmptyPage = (currentPageId: string | null) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { me } = useAccount({
|
const { me } = useAccount({
|
||||||
root: {
|
root: {
|
||||||
@@ -36,21 +36,17 @@ export const DeleteEmptyPage = (currentPageId: string | null) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRouteChange = () => {
|
return () => {
|
||||||
if (!currentPageId || !me?.root?.personalPages) return
|
if (!currentPageId || !me?.root?.personalPages) return
|
||||||
|
|
||||||
const currentPage = me.root.personalPages.find(page => page?.id === currentPageId)
|
const currentPage = me.root.personalPages.find(page => page?.id === currentPageId)
|
||||||
if (currentPage && emptyPage(currentPage)) {
|
if (currentPage && isPageEmpty(currentPage)) {
|
||||||
const index = me.root.personalPages.findIndex(page => page?.id === currentPageId)
|
const index = me.root.personalPages.findIndex(page => page?.id === currentPageId)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
me.root.personalPages.splice(index, 1)
|
me.root.personalPages.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
|
||||||
handleRouteChange()
|
|
||||||
}
|
|
||||||
}, [currentPageId, me, router])
|
}, [currentPageId, me, router])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +58,9 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
|
|||||||
const { deletePage } = usePageActions()
|
const { deletePage } = usePageActions()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
|
|
||||||
DeleteEmptyPage(pageId)
|
// useDeleteEmptyPage(pageId)
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = useCallback(async () => {
|
||||||
const result = await confirm({
|
const result = await confirm({
|
||||||
title: "Delete page",
|
title: "Delete page",
|
||||||
description: "Are you sure you want to delete this page?",
|
description: "Are you sure you want to delete this page?",
|
||||||
@@ -78,7 +74,7 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
|
|||||||
deletePage(me, pageId as ID<PersonalPage>)
|
deletePage(me, pageId as ID<PersonalPage>)
|
||||||
router.push("/pages")
|
router.push("/pages")
|
||||||
}
|
}
|
||||||
}
|
}, [confirm, deletePage, me, pageId, router])
|
||||||
|
|
||||||
if (!page) return null
|
if (!page) return null
|
||||||
|
|
||||||
@@ -132,29 +128,32 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
|||||||
const isContentInitialMount = useRef(true)
|
const isContentInitialMount = useRef(true)
|
||||||
const isInitialFocusApplied = useRef(false)
|
const isInitialFocusApplied = useRef(false)
|
||||||
|
|
||||||
const updatePageContent = (content: Content, model: PersonalPage) => {
|
const updatePageContent = useCallback((content: Content, model: PersonalPage) => {
|
||||||
if (isContentInitialMount.current) {
|
if (isContentInitialMount.current) {
|
||||||
isContentInitialMount.current = false
|
isContentInitialMount.current = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
model.content = content
|
model.content = content
|
||||||
model.updatedAt = new Date()
|
model.updatedAt = new Date()
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const handleUpdateTitle = (editor: Editor) => {
|
const handleUpdateTitle = useCallback(
|
||||||
if (isTitleInitialMount.current) {
|
(editor: Editor) => {
|
||||||
isTitleInitialMount.current = false
|
if (isTitleInitialMount.current) {
|
||||||
return
|
isTitleInitialMount.current = false
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const newTitle = editor.getText()
|
const newTitle = editor.getText()
|
||||||
if (newTitle !== page.title) {
|
if (newTitle !== page.title) {
|
||||||
const slug = generateUniqueSlug(page.title?.toString() || "")
|
const slug = generateUniqueSlug(page.title?.toString() || "")
|
||||||
page.title = newTitle
|
page.title = newTitle
|
||||||
page.slug = slug
|
page.slug = slug
|
||||||
page.updatedAt = new Date()
|
page.updatedAt = new Date()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[page]
|
||||||
|
)
|
||||||
|
|
||||||
const handleTitleKeyDown = useCallback((view: EditorView, event: KeyboardEvent) => {
|
const handleTitleKeyDown = useCallback((view: EditorView, event: KeyboardEvent) => {
|
||||||
const editor = titleEditorRef.current
|
const editor = titleEditorRef.current
|
||||||
@@ -254,7 +253,7 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
|||||||
contentEditorRef.current.editor.commands.focus()
|
contentEditorRef.current.editor.commands.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [page.title, titleEditor, contentEditorRef])
|
}, [page.title, titleEditor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex grow flex-col overflow-y-auto [scrollbar-gutter:stable]">
|
<div className="relative flex grow flex-col overflow-y-auto [scrollbar-gutter:stable]">
|
||||||
|
|||||||
@@ -1,54 +1,58 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
|
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
|
||||||
import { LaIcon } from "@/components/custom/la-icon"
|
import { LaIcon } from "@/components/custom/la-icon"
|
||||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||||
import { useRouter } from "next/navigation"
|
import { usePageActions } from "./hooks/use-page-actions"
|
||||||
import { PersonalPage } from "@/lib/schema"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
|
|
||||||
export const PageHeader = React.memo(() => {
|
interface PageHeaderProps {}
|
||||||
|
|
||||||
|
export const PageHeader: React.FC<PageHeaderProps> = React.memo(() => {
|
||||||
const { me } = useAccount()
|
const { me } = useAccount()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { newPage } = usePageActions()
|
||||||
|
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleNewPageClick = () => {
|
||||||
try {
|
const page = newPage(me)
|
||||||
const newPersonalPage = PersonalPage.create(
|
router.push(`/pages/${page.id}`)
|
||||||
{ public: false, createdAt: new Date(), updatedAt: new Date() },
|
|
||||||
{ owner: me._owner }
|
|
||||||
)
|
|
||||||
me.root?.personalPages?.push(newPersonalPage)
|
|
||||||
router.push(`/pages/${newPersonalPage.id}`)
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to create page")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentHeader className="px-6 py-5 max-lg:px-4">
|
<ContentHeader className="px-6 py-4 max-lg:px-4">
|
||||||
<div className="flex min-w-0 shrink-0 items-center gap-1.5">
|
<HeaderTitle />
|
||||||
<SidebarToggleButton />
|
<div className="flex flex-auto" />
|
||||||
<div className="flex min-h-0 items-center">
|
<NewPageButton onClick={handleNewPageClick} />
|
||||||
<span className="truncate text-left font-bold lg:text-xl">Pages</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-auto"></div>
|
|
||||||
|
|
||||||
<div className="flex w-auto items-center justify-end">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button size="sm" type="button" variant="secondary" className="gap-x-2" onClick={handleClick}>
|
|
||||||
<LaIcon name="Plus" />
|
|
||||||
<span className="hidden md:block">New page</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ContentHeader>
|
</ContentHeader>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
PageHeader.displayName = "PageHeader"
|
PageHeader.displayName = "PageHeader"
|
||||||
|
|
||||||
|
const HeaderTitle: React.FC = () => (
|
||||||
|
<div className="flex min-w-0 shrink-0 items-center gap-1.5">
|
||||||
|
<SidebarToggleButton />
|
||||||
|
<div className="flex min-h-0 items-center">
|
||||||
|
<span className="truncate text-left font-bold lg:text-xl">Pages</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface NewPageButtonProps {
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const NewPageButton: React.FC<NewPageButtonProps> = ({ onClick }) => (
|
||||||
|
<div className="flex w-auto items-center justify-end">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button size="sm" type="button" variant="secondary" className="gap-x-2" onClick={onClick}>
|
||||||
|
<LaIcon name="Plus" />
|
||||||
|
<span className="hidden md:block">New page</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useCallback } from "react"
|
|
||||||
import { PersonalPage, PersonalPageLists } from "@/lib/schema"
|
|
||||||
|
|
||||||
interface UseKeyboardNavigationProps {
|
|
||||||
personalPages?: PersonalPageLists | null
|
|
||||||
activeItemIndex: number | null
|
|
||||||
setActiveItemIndex: React.Dispatch<React.SetStateAction<number | null>>
|
|
||||||
isCommandPaletteOpen: boolean
|
|
||||||
disableEnterKey: boolean
|
|
||||||
onEnter?: (selectedPage: PersonalPage) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useKeyboardNavigation = ({
|
|
||||||
personalPages,
|
|
||||||
activeItemIndex,
|
|
||||||
setActiveItemIndex,
|
|
||||||
isCommandPaletteOpen,
|
|
||||||
disableEnterKey,
|
|
||||||
onEnter
|
|
||||||
}: UseKeyboardNavigationProps) => {
|
|
||||||
const listRef = useRef<HTMLDivElement>(null)
|
|
||||||
const itemRefs = useRef<(HTMLAnchorElement | null)[]>([])
|
|
||||||
const itemCount = personalPages?.length || 0
|
|
||||||
|
|
||||||
const scrollIntoView = useCallback((index: number) => {
|
|
||||||
if (itemRefs.current[index]) {
|
|
||||||
itemRefs.current[index]?.scrollIntoView({
|
|
||||||
block: "nearest"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeItemIndex !== null) {
|
|
||||||
scrollIntoView(activeItemIndex)
|
|
||||||
}
|
|
||||||
}, [activeItemIndex, scrollIntoView])
|
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
|
||||||
(e: KeyboardEvent) => {
|
|
||||||
if (isCommandPaletteOpen) return
|
|
||||||
|
|
||||||
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
||||||
e.preventDefault()
|
|
||||||
setActiveItemIndex(prevIndex => {
|
|
||||||
if (prevIndex === null) return 0
|
|
||||||
const newIndex = e.key === "ArrowUp" ? (prevIndex - 1 + itemCount) % itemCount : (prevIndex + 1) % itemCount
|
|
||||||
return newIndex
|
|
||||||
})
|
|
||||||
} else if (e.key === "Enter" && !disableEnterKey && activeItemIndex !== null && personalPages) {
|
|
||||||
e.preventDefault()
|
|
||||||
const selectedPage = personalPages[activeItemIndex]
|
|
||||||
if (selectedPage) onEnter?.(selectedPage)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalPages, onEnter]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
||||||
}, [handleKeyDown])
|
|
||||||
|
|
||||||
const setItemRef = useCallback((el: HTMLAnchorElement | null, index: number) => {
|
|
||||||
itemRefs.current[index] = el
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return { listRef, setItemRef }
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,15 @@ import { LaAccount, PersonalPage } from "@/lib/schema"
|
|||||||
import { ID } from "jazz-tools"
|
import { ID } from "jazz-tools"
|
||||||
|
|
||||||
export const usePageActions = () => {
|
export const usePageActions = () => {
|
||||||
|
const newPage = useCallback((me: LaAccount): PersonalPage => {
|
||||||
|
const newPersonalPage = PersonalPage.create(
|
||||||
|
{ public: false, createdAt: new Date(), updatedAt: new Date() },
|
||||||
|
{ owner: me._owner }
|
||||||
|
)
|
||||||
|
me.root?.personalPages?.push(newPersonalPage)
|
||||||
|
return newPersonalPage
|
||||||
|
}, [])
|
||||||
|
|
||||||
const deletePage = useCallback((me: LaAccount, pageId: ID<PersonalPage>): void => {
|
const deletePage = useCallback((me: LaAccount, pageId: ID<PersonalPage>): void => {
|
||||||
if (!me.root?.personalPages) return
|
if (!me.root?.personalPages) return
|
||||||
|
|
||||||
@@ -32,5 +41,5 @@ export const usePageActions = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return { deletePage }
|
return { newPage, deletePage }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import React, { useMemo, useCallback } from "react"
|
import React, { useMemo, useCallback, useEffect } from "react"
|
||||||
import { Primitive } from "@radix-ui/react-primitive"
|
import { Primitive } from "@radix-ui/react-primitive"
|
||||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||||
import { useAtom } from "jotai"
|
import { useAtom } from "jotai"
|
||||||
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
|
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
|
||||||
import { PageItem } from "./partials/page-item"
|
import { PageItem } from "./partials/page-item"
|
||||||
import { useKeyboardNavigation } from "./hooks/use-keyboard-navigation"
|
|
||||||
import { useMedia } from "react-use"
|
import { useMedia } from "react-use"
|
||||||
import { Column } from "./partials/column"
|
import { Column } from "./partials/column"
|
||||||
import { useColumnStyles } from "./hooks/use-column-styles"
|
import { useColumnStyles } from "./hooks/use-column-styles"
|
||||||
import { PersonalPage, PersonalPageLists } from "@/lib/schema"
|
import { PersonalPage, PersonalPageLists } from "@/lib/schema"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useActiveItemScroll } from "@/hooks/use-active-item-scroll"
|
||||||
|
|
||||||
interface PageListProps {
|
interface PageListProps {
|
||||||
activeItemIndex: number | null
|
activeItemIndex: number | null
|
||||||
@@ -23,6 +23,7 @@ export const PageList: React.FC<PageListProps> = ({ activeItemIndex, setActiveIt
|
|||||||
const { me } = useAccount({ root: { personalPages: [] } })
|
const { me } = useAccount({ root: { personalPages: [] } })
|
||||||
const personalPages = useMemo(() => me?.root?.personalPages, [me?.root?.personalPages])
|
const personalPages = useMemo(() => me?.root?.personalPages, [me?.root?.personalPages])
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const itemCount = personalPages?.length || 0
|
||||||
|
|
||||||
const handleEnter = useCallback(
|
const handleEnter = useCallback(
|
||||||
(selectedPage: PersonalPage) => {
|
(selectedPage: PersonalPage) => {
|
||||||
@@ -31,24 +32,35 @@ export const PageList: React.FC<PageListProps> = ({ activeItemIndex, setActiveIt
|
|||||||
[router]
|
[router]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { listRef, setItemRef } = useKeyboardNavigation({
|
const handleKeyDown = useCallback(
|
||||||
personalPages,
|
(e: KeyboardEvent) => {
|
||||||
activeItemIndex,
|
if (isCommandPaletteOpen) return
|
||||||
setActiveItemIndex,
|
|
||||||
isCommandPaletteOpen,
|
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
||||||
disableEnterKey,
|
e.preventDefault()
|
||||||
onEnter: handleEnter
|
setActiveItemIndex(prevIndex => {
|
||||||
})
|
if (prevIndex === null) return 0
|
||||||
|
const newIndex = e.key === "ArrowUp" ? (prevIndex - 1 + itemCount) % itemCount : (prevIndex + 1) % itemCount
|
||||||
|
return newIndex
|
||||||
|
})
|
||||||
|
} else if (e.key === "Enter" && !disableEnterKey && activeItemIndex !== null && personalPages) {
|
||||||
|
e.preventDefault()
|
||||||
|
const selectedPage = personalPages[activeItemIndex]
|
||||||
|
if (selectedPage) handleEnter?.(selectedPage)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalPages, handleEnter]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("keydown", handleKeyDown)
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||||
|
}, [handleKeyDown])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<div className="flex h-full w-full flex-col overflow-hidden border-t">
|
||||||
{!isTablet && <ColumnHeader />}
|
{!isTablet && <ColumnHeader />}
|
||||||
<PageListItems
|
<PageListItems personalPages={personalPages} activeItemIndex={activeItemIndex} />
|
||||||
listRef={listRef}
|
|
||||||
setItemRef={setItemRef}
|
|
||||||
personalPages={personalPages}
|
|
||||||
activeItemIndex={activeItemIndex}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -72,29 +84,30 @@ export const ColumnHeader: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PageListItemsProps {
|
interface PageListItemsProps {
|
||||||
listRef: React.RefObject<HTMLDivElement>
|
|
||||||
setItemRef: (el: HTMLAnchorElement | null, index: number) => void
|
|
||||||
personalPages?: PersonalPageLists | null
|
personalPages?: PersonalPageLists | null
|
||||||
activeItemIndex: number | null
|
activeItemIndex: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageListItems: React.FC<PageListItemsProps> = ({ listRef, setItemRef, personalPages, activeItemIndex }) => (
|
const PageListItems: React.FC<PageListItemsProps> = ({ personalPages, activeItemIndex }) => {
|
||||||
<Primitive.div
|
const setElementRef = useActiveItemScroll<HTMLAnchorElement>({ activeIndex: activeItemIndex })
|
||||||
ref={listRef}
|
|
||||||
className="divide-primary/5 mx-auto my-2 flex w-[99%] flex-1 flex-col divide-y overflow-y-auto outline-none [scrollbar-gutter:stable]"
|
return (
|
||||||
tabIndex={-1}
|
<Primitive.div
|
||||||
role="list"
|
className="divide-primary/5 flex flex-1 flex-col divide-y overflow-y-auto outline-none [scrollbar-gutter:stable]"
|
||||||
>
|
tabIndex={-1}
|
||||||
{personalPages?.map(
|
role="list"
|
||||||
(page, index) =>
|
>
|
||||||
page?.id && (
|
{personalPages?.map(
|
||||||
<PageItem
|
(page, index) =>
|
||||||
key={page.id}
|
page?.id && (
|
||||||
ref={(el: HTMLAnchorElement | null) => setItemRef(el, index)}
|
<PageItem
|
||||||
page={page}
|
key={page.id}
|
||||||
isActive={index === activeItemIndex}
|
ref={el => setElementRef(el, index)}
|
||||||
/>
|
page={page}
|
||||||
)
|
isActive={index === activeItemIndex}
|
||||||
)}
|
/>
|
||||||
</Primitive.div>
|
)
|
||||||
)
|
)}
|
||||||
|
</Primitive.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,14 +21,10 @@ export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(({ pa
|
|||||||
<Link
|
<Link
|
||||||
ref={ref}
|
ref={ref}
|
||||||
tabIndex={isActive ? 0 : -1}
|
tabIndex={isActive ? 0 : -1}
|
||||||
className={cn(
|
className={cn("relative block cursor-default outline-none", "min-h-12 py-2 max-lg:px-4 sm:px-6", {
|
||||||
"relative block cursor-default rounded-lg outline-none",
|
"bg-muted-foreground/5": isActive,
|
||||||
"h-12 items-center gap-x-2 py-2 max-lg:px-4 sm:px-6",
|
"hover:bg-muted/50": !isActive
|
||||||
{
|
})}
|
||||||
"bg-muted-foreground/10": isActive,
|
|
||||||
"hover:bg-muted/50": !isActive
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
href={`/pages/${page.id}`}
|
href={`/pages/${page.id}`}
|
||||||
role="listitem"
|
role="listitem"
|
||||||
>
|
>
|
||||||
@@ -38,14 +34,9 @@ export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(({ pa
|
|||||||
</Column.Wrapper>
|
</Column.Wrapper>
|
||||||
|
|
||||||
{!isTablet && (
|
{!isTablet && (
|
||||||
<>
|
<Column.Wrapper style={columnStyles.topic}>
|
||||||
{/* <Column.Wrapper style={columnStyles.content}>
|
{page.topic && <Badge variant="secondary">{page.topic.prettyName}</Badge>}
|
||||||
<Column.Text className="text-[13px]">{page.slug}</Column.Text>
|
</Column.Wrapper>
|
||||||
</Column.Wrapper> */}
|
|
||||||
<Column.Wrapper style={columnStyles.topic}>
|
|
||||||
{page.topic && <Badge variant="secondary">{page.topic.prettyName}</Badge>}
|
|
||||||
</Column.Wrapper>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Column.Wrapper style={columnStyles.updated} className="flex justify-end">
|
<Column.Wrapper style={columnStyles.updated} className="flex justify-end">
|
||||||
|
|||||||
Reference in New Issue
Block a user