Move to TanStack Start from Next.js (#184)

This commit is contained in:
Aslam
2024-10-07 16:44:17 +07:00
committed by GitHub
parent 3a89a1c07f
commit 950ebc3dad
514 changed files with 20021 additions and 15508 deletions

View File

@@ -0,0 +1,91 @@
import * as React from "react"
import * as smd from "streaming-markdown"
interface AiSearchProps {
searchQuery: string
}
const AiSearch: React.FC<AiSearchProps> = (props: { searchQuery: string }) => {
const [error, setError] = React.useState<string>("")
const root_el = React.useRef<HTMLDivElement | null>(null)
const [parser, md_el] = React.useMemo(() => {
const md_el = document.createElement("div")
const renderer = smd.default_renderer(md_el)
const parser = smd.parser(renderer)
return [parser, md_el]
}, [])
React.useEffect(() => {
if (root_el.current) {
root_el.current.appendChild(md_el)
}
}, [md_el])
React.useEffect(() => {
const question = props.searchQuery
fetchData()
async function fetchData() {
let response: Response
try {
response = await fetch("/api/search-stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ question: question }),
})
} catch (error) {
console.error("Error fetching data:", error)
setError("Error fetching data")
return
}
if (!response.body) {
console.error("Response has no body")
setError("Response has no body")
return
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let done = false
while (!done) {
const res = await reader.read()
if (res.value) {
const text = decoder.decode(res.value)
smd.parser_write(parser, text)
}
if (res.done) {
smd.parser_end(parser)
done = true
}
}
}
}, [props.searchQuery, parser])
return (
<div className="mx-auto flex max-w-3xl flex-col items-center">
<div className="w-full rounded-lg bg-inherit p-6 text-black dark:text-white">
<div className="mb-6 rounded-lg bg-blue-700 p-4 text-white">
<h2 className="text-lg font-medium"> This is what I have found:</h2>
</div>
<div
className="rounded-xl bg-neutral-100 p-4 dark:bg-[#121212]"
ref={root_el}
></div>
</div>
<p className="text-md pb-5 font-semibold opacity-50">{error}</p>
<button className="text-md rounded-2xl bg-neutral-300 px-6 py-3 font-semibold text-opacity-50 shadow-inner shadow-neutral-400/50 transition-colors hover:bg-neutral-700 dark:bg-neutral-800 dark:shadow-neutral-700/50">
Ask Community
</button>
</div>
)
}
export default AiSearch

View File

@@ -0,0 +1,44 @@
import React from "react"
import { cn } from "@/lib/utils"
interface ColumnWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
style?: { [key: string]: string }
}
interface ColumnTextProps extends React.HTMLAttributes<HTMLSpanElement> {}
const ColumnWrapper = React.forwardRef<HTMLDivElement, ColumnWrapperProps>(
({ children, className, style, ...props }, ref) => (
<div
className={cn("flex grow flex-row items-center justify-start", className)}
style={{
width: "var(--width)",
minWidth: "var(--min-width, min-content)",
maxWidth: "min(var(--width), var(--max-width))",
flexBasis: "var(--width)",
...style,
}}
ref={ref}
{...props}
>
{children}
</div>
),
)
ColumnWrapper.displayName = "ColumnWrapper"
const ColumnText = React.forwardRef<HTMLSpanElement, ColumnTextProps>(
({ children, className, ...props }, ref) => (
<span className={cn("text-left text-xs", className)} ref={ref} {...props}>
{children}
</span>
),
)
ColumnText.displayName = "ColumnText"
export const Column = {
Wrapper: ColumnWrapper,
Text: ColumnText,
}

View File

@@ -0,0 +1,58 @@
import * as React from "react"
import { Button } from "@/components/ui/button"
import { useAtom } from "jotai"
import { isCollapseAtom, toggleCollapseAtom } from "@/store/sidebar"
import { useMedia } from "@/hooks/use-media"
import { cn } from "@/lib/utils"
import { LaIcon } from "@/components/custom/la-icon"
type ContentHeaderProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title">
export const ContentHeader = React.forwardRef<
HTMLDivElement,
ContentHeaderProps
>(({ children, className, ...props }, ref) => {
return (
<header
className={cn(
"flex min-h-10 min-w-0 shrink-0 items-center gap-3 pl-8 pr-6 transition-opacity max-lg:px-4",
className,
)}
ref={ref}
{...props}
>
{children}
</header>
)
})
ContentHeader.displayName = "ContentHeader"
export const SidebarToggleButton: React.FC = () => {
const [isCollapse] = useAtom(isCollapseAtom)
const [, toggle] = useAtom(toggleCollapseAtom)
const isTablet = useMedia("(max-width: 1024px)")
if (!isCollapse && !isTablet) return null
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
e.stopPropagation()
toggle()
}
return (
<div className="flex items-center gap-1">
<Button
type="button"
size="icon"
variant="ghost"
aria-label="Menu"
className="text-primary/60"
onClick={handleClick}
>
<LaIcon name="PanelLeft" />
</Button>
</div>
)
}

