mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
fix: topic selector virtual (#142)
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import React, { useCallback, useRef, useEffect } from "react"
|
||||
import { ID } from "jazz-tools"
|
||||
import { PersonalPage } from "@/lib/schema"
|
||||
import { useCallback, useRef, useEffect } from "react"
|
||||
import { LAEditor, LAEditorRef } from "@/components/la-editor"
|
||||
import { Content, EditorContent, useEditor } from "@tiptap/react"
|
||||
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 page = useCoState(PersonalPage, pageId as ID<PersonalPage>)
|
||||
const router = useRouter()
|
||||
|
||||
const confirm = useConfirm()
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -38,17 +36,11 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
|
||||
description: "Are you sure you want to delete this page?",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
cancelButton: {
|
||||
variant: "outline"
|
||||
},
|
||||
confirmButton: {
|
||||
variant: "destructive"
|
||||
}
|
||||
cancelButton: { variant: "outline" },
|
||||
confirmButton: { variant: "destructive" }
|
||||
})
|
||||
|
||||
if (result) {
|
||||
if (!me?.root.personalPages) return
|
||||
|
||||
if (result && me?.root.personalPages) {
|
||||
try {
|
||||
const index = me.root.personalPages.findIndex(item => item?.id === pageId)
|
||||
if (index === -1) {
|
||||
@@ -56,22 +48,11 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
|
||||
return
|
||||
}
|
||||
|
||||
toast.success("Page deleted.", {
|
||||
position: "bottom-right",
|
||||
description: (
|
||||
<span>
|
||||
<strong>{page?.title}</strong> has been deleted.
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
me.root.personalPages.splice(index, 1)
|
||||
|
||||
// push without history
|
||||
toast.success("Page deleted.", { position: "bottom-right" })
|
||||
router.replace("/")
|
||||
} catch (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="flex h-full w-full">
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
{!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>
|
||||
)}
|
||||
{!isMobile && <SidebarActions page={page} handleDelete={handleDelete} />}
|
||||
</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 titleEditorRef = useRef<Editor | null>(null)
|
||||
const contentEditorRef = useRef<LAEditorRef>(null)
|
||||
|
||||
const isTitleInitialMount = useRef(true)
|
||||
const isContentInitialMount = useRef(true)
|
||||
|
||||
@@ -132,7 +114,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
isContentInitialMount.current = false
|
||||
return
|
||||
}
|
||||
|
||||
model.content = content
|
||||
model.updatedAt = new Date()
|
||||
}
|
||||
@@ -143,13 +124,10 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
return
|
||||
}
|
||||
|
||||
const personalPages = me?.root?.personalPages?.toJSON() || []
|
||||
const newTitle = editor.getText()
|
||||
|
||||
// Only update if the title has actually changed
|
||||
if (newTitle !== page.title) {
|
||||
const personalPages = me?.root?.personalPages?.toJSON() || []
|
||||
const slug = generateUniqueSlug(personalPages, page.slug || "")
|
||||
|
||||
page.title = newTitle
|
||||
page.slug = slug
|
||||
page.updatedAt = new Date()
|
||||
@@ -181,7 +159,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return false
|
||||
}, [])
|
||||
|
||||
@@ -198,7 +175,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
titleEditorRef.current?.commands.focus("end")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}, [])
|
||||
|
||||
@@ -217,9 +193,7 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
strike: false,
|
||||
focus: false,
|
||||
gapcursor: false,
|
||||
placeholder: {
|
||||
placeholder: TITLE_PLACEHOLDER
|
||||
}
|
||||
placeholder: { placeholder: TITLE_PLACEHOLDER }
|
||||
})
|
||||
],
|
||||
editorProps: {
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import React from "react"
|
||||
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
|
||||
import { PersonalPage } from "@/lib/schema/personal-page"
|
||||
import { useMedia } from "react-use"
|
||||
import { TopicSelector } from "@/components/custom/topic-selector"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
|
||||
export const DetailPageHeader = ({ page, handleDelete }: { page: PersonalPage; handleDelete: () => void }) => {
|
||||
const isMobile = useMedia("(max-width: 770px)")
|
||||
interface DetailPageHeaderProps {
|
||||
page: PersonalPage
|
||||
handleDelete: () => void
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
export const DetailPageHeader: React.FC<DetailPageHeaderProps> = ({ page, handleDelete, isMobile }) => {
|
||||
if (!isMobile) return null
|
||||
|
||||
return (
|
||||
isMobile && (
|
||||
<>
|
||||
<ContentHeader className="lg:min-h-0">
|
||||
<div className="flex min-w-0 gap-2">
|
||||
<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>
|
||||
<>
|
||||
<ContentHeader className="lg:min-h-0">
|
||||
<div className="flex min-w-0 gap-2">
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user