fix: topic selector virtual (#142)

This commit is contained in:
Aslam
2024-09-06 07:31:22 +07:00
committed by GitHub
parent 1c01a18c89
commit 2bebcbc20a
3 changed files with 72 additions and 97 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,9 +1,8 @@
"use client" "use client"
import * as React from "react" import React, { useCallback, useRef, useEffect } from "react"
import { ID } from "jazz-tools" import { ID } from "jazz-tools"
import { PersonalPage } from "@/lib/schema" import { PersonalPage } from "@/lib/schema"
import { useCallback, useRef, useEffect } from "react"
import { LAEditor, LAEditorRef } from "@/components/la-editor" import { LAEditor, LAEditorRef } from "@/components/la-editor"
import { Content, EditorContent, useEditor } from "@tiptap/react" import { Content, EditorContent, useEditor } from "@tiptap/react"
import { StarterKit } from "@/components/la-editor/extensions/starter-kit" import { StarterKit } from "@/components/la-editor/extensions/starter-kit"
@@ -29,7 +28,6 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
const isMobile = useMedia("(max-width: 770px)") const isMobile = useMedia("(max-width: 770px)")
const page = useCoState(PersonalPage, pageId as ID<PersonalPage>) const page = useCoState(PersonalPage, pageId as ID<PersonalPage>)
const router = useRouter() const router = useRouter()
const confirm = useConfirm() const confirm = useConfirm()
const handleDelete = async () => { const handleDelete = async () => {
@@ -38,17 +36,11 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
description: "Are you sure you want to delete this page?", description: "Are you sure you want to delete this page?",
confirmText: "Delete", confirmText: "Delete",
cancelText: "Cancel", cancelText: "Cancel",
cancelButton: { cancelButton: { variant: "outline" },
variant: "outline" confirmButton: { variant: "destructive" }
},
confirmButton: {
variant: "destructive"
}
}) })
if (result) { if (result && me?.root.personalPages) {
if (!me?.root.personalPages) return
try { try {
const index = me.root.personalPages.findIndex(item => item?.id === pageId) const index = me.root.personalPages.findIndex(item => item?.id === pageId)
if (index === -1) { if (index === -1) {
@@ -56,22 +48,11 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
return return
} }
toast.success("Page deleted.", {
position: "bottom-right",
description: (
<span>
<strong>{page?.title}</strong> has been deleted.
</span>
)
})
me.root.personalPages.splice(index, 1) me.root.personalPages.splice(index, 1)
toast.success("Page deleted.", { position: "bottom-right" })
// push without history
router.replace("/") router.replace("/")
} catch (error) { } catch (error) {
console.error("Delete operation fail", { error }) console.error("Delete operation fail", { error })
return
} }
} }
} }
@@ -82,48 +63,49 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
<div className="absolute inset-0 flex flex-row overflow-hidden"> <div className="absolute inset-0 flex flex-row overflow-hidden">
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<div className="relative flex min-w-0 grow basis-[760px] flex-col"> <div className="relative flex min-w-0 grow basis-[760px] flex-col">
<DetailPageHeader page={page} handleDelete={handleDelete} /> <DetailPageHeader page={page} handleDelete={handleDelete} isMobile={isMobile} />
<DetailPageForm page={page} /> <DetailPageForm page={page} />
</div> </div>
{!isMobile && <SidebarActions page={page} handleDelete={handleDelete} />}
{!isMobile && (
<div className="relative min-w-56 max-w-72 border-l">
<div className="flex">
<div className="flex h-10 flex-auto flex-row items-center justify-between px-5">
<span className="text-left text-[13px] font-medium">Page actions</span>
</div>
<div className="absolute bottom-0 left-0 right-0 top-10 space-y-3 overflow-y-auto px-4 py-1.5">
<TopicSelector
value={page.topic?.name}
onTopicChange={topic => {
page.topic = topic
page.updatedAt = new Date()
}}
variant="ghost"
className="-ml-1.5"
renderSelectedText={() => (
<span className="truncate">{page.topic?.prettyName || "Select a topic"}</span>
)}
/>
<Button size="sm" variant="ghost" onClick={handleDelete} className="-ml-1.5">
<LaIcon name="Trash" className="mr-2 size-3.5" />
<span className="text-sm">Delete</span>
</Button>
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
) )
} }
export const DetailPageForm = ({ page }: { page: PersonalPage }) => { const SidebarActions = ({ page, handleDelete }: { page: PersonalPage; handleDelete: () => void }) => (
<div className="relative min-w-56 max-w-72 border-l">
<div className="flex">
<div className="flex h-10 flex-auto flex-row items-center justify-between px-5">
<span className="text-left text-[13px] font-medium">Page actions</span>
</div>
<div className="absolute bottom-0 left-0 right-0 top-10 space-y-3 overflow-y-auto px-4 py-1.5">
<div className="flex flex-row">
<TopicSelector
value={page.topic?.name}
onTopicChange={topic => {
page.topic = topic
page.updatedAt = new Date()
}}
variant="ghost"
className="-ml-1.5"
renderSelectedText={() => <span className="truncate">{page.topic?.prettyName || "Select a topic"}</span>}
/>
</div>
<div className="flex flex-row">
<Button size="sm" variant="ghost" onClick={handleDelete} className="-ml-1.5">
<LaIcon name="Trash" className="mr-2 size-3.5" />
<span className="text-sm">Delete</span>
</Button>
</div>
</div>
</div>
</div>
)
const DetailPageForm = ({ page }: { page: PersonalPage }) => {
const { me } = useAccount() const { me } = useAccount()
const titleEditorRef = useRef<Editor | null>(null) const titleEditorRef = useRef<Editor | null>(null)
const contentEditorRef = useRef<LAEditorRef>(null) const contentEditorRef = useRef<LAEditorRef>(null)
const isTitleInitialMount = useRef(true) const isTitleInitialMount = useRef(true)
const isContentInitialMount = useRef(true) const isContentInitialMount = useRef(true)
@@ -132,7 +114,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
isContentInitialMount.current = false isContentInitialMount.current = false
return return
} }
model.content = content model.content = content
model.updatedAt = new Date() model.updatedAt = new Date()
} }
@@ -143,13 +124,10 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
return return
} }
const personalPages = me?.root?.personalPages?.toJSON() || []
const newTitle = editor.getText() const newTitle = editor.getText()
// Only update if the title has actually changed
if (newTitle !== page.title) { if (newTitle !== page.title) {
const personalPages = me?.root?.personalPages?.toJSON() || []
const slug = generateUniqueSlug(personalPages, page.slug || "") const slug = generateUniqueSlug(personalPages, page.slug || "")
page.title = newTitle page.title = newTitle
page.slug = slug page.slug = slug
page.updatedAt = new Date() page.updatedAt = new Date()
@@ -181,7 +159,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
} }
break break
} }
return false return false
}, []) }, [])
@@ -198,7 +175,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
titleEditorRef.current?.commands.focus("end") titleEditorRef.current?.commands.focus("end")
return true return true
} }
return false return false
}, []) }, [])
@@ -217,9 +193,7 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
strike: false, strike: false,
focus: false, focus: false,
gapcursor: false, gapcursor: false,
placeholder: { placeholder: { placeholder: TITLE_PLACEHOLDER }
placeholder: TITLE_PLACEHOLDER
}
}) })
], ],
editorProps: { editorProps: {

View File

@@ -1,42 +1,43 @@
"use client" import React from "react"
import * as React from "react"
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header" import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
import { PersonalPage } from "@/lib/schema/personal-page" import { PersonalPage } from "@/lib/schema/personal-page"
import { useMedia } from "react-use"
import { TopicSelector } from "@/components/custom/topic-selector" import { TopicSelector } from "@/components/custom/topic-selector"
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"
export const DetailPageHeader = ({ page, handleDelete }: { page: PersonalPage; handleDelete: () => void }) => { interface DetailPageHeaderProps {
const isMobile = useMedia("(max-width: 770px)") page: PersonalPage
handleDelete: () => void
isMobile: boolean
}
export const DetailPageHeader: React.FC<DetailPageHeaderProps> = ({ page, handleDelete, isMobile }) => {
if (!isMobile) return null
return ( return (
isMobile && ( <>
<> <ContentHeader className="lg:min-h-0">
<ContentHeader className="lg:min-h-0"> <div className="flex min-w-0 gap-2">
<div className="flex min-w-0 gap-2"> <SidebarToggleButton />
<SidebarToggleButton />
</div>
</ContentHeader>
<div className="flex flex-row items-start gap-1.5 border-b px-6 py-2 max-lg:pl-4">
<TopicSelector
value={page.topic?.name}
onTopicChange={topic => {
page.topic = topic
page.updatedAt = new Date()
}}
align="start"
variant="outline"
renderSelectedText={() => <span className="truncate">{page.topic?.prettyName || "Select a topic"}</span>}
/>
<Button size="sm" variant="outline" onClick={handleDelete}>
<LaIcon name="Trash" className="mr-2 size-3.5" />
Delete
</Button>
</div> </div>
</> </ContentHeader>
)
<div className="flex flex-row items-start gap-1.5 border-b px-6 py-2 max-lg:pl-4">
<TopicSelector
value={page.topic?.name}
onTopicChange={topic => {
page.topic = topic
page.updatedAt = new Date()
}}
align="start"
variant="outline"
renderSelectedText={() => <span className="truncate">{page.topic?.prettyName || "Select a topic"}</span>}
/>
<Button size="sm" variant="outline" onClick={handleDelete}>
<LaIcon name="Trash" className="mr-2 size-3.5" />
Delete
</Button>
</div>
</>
) )
} }