View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { format } from "date-fns"
import { Calendar as CalendarIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
interface DatePickerProps {
date: Date | undefined
onDateChange: (date: Date | undefined) => void
className?: string
}
export function DatePicker({ date, onDateChange, className }: DatePickerProps) {
const [open, setOpen] = React.useState(false)
const selectDate = (selectedDate: Date | undefined) => {
onDateChange(selectedDate)
setOpen(false)
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-[240px] justify-start text-left font-normal",
!date && "text-muted-foreground",
className,
)}
onClick={(e) => e.stopPropagation()}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
align="start"
onClick={(e) => e.stopPropagation()}
>
<Calendar
mode="single"
selected={date}
onSelect={selectDate}
initialFocus
/>
</PopoverContent>
</Popover>
)
}

View File

@@ -0,0 +1,30 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { icons } from "lucide-react"
export type IconProps = {
name: keyof typeof icons
className?: string
strokeWidth?: number
[key: string]: any
}
export const LaIcon = React.memo(
({ name, className, size, strokeWidth, ...props }: IconProps) => {
const IconComponent = icons[name]
if (!IconComponent) {
return null
}
return (
<IconComponent
className={cn(!size ? "size-4" : size, className)}
strokeWidth={strokeWidth || 2}
{...props}
/>
)
},
)
LaIcon.displayName = "LaIcon"

View File

@@ -0,0 +1,137 @@
import * as React from "react"
import { useAtom } from "jotai"
import { Button } 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 { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
import { linkLearningStateSelectorAtom } from "@/store/link"
import {
Command,
CommandInput,
CommandList,
CommandItem,
CommandGroup,
} from "@/components/ui/command"
import { ScrollArea } from "@/components/ui/scroll-area"
import { icons } from "lucide-react"
interface LearningStateSelectorProps {
showSearch?: boolean
defaultLabel?: string
searchPlaceholder?: string
value?: string
onChange: (value: LearningStateValue) => void
className?: string
defaultIcon?: keyof typeof icons
}
export const LearningStateSelector: React.FC<LearningStateSelectorProps> = ({
showSearch = true,
defaultLabel = "State",
searchPlaceholder = "Search state...",
value,
onChange,
className,
defaultIcon,
}) => {
const [isLearningStateSelectorOpen, setIsLearningStateSelectorOpen] = useAtom(
linkLearningStateSelectorAtom,
)
const selectedLearningState = React.useMemo(
() => LEARNING_STATES.find((ls) => ls.value === value),
[value],
)
const handleSelect = (selectedValue: string) => {
onChange(selectedValue as LearningStateValue)
setIsLearningStateSelectorOpen(false)
}
const iconName = selectedLearningState?.icon || defaultIcon
const labelText = selectedLearningState?.label || defaultLabel
return (
<Popover
open={isLearningStateSelectorOpen}
onOpenChange={setIsLearningStateSelectorOpen}
>
<PopoverTrigger asChild>
<Button
size="sm"
type="button"
role="combobox"
variant="secondary"
className={cn("gap-x-2 text-sm", className)}
>
{iconName && (
<LaIcon
name={iconName}
className={cn(selectedLearningState?.className)}
/>
)}
{labelText && (
<span
className={cn("truncate", selectedLearningState?.className || "")}
>
{labelText}
</span>
)}
<LaIcon name="ChevronDown" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-52 rounded-lg p-0" side="bottom" align="end">
<LearningStateSelectorContent
showSearch={showSearch}
searchPlaceholder={searchPlaceholder}
value={value}
onSelect={handleSelect}
/>
</PopoverContent>
</Popover>
)
}
interface LearningStateSelectorContentProps {
showSearch: boolean
searchPlaceholder: string
value?: string
onSelect: (value: string) => void
}
export const LearningStateSelectorContent: React.FC<
LearningStateSelectorContentProps
> = ({ showSearch, searchPlaceholder, value, onSelect }) => {
return (
<Command>
{showSearch && (
<CommandInput placeholder={searchPlaceholder} className="h-9" />
)}
<CommandList>
<ScrollArea>
<CommandGroup>
{LEARNING_STATES.map((ls) => (
<CommandItem key={ls.value} value={ls.value} onSelect={onSelect}>
{ls.icon && (
<LaIcon name={ls.icon} className={cn("mr-2", ls.className)} />
)}
<span className={ls.className}>{ls.label}</span>
<LaIcon
name="Check"
className={cn(
"absolute right-3",
ls.value === value ? "text-primary" : "text-transparent",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</CommandList>
</Command>
)
}

View File

@@ -0,0 +1,32 @@
import * as React from "react"
import { cn } from "@/lib/utils"
interface SpinnerProps extends React.SVGAttributes<SVGElement> {}
export const Spinner = React.forwardRef<SVGSVGElement, SpinnerProps>(
({ className, ...props }, ref) => (
<svg
ref={ref}
className={cn("h-4 w-4 animate-spin", className)}
viewBox="0 0 24 24"
{...props}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
),
)
Spinner.displayName = "Spinner"

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import BaseTextareaAutosize from "react-textarea-autosize"
import { TextareaAutosizeProps as BaseTextareaAutosizeProps } from "react-textarea-autosize"
import { cn } from "@/lib/utils"
export interface TextareaProps extends Omit<BaseTextareaAutosizeProps, "ref"> {}
const TextareaAutosize = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<BaseTextareaAutosize
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
)
},
)
TextareaAutosize.displayName = "TextareaAutosize"
export { TextareaAutosize }

View File

@@ -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