mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
fix: topic selector (#129)
* feat: add jazz globa group cons * chore: remove topic selector atom * chore: use jazz from constant * chore: remove delete model and add new topic selector * chore: use jazz group id form constant in search component * chore: use jazz group id form constant in public home route * fix: topic selector in link * fix: topic section in detail topic * chore: update la editor * chore: content header tweak class * chore: add btn variant to topic selector * refactor: tweak border for link header * chore: page header * fix: page detail route
This commit is contained in:
@@ -2,10 +2,9 @@
|
||||
import * as react from "react"
|
||||
import { useCoState } from "@/lib/providers/jazz-provider"
|
||||
import { PublicGlobalGroup } from "@/lib/schema/master/public-group"
|
||||
import { ID } from "jazz-tools"
|
||||
import dynamic from "next/dynamic"
|
||||
import { Button } from "../ui/button"
|
||||
import Link from "next/link"
|
||||
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
|
||||
|
||||
let graph_data_promise = import("./graph-data.json").then(a => a.default)
|
||||
const ForceGraphClient = dynamic(() => import("./force-graph-client-lazy"), { ssr: false })
|
||||
@@ -16,15 +15,11 @@ export function PublicHomeRoute() {
|
||||
const [placeholder, setPlaceholder] = react.useState("Search something...")
|
||||
const [currentTopicIndex, setCurrentTopicIndex] = react.useState(0)
|
||||
const [currentCharIndex, setCurrentCharIndex] = react.useState(0)
|
||||
const globalGroup = useCoState(
|
||||
PublicGlobalGroup,
|
||||
process.env.NEXT_PUBLIC_JAZZ_GLOBAL_GROUP as ID<PublicGlobalGroup>,
|
||||
{
|
||||
root: {
|
||||
topics: []
|
||||
}
|
||||
const globalGroup = useCoState(PublicGlobalGroup, JAZZ_GLOBAL_GROUP_ID, {
|
||||
root: {
|
||||
topics: []
|
||||
}
|
||||
)
|
||||
})
|
||||
const topics = globalGroup?.root.topics?.map(topic => topic?.prettyName) || []
|
||||
|
||||
react.useEffect(() => {
|
||||
|
||||
@@ -42,7 +42,7 @@ export const LinkHeader = React.memo(() => {
|
||||
</ContentHeader>
|
||||
|
||||
{isTablet && (
|
||||
<div className="border-b-primary/5 flex min-h-10 flex-row items-start justify-between border-b px-6 py-2 max-lg:pl-4">
|
||||
<div className="flex min-h-10 flex-row items-start justify-between border-b px-6 py-2 max-lg:pl-4">
|
||||
<LearningTab />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -12,12 +12,13 @@ import { UrlInput } from "./url-input"
|
||||
import { UrlBadge } from "./url-badge"
|
||||
import { TitleInput } from "./title-input"
|
||||
import { NotesSection } from "./notes-section"
|
||||
import { TopicSelector } from "./topic-selector"
|
||||
import { DescriptionInput } from "./description-input"
|
||||
import { atom, useAtom } from "jotai"
|
||||
import { linkLearningStateSelectorAtom, linkTopicSelectorAtom } from "@/store/link"
|
||||
import { linkLearningStateSelectorAtom } from "@/store/link"
|
||||
import { FormField, FormItem, FormLabel } from "@/components/ui/form"
|
||||
import { LearningStateSelector } from "@/components/custom/learning-state-selector"
|
||||
import { TopicSelector, topicSelectorAtom } from "@/components/custom/topic-selector"
|
||||
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
|
||||
|
||||
export const globalLinkFormExceptionRefsAtom = atom<React.RefObject<HTMLElement>[]>([])
|
||||
|
||||
@@ -47,8 +48,7 @@ export const LinkForm: React.FC<LinkFormProps> = ({
|
||||
onClose,
|
||||
exceptionsRefs = []
|
||||
}) => {
|
||||
const [selectedTopic, setSelectedTopic] = React.useState<Topic | undefined>()
|
||||
const [istopicSelectorOpen] = useAtom(linkTopicSelectorAtom)
|
||||
const [istopicSelectorOpen] = useAtom(topicSelectorAtom)
|
||||
const [islearningStateSelectorOpen] = useAtom(linkLearningStateSelectorAtom)
|
||||
const [globalExceptionRefs] = useAtom(globalLinkFormExceptionRefsAtom)
|
||||
|
||||
@@ -65,6 +65,14 @@ export const LinkForm: React.FC<LinkFormProps> = ({
|
||||
mode: "all"
|
||||
})
|
||||
|
||||
const topicName = form.watch("topic")
|
||||
const findTopic = React.useMemo(
|
||||
() => me && Topic.findUnique({ topicName }, JAZZ_GLOBAL_GROUP_ID, me),
|
||||
[topicName, me]
|
||||
)
|
||||
|
||||
const selectedTopic = useCoState(Topic, findTopic, {})
|
||||
|
||||
const allExceptionRefs = React.useMemo(
|
||||
() => [...exceptionsRefs, ...globalExceptionRefs],
|
||||
[exceptionsRefs, globalExceptionRefs]
|
||||
@@ -101,10 +109,11 @@ export const LinkForm: React.FC<LinkFormProps> = ({
|
||||
description: selectedLink.description,
|
||||
completed: selectedLink.completed,
|
||||
notes: selectedLink.notes,
|
||||
learningState: selectedLink.learningState
|
||||
learningState: selectedLink.learningState,
|
||||
topic: selectedLink.topic?.name
|
||||
})
|
||||
}
|
||||
}, [selectedLink, form])
|
||||
}, [selectedLink, selectedLink?.topic, form])
|
||||
|
||||
const fetchMetadata = async (url: string) => {
|
||||
setIsFetching(true)
|
||||
@@ -214,7 +223,20 @@ export const LinkForm: React.FC<LinkFormProps> = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<TopicSelector onSelect={topic => setSelectedTopic(topic)} />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="topic"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-0">
|
||||
<FormLabel className="sr-only">Topic</FormLabel>
|
||||
<TopicSelector
|
||||
{...field}
|
||||
renderSelectedText={() => <span>{selectedTopic?.prettyName || "Select a topic"}</span>}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -222,17 +244,7 @@ export const LinkForm: React.FC<LinkFormProps> = ({
|
||||
<UrlBadge urlFetched={urlFetched} handleResetUrl={handleResetUrl} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-row items-center justify-between gap-2 rounded-b-md border-t px-3 py-2"
|
||||
onClick={e => {
|
||||
if (!(e.target as HTMLElement).closest("button")) {
|
||||
const notesInput = e.currentTarget.querySelector("input")
|
||||
if (notesInput) {
|
||||
notesInput.focus()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between gap-2 rounded-b-md border-t px-3 py-2">
|
||||
<NotesSection />
|
||||
|
||||
{isFetching ? (
|
||||
|
||||
@@ -13,7 +13,7 @@ export const NotesSection: React.FC = () => {
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative space-y-0">
|
||||
<FormItem className="relative grow space-y-0">
|
||||
<FormLabel className="sr-only">Note</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Command, CommandInput, CommandList, CommandItem, CommandGroup } from "@/components/ui/command"
|
||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useAtom } from "jotai"
|
||||
import { linkTopicSelectorAtom } from "@/store/link"
|
||||
import { LinkFormValues } from "./schema"
|
||||
import { useCoState } from "@/lib/providers/jazz-provider"
|
||||
import { PublicGlobalGroup } from "@/lib/schema/master/public-group"
|
||||
import { ID } from "jazz-tools"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
import { Topic } from "@/lib/schema"
|
||||
|
||||
interface TopicSelectorProps {
|
||||
onSelect?: (value: Topic) => void
|
||||
}
|
||||
|
||||
export const TopicSelector: React.FC<TopicSelectorProps> = ({ onSelect }) => {
|
||||
const globalGroup = useCoState(
|
||||
PublicGlobalGroup,
|
||||
process.env.NEXT_PUBLIC_JAZZ_GLOBAL_GROUP as ID<PublicGlobalGroup>,
|
||||
{
|
||||
root: {
|
||||
topics: []
|
||||
}
|
||||
}
|
||||
)
|
||||
const [isTopicSelectorOpen, setIsTopicSelectorOpen] = useAtom(linkTopicSelectorAtom)
|
||||
const form = useFormContext<LinkFormValues>()
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
const topic = globalGroup?.root.topics.find(topic => topic?.name === value)
|
||||
if (topic) {
|
||||
onSelect?.(topic)
|
||||
form?.setValue("topic", value)
|
||||
}
|
||||
setIsTopicSelectorOpen(false)
|
||||
}
|
||||
|
||||
const selectedValue = form ? form.watch("topic") : null
|
||||
|
||||
return (
|
||||
<Popover open={isTopicSelectorOpen} onOpenChange={setIsTopicSelectorOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button size="sm" type="button" role="combobox" variant="secondary" className="gap-x-2 text-sm">
|
||||
<span className="truncate">
|
||||
{selectedValue
|
||||
? globalGroup?.root.topics.find(topic => topic?.id && topic.name === selectedValue)?.prettyName
|
||||
: "Topic"}
|
||||
</span>
|
||||
<LaIcon name="ChevronDown" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="z-50 w-52 rounded-lg p-0"
|
||||
side="bottom"
|
||||
align="end"
|
||||
onCloseAutoFocus={e => e.preventDefault()}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search topic..." className="h-9" />
|
||||
<CommandList>
|
||||
<ScrollArea>
|
||||
<CommandGroup>
|
||||
{globalGroup?.root.topics.map(
|
||||
topic =>
|
||||
topic?.id && (
|
||||
<CommandItem key={topic.id} value={topic.name} onSelect={handleSelect}>
|
||||
{topic.prettyName}
|
||||
<CheckIcon
|
||||
size={16}
|
||||
className={cn(
|
||||
"absolute right-3",
|
||||
topic.name === selectedValue ? "text-primary" : "text-transparent"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useAtom } from "jotai"
|
||||
import { ID } from "jazz-tools"
|
||||
import { PersonalPage, Topic } from "@/lib/schema"
|
||||
import { useCallback, useRef, useEffect, useState } from "react"
|
||||
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"
|
||||
@@ -13,26 +12,46 @@ import { useAccount, useCoState } from "@/lib/providers/jazz-provider"
|
||||
import { EditorView } from "@tiptap/pm/view"
|
||||
import { Editor } from "@tiptap/core"
|
||||
import { generateUniqueSlug } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
import { pageTopicSelectorAtom } from "@/store/page"
|
||||
import { TopicSelector } from "@/components/routes/link/partials/form/topic-selector"
|
||||
import { FocusClasses } from "@tiptap/extension-focus"
|
||||
import DeletePageModal from "@/components/custom/delete-modal"
|
||||
import { DetailPageHeader } from "./header"
|
||||
import { useMedia } from "react-use"
|
||||
import TopicSelector from "@/components/custom/topic-selector"
|
||||
|
||||
const TITLE_PLACEHOLDER = "Untitled"
|
||||
|
||||
export function PageDetailRoute({ pageId }: { pageId: string }) {
|
||||
const isMobile = useMedia("(max-width: 770px)")
|
||||
const page = useCoState(PersonalPage, pageId as ID<PersonalPage>)
|
||||
|
||||
if (!page) return <div>Loading...</div>
|
||||
if (!page) return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<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} />
|
||||
<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-4">
|
||||
<span className="text-left text-sm font-medium">Page actions</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 top-10 overflow-y-auto px-4 py-1.5">
|
||||
<TopicSelector
|
||||
value={page.topic?.name}
|
||||
onTopicChange={topic => {
|
||||
page.topic = topic
|
||||
page.updatedAt = new Date()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -42,9 +61,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
const { me } = useAccount()
|
||||
const titleEditorRef = useRef<Editor | null>(null)
|
||||
const contentEditorRef = useRef<LAEditorRef>(null)
|
||||
const [, setTopicSelectorOpen] = useAtom(pageTopicSelectorAtom)
|
||||
const [, setSelectedPageTopic] = useState<Topic | null>(page.topic || null)
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||
|
||||
const isTitleInitialMount = useRef(true)
|
||||
const isContentInitialMount = useRef(true)
|
||||
@@ -55,7 +71,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Updating page content")
|
||||
model.content = content
|
||||
model.updatedAt = new Date()
|
||||
}
|
||||
@@ -66,22 +81,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
* The logic changed, but we keep this commented code for reference
|
||||
*/
|
||||
// const newTitle = editor.getText().trim()
|
||||
|
||||
// if (!newTitle) {
|
||||
// toast.error("Update failed", {
|
||||
// description: "Title must be longer than or equal to 1 character"
|
||||
// })
|
||||
// editor.commands.setContent(page.title || "")
|
||||
// return
|
||||
// }
|
||||
|
||||
// if (newTitle === page.title) return
|
||||
|
||||
console.log("Updating page title")
|
||||
const personalPages = me?.root?.personalPages?.toJSON() || []
|
||||
const slug = generateUniqueSlug(personalPages, page.slug || "")
|
||||
|
||||
@@ -101,29 +100,44 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
const { selection } = state
|
||||
const { $anchor } = selection
|
||||
|
||||
if ((event.key === "ArrowLeft" || event.key === "ArrowUp") && $anchor.pos - 1 === 0) {
|
||||
event.preventDefault()
|
||||
titleEditorRef.current?.commands.focus("end")
|
||||
return true
|
||||
switch (event.key) {
|
||||
case "ArrowRight":
|
||||
case "ArrowDown":
|
||||
if ($anchor.pos === state.doc.content.size - 1) {
|
||||
event.preventDefault()
|
||||
contentEditorRef.current?.editor?.commands.focus("start")
|
||||
return true
|
||||
}
|
||||
break
|
||||
case "Enter":
|
||||
if (!event.shiftKey) {
|
||||
event.preventDefault()
|
||||
contentEditorRef.current?.editor?.commands.focus("start")
|
||||
return true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return false
|
||||
}, [])
|
||||
|
||||
const handleContentKeyDown = useCallback((view: EditorView, event: KeyboardEvent) => {
|
||||
const editor = contentEditorRef.current?.editor
|
||||
if (!editor) return false
|
||||
|
||||
const { state } = editor
|
||||
const { selection } = state
|
||||
const { $anchor } = selection
|
||||
|
||||
if ((event.key === "ArrowLeft" || event.key === "ArrowUp") && $anchor.pos - 1 === 0) {
|
||||
event.preventDefault()
|
||||
titleEditorRef.current?.commands.focus("end")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}, [])
|
||||
|
||||
const confirmDelete = (page: PersonalPage) => {
|
||||
console.log("Deleting page:", page.id)
|
||||
setDeleteModalOpen(false)
|
||||
//TODO: add delete logic
|
||||
}
|
||||
|
||||
const titleEditor = useEditor({
|
||||
immediatelyRender: false,
|
||||
autofocus: true,
|
||||
@@ -139,7 +153,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
strike: false,
|
||||
focus: false,
|
||||
gapcursor: false,
|
||||
history: false,
|
||||
placeholder: {
|
||||
placeholder: TITLE_PLACEHOLDER
|
||||
}
|
||||
@@ -152,7 +165,8 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
"aria-readonly": "false",
|
||||
"aria-multiline": "false",
|
||||
"aria-label": TITLE_PLACEHOLDER,
|
||||
translate: "no"
|
||||
translate: "no",
|
||||
class: "focus:outline-none"
|
||||
},
|
||||
handleKeyDown: handleTitleKeyDown
|
||||
},
|
||||
@@ -160,9 +174,7 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
if (page.title) editor.commands.setContent(`<p>${page.title}</p>`)
|
||||
},
|
||||
onBlur: ({ editor }) => handleUpdateTitle(editor),
|
||||
onUpdate: ({ editor }) => {
|
||||
handleUpdateTitle(editor)
|
||||
}
|
||||
onUpdate: ({ editor }) => handleUpdateTitle(editor)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -177,37 +189,20 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div tabIndex={0} className="relative flex grow flex-col overflow-y-auto">
|
||||
<div className="relative mx-auto flex h-full w-[calc(100%-40px)] shrink-0 grow flex-col sm:w-[calc(100%-80px)]">
|
||||
<div className="relative flex grow flex-col overflow-y-auto [scrollbar-gutter:stable]">
|
||||
<div className="relative mx-auto flex h-full w-[calc(100%-80px)] shrink-0 grow flex-col max-lg:w-[calc(100%-40px)] max-lg:max-w-[unset]">
|
||||
<form className="flex shrink-0 flex-col">
|
||||
<div className="mb-2 mt-8 flex flex-row justify-between py-1.5">
|
||||
<div className="mb-2 mt-8 py-1.5">
|
||||
<EditorContent
|
||||
editor={titleEditor}
|
||||
className="la-editor no-command grow cursor-text select-text text-2xl font-semibold leading-[calc(1.33333)] tracking-[-0.00625rem]"
|
||||
/>
|
||||
<div className="items-center space-x-4">
|
||||
<TopicSelector
|
||||
onSelect={topic => {
|
||||
page.topic = topic
|
||||
setSelectedPageTopic(topic)
|
||||
setTopicSelectorOpen(false)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="text-foreground bg-truncat"
|
||||
onClick={() => setDeleteModalOpen(true)}
|
||||
>
|
||||
<LaIcon name="Trash" className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-auto flex-col">
|
||||
<div className="relative flex h-full max-w-full grow flex-col items-stretch p-0">
|
||||
<LAEditor
|
||||
ref={contentEditorRef}
|
||||
editorClassName="-mx-3.5 px-3.5 py-2.5 flex-auto"
|
||||
editorClassName="-mx-3.5 px-3.5 py-2.5 flex-auto focus:outline-none"
|
||||
value={page.content}
|
||||
placeholder="Add content..."
|
||||
output="json"
|
||||
@@ -221,15 +216,6 @@ export const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<DeletePageModal
|
||||
isOpen={deleteModalOpen}
|
||||
onClose={() => setDeleteModalOpen(false)}
|
||||
onConfirm={() => {
|
||||
confirmDelete(page)
|
||||
}}
|
||||
title={page.title || ""}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,24 +2,33 @@
|
||||
|
||||
import * as React from "react"
|
||||
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
|
||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from "@/components/ui/breadcrumb"
|
||||
import { PersonalPage } from "@/lib/schema/personal-page"
|
||||
import { ID } from "jazz-tools"
|
||||
import { useMedia } from "react-use"
|
||||
import { TopicSelector } from "@/components/custom/topic-selector"
|
||||
|
||||
export const DetailPageHeader = ({ page }: { page: PersonalPage }) => {
|
||||
const isMobile = useMedia("(max-width: 770px)")
|
||||
|
||||
export const DetailPageHeader = ({ pageId }: { pageId: ID<PersonalPage> }) => {
|
||||
return (
|
||||
<ContentHeader>
|
||||
<div className="flex min-w-0 gap-2">
|
||||
<SidebarToggleButton />
|
||||
isMobile && (
|
||||
<>
|
||||
<ContentHeader className="lg:min-h-0">
|
||||
<div className="flex min-w-0 gap-2">
|
||||
<SidebarToggleButton />
|
||||
</div>
|
||||
</ContentHeader>
|
||||
|
||||
<Breadcrumb className="flex flex-row items-center">
|
||||
<BreadcrumbList className="sm:gap-2">
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="text-foreground font-medium">Pages</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</ContentHeader>
|
||||
<div className="flex flex-row items-start justify-between 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"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useState } from "react"
|
||||
import { useAccount, useCoState } from "@/lib/providers/jazz-provider"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
import AiSearch from "../../custom/ai-search"
|
||||
import { ID } from "jazz-tools"
|
||||
import Link from "next/link"
|
||||
import { Topic, PersonalLink, PersonalPage } from "@/lib/schema"
|
||||
import { PublicGlobalGroup } from "@/lib/schema/master/public-group"
|
||||
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
|
||||
|
||||
interface SearchTitleProps {
|
||||
title: string
|
||||
@@ -80,15 +80,11 @@ export const SearchWrapper = () => {
|
||||
root: { personalLinks: [], personalPages: [] }
|
||||
})
|
||||
|
||||
const globalGroup = useCoState(
|
||||
PublicGlobalGroup,
|
||||
process.env.NEXT_PUBLIC_JAZZ_GLOBAL_GROUP as ID<PublicGlobalGroup>,
|
||||
{
|
||||
root: {
|
||||
topics: []
|
||||
}
|
||||
const globalGroup = useCoState(PublicGlobalGroup, JAZZ_GLOBAL_GROUP_ID, {
|
||||
root: {
|
||||
topics: []
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.toLowerCase()
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { LinkItem } from "./link-item"
|
||||
import { LaAccount, PersonalLinkLists, Section as SectionSchema, Topic, UserRoot } from "@/lib/schema"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
|
||||
interface SectionProps {
|
||||
topic: Topic
|
||||
@@ -29,7 +29,7 @@ export function Section({
|
||||
me,
|
||||
personalLinks
|
||||
}: SectionProps) {
|
||||
const [nLinksToLoad, setNLinksToLoad] = useState(10);
|
||||
const [nLinksToLoad, setNLinksToLoad] = useState(10)
|
||||
|
||||
const linksToLoad = useMemo(() => {
|
||||
return section.links?.slice(0, nLinksToLoad)
|
||||
@@ -43,25 +43,24 @@ export function Section({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-px py-2">
|
||||
{linksToLoad?.map(
|
||||
(link, index) =>
|
||||
link?.url ? (
|
||||
<LinkItem
|
||||
key={index}
|
||||
topic={topic}
|
||||
link={link}
|
||||
isActive={activeIndex === startIndex + index}
|
||||
index={startIndex + index}
|
||||
setActiveIndex={setActiveIndex}
|
||||
ref={el => {
|
||||
linkRefs.current[startIndex + index] = el
|
||||
}}
|
||||
me={me}
|
||||
personalLinks={personalLinks}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton key={index} className="h-14 xl:h-11 w-full" />
|
||||
)
|
||||
{linksToLoad?.map((link, index) =>
|
||||
link?.url ? (
|
||||
<LinkItem
|
||||
key={index}
|
||||
topic={topic}
|
||||
link={link}
|
||||
isActive={activeIndex === startIndex + index}
|
||||
index={startIndex + index}
|
||||
setActiveIndex={setActiveIndex}
|
||||
ref={el => {
|
||||
linkRefs.current[startIndex + index] = el
|
||||
}}
|
||||
me={me}
|
||||
personalLinks={personalLinks}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton key={index} className="h-14 w-full xl:h-11" />
|
||||
)
|
||||
)}
|
||||
{section.links?.length && section.links?.length > nLinksToLoad && (
|
||||
<LoadMoreSpinner onLoadMore={() => setNLinksToLoad(n => n + 10)} />
|
||||
@@ -88,23 +87,25 @@ const LoadMoreSpinner = ({ onLoadMore }: { onLoadMore: () => void }) => {
|
||||
const observer = new IntersectionObserver(handleIntersection, {
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: 1.0,
|
||||
threshold: 1.0
|
||||
})
|
||||
|
||||
if (spinnerRef.current) {
|
||||
observer.observe(spinnerRef.current)
|
||||
const currentSpinnerRef = spinnerRef.current
|
||||
|
||||
if (currentSpinnerRef) {
|
||||
observer.observe(currentSpinnerRef)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (spinnerRef.current) {
|
||||
observer.unobserve(spinnerRef.current)
|
||||
if (currentSpinnerRef) {
|
||||
observer.unobserve(currentSpinnerRef)
|
||||
}
|
||||
}
|
||||
}, [handleIntersection])
|
||||
|
||||
return (
|
||||
<div ref={spinnerRef} className="flex justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
<LaIcon name="Loader" className="size-6 animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user