mirror of
https://github.com/linsa-io/linsa.git
synced 2026-05-29 10:00:50 +02:00
Move to TanStack Start from Next.js (#184)
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import * as React 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 = React.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 = React.useCallback(
|
||||
(selectedTopicName: string, topic: Topic) => {
|
||||
onChange?.(selectedTopicName)
|
||||
onTopicChange?.(topic)
|
||||
setIsTopicSelectorOpen(false)
|
||||
},
|
||||
[onChange, setIsTopicSelectorOpen, onTopicChange],
|
||||
)
|
||||
|
||||
const displaySelectedText = React.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}
|
||||
>
|
||||
{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 = React.useMemo(
|
||||
() =>
|
||||
topics.filter((topic) =>
|
||||
topic?.prettyName.toLowerCase().includes(search.toLowerCase()),
|
||||
),
|
||||
[topics, search],
|
||||
)
|
||||
|
||||
const parentRef = React.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
|
||||
Reference in New Issue
Block a user