Files
archived-linsa/web/components/custom/topic-selector.tsx
Aslam a3913baff9 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
2024-09-04 05:32:37 +07:00

180 lines
5.2 KiB
TypeScript

import React, { useMemo, useCallback, useRef, forwardRef } from "react"
import { atom, useAtom } from "jotai"
import { useVirtualizer } from "@tanstack/react-virtual"
import { Button, buttonVariants } from "@/components/ui/button"
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
import { cn } from "@/lib/utils"
import { LaIcon } from "@/components/custom/la-icon"
import { Command, CommandInput, CommandList, CommandItem, CommandGroup } from "@/components/ui/command"
import { useCoState } from "@/lib/providers/jazz-provider"
import { PublicGlobalGroup } from "@/lib/schema/master/public-group"
import { ListOfTopics, Topic } from "@/lib/schema"
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
import { VariantProps } from "class-variance-authority"
interface TopicSelectorProps extends VariantProps<typeof buttonVariants> {
showSearch?: boolean
defaultLabel?: string
searchPlaceholder?: string
value?: string | null
onChange?: (value: string) => void
onTopicChange?: (value: Topic) => void
className?: string
renderSelectedText?: (value?: string | null) => React.ReactNode
side?: "bottom" | "top" | "right" | "left"
align?: "center" | "end" | "start"
}
export const topicSelectorAtom = atom(false)
export const TopicSelector = forwardRef<HTMLButtonElement, TopicSelectorProps>(
(
{
showSearch = true,
defaultLabel = "Select topic",
searchPlaceholder = "Search topic...",
value,
onChange,
onTopicChange,
className,
renderSelectedText,
side = "bottom",
align = "end",
...props
},
ref
) => {
const [isTopicSelectorOpen, setIsTopicSelectorOpen] = useAtom(topicSelectorAtom)
const group = useCoState(PublicGlobalGroup, JAZZ_GLOBAL_GROUP_ID, { root: { topics: [] } })
const handleSelect = useCallback(
(selectedTopicName: string, topic: Topic) => {
onChange?.(selectedTopicName)
onTopicChange?.(topic)
setIsTopicSelectorOpen(false)
},
[onChange, setIsTopicSelectorOpen, onTopicChange]
)
const displaySelectedText = useMemo(() => {
if (renderSelectedText) {
return renderSelectedText(value)
}
return <span className="truncate">{value || defaultLabel}</span>
}, [value, defaultLabel, renderSelectedText])
return (
<Popover open={isTopicSelectorOpen} onOpenChange={setIsTopicSelectorOpen}>
<PopoverTrigger asChild>
<Button
ref={ref}
size="sm"
type="button"
role="combobox"
variant="secondary"
className={cn("gap-x-2 text-sm", className)}
{...props}
>
{displaySelectedText}
<LaIcon name="ChevronDown" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-52 rounded-lg p-0"
side={side}
align={align}
onCloseAutoFocus={e => e.preventDefault()}
>
{group?.root.topics && (
<TopicSelectorContent
showSearch={showSearch}
searchPlaceholder={searchPlaceholder}
value={value}
onSelect={handleSelect}
topics={group.root.topics}
/>
)}
</PopoverContent>
</Popover>
)
}
)
TopicSelector.displayName = "TopicSelector"
interface TopicSelectorContentProps extends Omit<TopicSelectorProps, "onChange" | "onTopicChange"> {
onSelect: (value: string, topic: Topic) => void
topics: ListOfTopics
}
const TopicSelectorContent: React.FC<TopicSelectorContentProps> = React.memo(
({ showSearch, searchPlaceholder, value, onSelect, topics }) => {
const [search, setSearch] = React.useState("")
const filteredTopics = useMemo(
() => topics.filter(topic => topic?.prettyName.toLowerCase().includes(search.toLowerCase())),
[topics, search]
)
const parentRef = useRef<HTMLDivElement>(null)
const rowVirtualizer = useVirtualizer({
count: filteredTopics.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
overscan: 5
})
return (
<Command>
{showSearch && (
<CommandInput placeholder={searchPlaceholder} className="h-9" value={search} onValueChange={setSearch} />
)}
<CommandList>
<div ref={parentRef} style={{ height: "200px", overflow: "auto" }}>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: "100%",
position: "relative"
}}
>
<CommandGroup>
{rowVirtualizer.getVirtualItems().map(virtualRow => {
const topic = filteredTopics[virtualRow.index]
return (
topic && (
<CommandItem
key={virtualRow.key}
value={topic.name}
onSelect={value => onSelect(value, topic)}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}}
>
<span>{topic.prettyName}</span>
<LaIcon
name="Check"
className={cn("absolute right-3", topic.name === value ? "text-primary" : "text-transparent")}
/>
</CommandItem>
)
)
})}
</CommandGroup>
</div>
</div>
</CommandList>
</Command>
)
}
)
TopicSelectorContent.displayName = "TopicSelectorContent"
export default TopicSelector