diff --git a/web/components/custom/command-palette/hooks/use-command-actions.ts b/web/components/custom/command-palette/hooks/use-command-actions.ts index 453e365c..589d95d6 100644 --- a/web/components/custom/command-palette/hooks/use-command-actions.ts +++ b/web/components/custom/command-palette/hooks/use-command-actions.ts @@ -3,11 +3,13 @@ import { ensureUrlProtocol } from "@/lib/utils" import { useTheme } from "next-themes" import { toast } from "sonner" 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 = () => { const { setTheme } = useTheme() const router = useRouter() + const { newPage } = usePageActions() const changeTheme = React.useCallback( (theme: string) => { @@ -35,19 +37,10 @@ export const useCommandActions = () => { const createNewPage = React.useCallback( (me: LaAccount) => { - try { - const newPersonalPage = PersonalPage.create( - { 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") - } + const page = newPage(me) + router.push(`/pages/${page.id}`) }, - [router] + [router, newPage] ) return { diff --git a/web/components/custom/sidebar/partial/page-section.tsx b/web/components/custom/sidebar/partial/page-section.tsx index 4b9daf28..c5845c58 100644 --- a/web/components/custom/sidebar/partial/page-section.tsx +++ b/web/components/custom/sidebar/partial/page-section.tsx @@ -7,7 +7,6 @@ import { atomWithStorage } from "jotai/utils" import { PersonalPage, PersonalPageLists } from "@/lib/schema/personal-page" import { Button } from "@/components/ui/button" import { LaIcon } from "@/components/custom/la-icon" -import { toast } from "sonner" import Link from "next/link" import { DropdownMenu, @@ -21,6 +20,7 @@ import { DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { icons } from "lucide-react" +import { usePageActions } from "@/components/routes/page/hooks/use-page-actions" type SortOption = "title" | "recent" type ShowOption = 5 | 10 | 15 | 20 | 0 @@ -101,20 +101,13 @@ const PageSectionHeader: React.FC = ({ pageCount, isActi const NewPageButton: React.FC = () => { const { me } = useAccount() const router = useRouter() + const { newPage } = usePageActions() if (!me) return null const handleClick = () => { - try { - const newPersonalPage = PersonalPage.create( - { 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") - } + const page = newPage(me) + router.push(`/pages/${page.id}`) } return ( diff --git a/web/components/routes/page/detail/PageDetailRoute.tsx b/web/components/routes/page/detail/PageDetailRoute.tsx index ebeeb250..0c6467ff 100644 --- a/web/components/routes/page/detail/PageDetailRoute.tsx +++ b/web/components/routes/page/detail/PageDetailRoute.tsx @@ -23,11 +23,11 @@ import { usePageActions } from "../hooks/use-page-actions" 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) } -export const DeleteEmptyPage = (currentPageId: string | null) => { +const useDeleteEmptyPage = (currentPageId: string | null) => { const router = useRouter() const { me } = useAccount({ root: { @@ -36,21 +36,17 @@ export const DeleteEmptyPage = (currentPageId: string | null) => { }) useEffect(() => { - const handleRouteChange = () => { + return () => { if (!currentPageId || !me?.root?.personalPages) return 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) if (index !== -1) { me.root.personalPages.splice(index, 1) } } } - - return () => { - handleRouteChange() - } }, [currentPageId, me, router]) } @@ -62,9 +58,9 @@ export function PageDetailRoute({ pageId }: { pageId: string }) { const { deletePage } = usePageActions() const confirm = useConfirm() - DeleteEmptyPage(pageId) + // useDeleteEmptyPage(pageId) - const handleDelete = async () => { + const handleDelete = useCallback(async () => { const result = await confirm({ title: "Delete 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) router.push("/pages") } - } + }, [confirm, deletePage, me, pageId, router]) if (!page) return null @@ -132,29 +128,32 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => { const isContentInitialMount = useRef(true) const isInitialFocusApplied = useRef(false) - const updatePageContent = (content: Content, model: PersonalPage) => { + const updatePageContent = useCallback((content: Content, model: PersonalPage) => { if (isContentInitialMount.current) { isContentInitialMount.current = false return } model.content = content model.updatedAt = new Date() - } + }, []) - const handleUpdateTitle = (editor: Editor) => { - if (isTitleInitialMount.current) { - isTitleInitialMount.current = false - return - } + const handleUpdateTitle = useCallback( + (editor: Editor) => { + if (isTitleInitialMount.current) { + isTitleInitialMount.current = false + return + } - const newTitle = editor.getText() - if (newTitle !== page.title) { - const slug = generateUniqueSlug(page.title?.toString() || "") - page.title = newTitle - page.slug = slug - page.updatedAt = new Date() - } - } + const newTitle = editor.getText() + if (newTitle !== page.title) { + const slug = generateUniqueSlug(page.title?.toString() || "") + page.title = newTitle + page.slug = slug + page.updatedAt = new Date() + } + }, + [page] + ) const handleTitleKeyDown = useCallback((view: EditorView, event: KeyboardEvent) => { const editor = titleEditorRef.current @@ -254,7 +253,7 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => { contentEditorRef.current.editor.commands.focus() } } - }, [page.title, titleEditor, contentEditorRef]) + }, [page.title, titleEditor]) return (
diff --git a/web/components/routes/page/header.tsx b/web/components/routes/page/header.tsx index af1c1b22..93ea9664 100644 --- a/web/components/routes/page/header.tsx +++ b/web/components/routes/page/header.tsx @@ -1,54 +1,58 @@ "use client" import * as React from "react" +import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header" import { LaIcon } from "@/components/custom/la-icon" import { useAccount } from "@/lib/providers/jazz-provider" -import { useRouter } from "next/navigation" -import { PersonalPage } from "@/lib/schema" -import { toast } from "sonner" +import { usePageActions } from "./hooks/use-page-actions" -export const PageHeader = React.memo(() => { +interface PageHeaderProps {} + +export const PageHeader: React.FC = React.memo(() => { const { me } = useAccount() const router = useRouter() + const { newPage } = usePageActions() if (!me) return null - const handleClick = () => { - try { - const newPersonalPage = PersonalPage.create( - { 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") - } + const handleNewPageClick = () => { + const page = newPage(me) + router.push(`/pages/${page.id}`) } return ( - -
- -
- Pages -
-
- -
- -
-
- -
-
+ + +
+ ) }) PageHeader.displayName = "PageHeader" + +const HeaderTitle: React.FC = () => ( +
+ +
+ Pages +
+
+) + +interface NewPageButtonProps { + onClick: () => void +} + +const NewPageButton: React.FC = ({ onClick }) => ( +
+
+ +
+
+) diff --git a/web/components/routes/page/hooks/use-keyboard-navigation.ts b/web/components/routes/page/hooks/use-keyboard-navigation.ts deleted file mode 100644 index 3878b653..00000000 --- a/web/components/routes/page/hooks/use-keyboard-navigation.ts +++ /dev/null @@ -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> - isCommandPaletteOpen: boolean - disableEnterKey: boolean - onEnter?: (selectedPage: PersonalPage) => void -} - -export const useKeyboardNavigation = ({ - personalPages, - activeItemIndex, - setActiveItemIndex, - isCommandPaletteOpen, - disableEnterKey, - onEnter -}: UseKeyboardNavigationProps) => { - const listRef = useRef(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 } -} diff --git a/web/components/routes/page/hooks/use-page-actions.ts b/web/components/routes/page/hooks/use-page-actions.ts index 986ea3d8..65bc7276 100644 --- a/web/components/routes/page/hooks/use-page-actions.ts +++ b/web/components/routes/page/hooks/use-page-actions.ts @@ -4,6 +4,15 @@ import { LaAccount, PersonalPage } from "@/lib/schema" import { ID } from "jazz-tools" 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): void => { if (!me.root?.personalPages) return @@ -32,5 +41,5 @@ export const usePageActions = () => { } }, []) - return { deletePage } + return { newPage, deletePage } } diff --git a/web/components/routes/page/list.tsx b/web/components/routes/page/list.tsx index fb525661..c226559a 100644 --- a/web/components/routes/page/list.tsx +++ b/web/components/routes/page/list.tsx @@ -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 { useAccount } from "@/lib/providers/jazz-provider" import { useAtom } from "jotai" import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette" import { PageItem } from "./partials/page-item" -import { useKeyboardNavigation } from "./hooks/use-keyboard-navigation" import { useMedia } from "react-use" import { Column } from "./partials/column" import { useColumnStyles } from "./hooks/use-column-styles" import { PersonalPage, PersonalPageLists } from "@/lib/schema" import { useRouter } from "next/navigation" +import { useActiveItemScroll } from "@/hooks/use-active-item-scroll" interface PageListProps { activeItemIndex: number | null @@ -23,6 +23,7 @@ export const PageList: React.FC = ({ activeItemIndex, setActiveIt const { me } = useAccount({ root: { personalPages: [] } }) const personalPages = useMemo(() => me?.root?.personalPages, [me?.root?.personalPages]) const router = useRouter() + const itemCount = personalPages?.length || 0 const handleEnter = useCallback( (selectedPage: PersonalPage) => { @@ -31,24 +32,35 @@ export const PageList: React.FC = ({ activeItemIndex, setActiveIt [router] ) - const { listRef, setItemRef } = useKeyboardNavigation({ - personalPages, - activeItemIndex, - setActiveItemIndex, - isCommandPaletteOpen, - disableEnterKey, - onEnter: handleEnter - }) + 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) handleEnter?.(selectedPage) + } + }, + [itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalPages, handleEnter] + ) + + useEffect(() => { + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [handleKeyDown]) return ( -
+
{!isTablet && } - +
) } @@ -72,29 +84,30 @@ export const ColumnHeader: React.FC = () => { } interface PageListItemsProps { - listRef: React.RefObject - setItemRef: (el: HTMLAnchorElement | null, index: number) => void personalPages?: PersonalPageLists | null activeItemIndex: number | null } -const PageListItems: React.FC = ({ listRef, setItemRef, personalPages, activeItemIndex }) => ( - - {personalPages?.map( - (page, index) => - page?.id && ( - setItemRef(el, index)} - page={page} - isActive={index === activeItemIndex} - /> - ) - )} - -) +const PageListItems: React.FC = ({ personalPages, activeItemIndex }) => { + const setElementRef = useActiveItemScroll({ activeIndex: activeItemIndex }) + + return ( + + {personalPages?.map( + (page, index) => + page?.id && ( + setElementRef(el, index)} + page={page} + isActive={index === activeItemIndex} + /> + ) + )} + + ) +} diff --git a/web/components/routes/page/partials/page-item.tsx b/web/components/routes/page/partials/page-item.tsx index 8b8488b7..65dacf66 100644 --- a/web/components/routes/page/partials/page-item.tsx +++ b/web/components/routes/page/partials/page-item.tsx @@ -21,14 +21,10 @@ export const PageItem = React.forwardRef(({ pa @@ -38,14 +34,9 @@ export const PageItem = React.forwardRef(({ pa {!isTablet && ( - <> - {/* - {page.slug} - */} - - {page.topic && {page.topic.prettyName}} - - + + {page.topic && {page.topic.prettyName}} + )}