mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 14:19:11 +03:00
Remove unused files
This commit is contained in:
1 parent
8be344a8e8
commit
85fc2cf73b
163 files changed
+93
-12235
No files matched your search
@@ -1,57 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
|
||||
))
|
||||
AccordionItem.displayName = 'AccordionItem'
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-transform hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
'rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="pb-4 pt-0">{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from 'ui'
|
||||
import { useCopyToClipboard } from 'components/interfaces/DataTableDemo/hooks/use-copy-to-clipboard'
|
||||
import { composeRefs } from 'components/interfaces/DataTableDemo/lib/compose-refs'
|
||||
import { cn } from 'ui'
|
||||
import { cva, VariantProps } from 'class-variance-authority'
|
||||
import { Check, Copy, Plus } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
const containerVariants = cva(
|
||||
'peer whitespace-pre-wrap break-all rounded-md border p-2 font-mono text-sm',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-border/50 bg-border/30',
|
||||
destructive: 'border-destructive/50 bg-destructive/30',
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface CopyToClipboardContainerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof containerVariants> {
|
||||
/**
|
||||
* If set and the content exceeds the maximum height,
|
||||
* a "Show content" button will collapse the full content
|
||||
*/
|
||||
maxHeight?: number
|
||||
}
|
||||
|
||||
export const CopyToClipboardContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
CopyToClipboardContainerProps
|
||||
>(({ children, variant, maxHeight, className, ...props }, ref) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [collapsible, setCollapsible] = React.useState(!!maxHeight)
|
||||
const innerRef = React.useRef<HTMLDivElement>(null)
|
||||
const { copy, isCopied } = useCopyToClipboard()
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (innerRef.current && maxHeight) {
|
||||
// REMINDER: scrollHeight will keep the max possible height of the content
|
||||
// if maxHeight is set, we show the button by default and remove it if height is too narrow.
|
||||
// That way, we avoid having a layout shift of first showing the full height and then collapsible.
|
||||
if (innerRef.current.scrollHeight <= maxHeight) {
|
||||
setCollapsible(false)
|
||||
}
|
||||
}
|
||||
}, [innerRef.current, maxHeight])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group relative text-left"
|
||||
style={
|
||||
{
|
||||
'--max-height': `${maxHeight}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={composeRefs(ref, innerRef)}
|
||||
className={cn(
|
||||
containerVariants({ variant }),
|
||||
collapsible && !open ? 'max-h-[var(--max-height)] overflow-hidden' : undefined,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="absolute right-2 top-2 h-6 w-6 opacity-0 focus:opacity-100 group-hover:opacity-100 peer-focus:opacity-100"
|
||||
onClick={() => {
|
||||
const content = innerRef.current?.textContent
|
||||
if (content) copy(content)
|
||||
}}
|
||||
>
|
||||
{!isCopied ? <Copy className="h-3 w-3" /> : <Check className="h-3 w-3" />}
|
||||
</Button>
|
||||
{collapsible && !open ? (
|
||||
<div className="absolute inset-x-px bottom-px flex items-center justify-center rounded-b-md bg-gradient-to-b from-background/0 to-background/100">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="my-1 rounded-full"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" /> Show content
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
CopyToClipboardContainer.displayName = 'CopyToClipboardContainer'
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
export interface InputWithAddonsProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
leading?: React.ReactNode
|
||||
trailing?: React.ReactNode
|
||||
containerClassName?: string
|
||||
}
|
||||
|
||||
const InputWithAddons = React.forwardRef<HTMLInputElement, InputWithAddonsProps>(
|
||||
({ leading, trailing, containerClassName, className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-input ring-offset-background focus-within:ring-ring group flex h-10 w-full rounded-md border bg-transparent text-sm focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 overflow-hidden',
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
{leading ? (
|
||||
<div className="border-input bg-muted/50 border-r px-3 py-2">{leading}</div>
|
||||
) : null}
|
||||
<input
|
||||
className={cn(
|
||||
'placeholder:text-muted-foreground bg-background w-full rounded-md px-3 py-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{trailing ? (
|
||||
<div className="border-input bg-muted/50 border-l px-3 py-2">{trailing}</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
InputWithAddons.displayName = 'InputWithAddons'
|
||||
|
||||
export { InputWithAddons }
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copy Pasta from: https://github.com/sadmann7/shadcn-table/blob/main/src/components/kbd.tsx#L54
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
export const kbdVariants = cva(
|
||||
'select-none rounded border px-1.5 py-px font-mono text-[0.7rem] font-normal font-mono shadow-sm disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-accent text-accent-foreground',
|
||||
outline: 'bg-background text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface KbdProps
|
||||
extends React.ComponentPropsWithoutRef<'kbd'>,
|
||||
VariantProps<typeof kbdVariants> {
|
||||
/**
|
||||
* The title of the `abbr` element inside the `kbd` element.
|
||||
* @default undefined
|
||||
* @type string | undefined
|
||||
* @example title="Command"
|
||||
*/
|
||||
abbrTitle?: string
|
||||
}
|
||||
|
||||
const Kbd = React.forwardRef<HTMLUnknownElement, KbdProps>(
|
||||
({ abbrTitle, children, className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<kbd className={cn(kbdVariants({ variant, className }))} ref={ref} {...props}>
|
||||
{abbrTitle ? (
|
||||
<abbr title={abbrTitle} className="no-underline">
|
||||
{children}
|
||||
</abbr>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</kbd>
|
||||
)
|
||||
}
|
||||
)
|
||||
Kbd.displayName = 'Kbd'
|
||||
|
||||
export { Kbd }
|
||||
@@ -1,49 +0,0 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from 'components/interfaces/DataTableDemo/components/custom/table'
|
||||
import { useCopyToClipboard } from 'components/interfaces/DataTableDemo/hooks/use-copy-to-clipboard'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
|
||||
interface KVTableProps {
|
||||
data: Record<string, string | number | boolean>
|
||||
}
|
||||
export function KVTable({ data }: KVTableProps) {
|
||||
return (
|
||||
<div className="mx-auto max-w-lg">
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-background">
|
||||
<Table>
|
||||
<TableBody>
|
||||
{Object.entries(data).map(([key, value]) => {
|
||||
return <RowAction key={key} label={key} value={value} />
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RowAction({ label, value }: { label: string; value: string | number | boolean }) {
|
||||
const { copy, isCopied } = useCopyToClipboard()
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className="group text-left *:border-border hover:bg-transparent [&>:not(:last-child)]:border-r"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
copy(value.toString())
|
||||
}}
|
||||
>
|
||||
<TableCell className="bg-muted/50 py-1 font-mono font-medium">{label}</TableCell>
|
||||
<TableCell className="relative py-1 font-mono">
|
||||
{value}
|
||||
<div className="invisible absolute right-1.5 top-1.5 rounded-sm border border-border bg-background p-0.5 backdrop-blur-sm group-hover:visible">
|
||||
{!isCopied ? <Copy className="h-3 w-3" /> : <Check className="h-3 w-3" />}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
Tabs_Shadcn_ as Tabs,
|
||||
TabsContent_Shadcn_ as TabsContent,
|
||||
TabsList_Shadcn_ as TabsList,
|
||||
TabsTrigger_Shadcn_ as TabsTrigger,
|
||||
} from 'ui'
|
||||
import { Braces, TableProperties } from 'lucide-react'
|
||||
import { CopyToClipboardContainer } from './copy-to-clipboard-container'
|
||||
import { KVTable } from './kv-table'
|
||||
|
||||
interface KVTabsProps {
|
||||
data: Record<string, string>
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function KVTabs({ data, className }: KVTabsProps) {
|
||||
return (
|
||||
<Tabs defaultValue="table" className={className}>
|
||||
<div className="flex items-center justify-end">
|
||||
<TabsList className="h-auto gap-1 bg-background px-0 py-0">
|
||||
<TabsTrigger
|
||||
value="table"
|
||||
className="px-0 py-0 text-muted-foreground/70 data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
||||
>
|
||||
<TableProperties className="h-4 w-4" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="raw"
|
||||
className="px-0 py-0 text-muted-foreground/70 data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
||||
>
|
||||
<Braces className="h-4 w-4" />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="table" className="rounded-md">
|
||||
<KVTable data={data} />
|
||||
</TabsContent>
|
||||
<TabsContent value="raw" asChild>
|
||||
{/* REMINDER: either `overflow-auto whitespace-pre` or `whitespace-pre-wrap` - depends if we want to wrap the text or not */}
|
||||
<CopyToClipboardContainer variant="default" className="overflow-auto whitespace-pre">
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</CopyToClipboardContainer>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import NextLink, { type LinkProps as NextLinkProps } from 'next/link'
|
||||
import React from 'react'
|
||||
import { cn } from 'ui'
|
||||
import { ArrowUpRight } from 'lucide-react'
|
||||
|
||||
export interface LinkProps extends NextLinkProps {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
hideArrow?: boolean
|
||||
}
|
||||
|
||||
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
||||
({ className, href, children, hideArrow, ...props }, ref) => {
|
||||
const isInternal = href?.toString().startsWith('/') || href?.toString().startsWith('#')
|
||||
const externalLinkProps = !isInternal ? { target: '_blank', rel: 'noreferrer' } : undefined
|
||||
|
||||
return (
|
||||
<NextLink
|
||||
className={cn(
|
||||
'group text-foreground underline underline-offset-4 decoration-border hover:decoration-foreground',
|
||||
'ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded-md',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
href={href}
|
||||
{...externalLinkProps}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{!isInternal && !hideArrow ? (
|
||||
<ArrowUpRight className="text-muted-foreground w-4 h-4 inline-block ml-0.5 group-hover:text-foreground group-hover:-translate-y-px group-hover:translate-x-px" />
|
||||
) : null}
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Link.displayName = 'Link'
|
||||
|
||||
export { Link }
|
||||
@@ -1,128 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-background/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {
|
||||
hideClose?: boolean
|
||||
}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, hideClose, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||
{children}
|
||||
{!hideClose ? (
|
||||
<SheetPrimitive.Close
|
||||
autoFocus={true}
|
||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
) : null}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||
)
|
||||
SheetHeader.displayName = 'SheetHeader'
|
||||
|
||||
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = 'SheetFooter'
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Props to https://github.com/shadcn-ui/ui/issues/885#issuecomment-2059600641
|
||||
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const initialValue = Array.isArray(props.value) ? props.value : [props.min, props.max]
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
{initialValue.map((_, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
})
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -1,333 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import type {
|
||||
DndContextProps,
|
||||
DraggableSyntheticListeners,
|
||||
DropAnimation,
|
||||
UniqueIdentifier,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
closestCenter,
|
||||
defaultDropAnimationSideEffects,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
restrictToHorizontalAxis,
|
||||
restrictToParentElement,
|
||||
restrictToVerticalAxis,
|
||||
} from '@dnd-kit/modifiers'
|
||||
import {
|
||||
arrayMove,
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
type SortableContextProps,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { Slot, type SlotProps } from '@radix-ui/react-slot'
|
||||
|
||||
import { composeRefs } from 'components/interfaces/DataTableDemo/lib/compose-refs'
|
||||
import { cn } from 'ui'
|
||||
import { Button, type ButtonProps } from 'ui'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
const orientationConfig = {
|
||||
vertical: {
|
||||
modifiers: [restrictToVerticalAxis, restrictToParentElement],
|
||||
strategy: verticalListSortingStrategy,
|
||||
},
|
||||
horizontal: {
|
||||
modifiers: [restrictToHorizontalAxis, restrictToParentElement],
|
||||
strategy: horizontalListSortingStrategy,
|
||||
},
|
||||
mixed: {
|
||||
modifiers: [restrictToParentElement],
|
||||
strategy: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
interface SortableProps<TData extends { id: UniqueIdentifier }> extends DndContextProps {
|
||||
/**
|
||||
* An array of data items that the sortable component will render.
|
||||
* @example
|
||||
* value={[
|
||||
* { id: 1, name: 'Item 1' },
|
||||
* { id: 2, name: 'Item 2' },
|
||||
* ]}
|
||||
*/
|
||||
value: TData[]
|
||||
|
||||
/**
|
||||
* An optional callback function that is called when the order of the data items changes.
|
||||
* It receives the new array of items as its argument.
|
||||
* @example
|
||||
* onValueChange={(items) => console.log(items)}
|
||||
*/
|
||||
onValueChange?: (items: TData[]) => void
|
||||
|
||||
/**
|
||||
* An optional callback function that is called when an item is moved.
|
||||
* It receives an event object with `activeIndex` and `overIndex` properties, representing the original and new positions of the moved item.
|
||||
* This will override the default behavior of updating the order of the data items.
|
||||
* @type (event: { activeIndex: number; overIndex: number }) => void
|
||||
* @example
|
||||
* onMove={(event) => console.log(`Item moved from index ${event.activeIndex} to index ${event.overIndex}`)}
|
||||
*/
|
||||
onMove?: (event: { activeIndex: number; overIndex: number }) => void
|
||||
|
||||
/**
|
||||
* A collision detection strategy that will be used to determine the closest sortable item.
|
||||
* @default closestCenter
|
||||
* @type DndContextProps["collisionDetection"]
|
||||
*/
|
||||
collisionDetection?: DndContextProps['collisionDetection']
|
||||
|
||||
/**
|
||||
* An array of modifiers that will be used to modify the behavior of the sortable component.
|
||||
* @default
|
||||
* [restrictToVerticalAxis, restrictToParentElement]
|
||||
* @type Modifier[]
|
||||
*/
|
||||
modifiers?: DndContextProps['modifiers']
|
||||
|
||||
/**
|
||||
* A sorting strategy that will be used to determine the new order of the data items.
|
||||
* @default verticalListSortingStrategy
|
||||
* @type SortableContextProps["strategy"]
|
||||
*/
|
||||
strategy?: SortableContextProps['strategy']
|
||||
|
||||
/**
|
||||
* Specifies the axis for the drag-and-drop operation. It can be "vertical", "horizontal", or "both".
|
||||
* @default "vertical"
|
||||
* @type "vertical" | "horizontal" | "mixed"
|
||||
*/
|
||||
orientation?: 'vertical' | 'horizontal' | 'mixed'
|
||||
|
||||
/**
|
||||
* An optional React node that is rendered on top of the sortable component.
|
||||
* It can be used to display additional information or controls.
|
||||
* @default null
|
||||
* @type React.ReactNode | null
|
||||
* @example
|
||||
* overlay={<Skeleton className="w-full h-8" />}
|
||||
*/
|
||||
overlay?: React.ReactNode | null
|
||||
}
|
||||
|
||||
function Sortable<TData extends { id: UniqueIdentifier }>({
|
||||
value,
|
||||
onValueChange,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragCancel,
|
||||
collisionDetection = closestCenter,
|
||||
modifiers,
|
||||
strategy,
|
||||
onMove,
|
||||
orientation = 'vertical',
|
||||
overlay,
|
||||
children,
|
||||
...props
|
||||
}: SortableProps<TData>) {
|
||||
const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null)
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor),
|
||||
useSensor(TouchSensor),
|
||||
useSensor(KeyboardSensor)
|
||||
)
|
||||
|
||||
const config = orientationConfig[orientation]
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
modifiers={modifiers ?? config.modifiers}
|
||||
sensors={sensors}
|
||||
onDragStart={(event) => {
|
||||
setActiveId(event.active.id)
|
||||
onDragStart?.(event)
|
||||
}}
|
||||
onDragEnd={(event) => {
|
||||
const { active, over } = event
|
||||
if (over && active.id !== over?.id) {
|
||||
const activeIndex = value.findIndex((item) => item.id === active.id)
|
||||
const overIndex = value.findIndex((item) => item.id === over.id)
|
||||
|
||||
if (onMove) {
|
||||
onMove({ activeIndex, overIndex })
|
||||
} else {
|
||||
onValueChange?.(arrayMove(value, activeIndex, overIndex))
|
||||
}
|
||||
}
|
||||
setActiveId(null)
|
||||
onDragEnd?.(event)
|
||||
}}
|
||||
onDragCancel={(event) => {
|
||||
setActiveId?.(null)
|
||||
onDragCancel?.(event)
|
||||
}}
|
||||
collisionDetection={collisionDetection}
|
||||
{...props}
|
||||
>
|
||||
<SortableContext items={value} strategy={strategy ?? config.strategy}>
|
||||
{children}
|
||||
</SortableContext>
|
||||
{overlay
|
||||
? // https://docs.dndkit.com/api-documentation/draggable/drag-overlay#portals
|
||||
createPortal(
|
||||
<SortableOverlay activeId={activeId}>{overlay}</SortableOverlay>,
|
||||
document.body
|
||||
)
|
||||
: null}
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
|
||||
const dropAnimationOpts: DropAnimation = {
|
||||
sideEffects: defaultDropAnimationSideEffects({
|
||||
styles: {
|
||||
active: {
|
||||
opacity: '0.4',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
interface SortableOverlayProps extends React.ComponentPropsWithRef<typeof DragOverlay> {
|
||||
activeId?: UniqueIdentifier | null
|
||||
}
|
||||
|
||||
const SortableOverlay = React.forwardRef<HTMLDivElement, SortableOverlayProps>(
|
||||
({ activeId, dropAnimation = dropAnimationOpts, children, ...props }, ref) => {
|
||||
return (
|
||||
<DragOverlay dropAnimation={dropAnimation} {...props}>
|
||||
{activeId ? (
|
||||
<SortableItem ref={ref} value={activeId} className="cursor-grabbing" asChild>
|
||||
{children}
|
||||
</SortableItem>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
)
|
||||
}
|
||||
)
|
||||
SortableOverlay.displayName = 'SortableOverlay'
|
||||
|
||||
interface SortableItemContextProps {
|
||||
attributes: React.HTMLAttributes<HTMLElement>
|
||||
listeners: DraggableSyntheticListeners | undefined
|
||||
isDragging?: boolean
|
||||
}
|
||||
|
||||
const SortableItemContext = React.createContext<SortableItemContextProps>({
|
||||
attributes: {},
|
||||
listeners: undefined,
|
||||
isDragging: false,
|
||||
})
|
||||
|
||||
function useSortableItem() {
|
||||
const context = React.useContext(SortableItemContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useSortableItem must be used within a SortableItem')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
interface SortableItemProps extends SlotProps {
|
||||
/**
|
||||
* The unique identifier of the item.
|
||||
* @example "item-1"
|
||||
* @type UniqueIdentifier
|
||||
*/
|
||||
value: UniqueIdentifier
|
||||
|
||||
/**
|
||||
* Specifies whether the item should act as a trigger for the drag-and-drop action.
|
||||
* @default false
|
||||
* @type boolean | undefined
|
||||
*/
|
||||
asTrigger?: boolean
|
||||
|
||||
/**
|
||||
* Merges the item's props into its immediate child.
|
||||
* @default false
|
||||
* @type boolean | undefined
|
||||
*/
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const SortableItem = React.forwardRef<HTMLDivElement, SortableItemProps>(
|
||||
({ value, asTrigger, asChild, className, ...props }, ref) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: value,
|
||||
})
|
||||
|
||||
const context = React.useMemo<SortableItemContextProps>(
|
||||
() => ({
|
||||
attributes,
|
||||
listeners,
|
||||
isDragging,
|
||||
}),
|
||||
[attributes, listeners, isDragging]
|
||||
)
|
||||
const style: React.CSSProperties = {
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
}
|
||||
|
||||
const Comp = asChild ? Slot : 'div'
|
||||
|
||||
return (
|
||||
<SortableItemContext.Provider value={context}>
|
||||
<Comp
|
||||
data-state={isDragging ? 'dragging' : undefined}
|
||||
className={cn(
|
||||
'data-[state=dragging]:cursor-grabbing',
|
||||
{ 'cursor-grab': !isDragging && asTrigger },
|
||||
className
|
||||
)}
|
||||
ref={composeRefs(ref, setNodeRef as React.Ref<HTMLDivElement>)}
|
||||
style={style}
|
||||
{...(asTrigger ? attributes : {})}
|
||||
{...(asTrigger ? listeners : {})}
|
||||
{...props}
|
||||
/>
|
||||
</SortableItemContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
SortableItem.displayName = 'SortableItem'
|
||||
|
||||
interface SortableDragHandleProps extends ButtonProps {
|
||||
withHandle?: boolean
|
||||
}
|
||||
|
||||
const SortableDragHandle = React.forwardRef<HTMLButtonElement, SortableDragHandleProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { attributes, listeners, isDragging } = useSortableItem()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={composeRefs(ref)}
|
||||
data-state={isDragging ? 'dragging' : undefined}
|
||||
className={cn('cursor-grab data-[state=dragging]:cursor-grabbing', className)}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
SortableDragHandle.displayName = 'SortableDragHandle'
|
||||
|
||||
export { Sortable, SortableDragHandle, SortableItem, SortableOverlay }
|
||||
@@ -1,102 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
|
||||
containerClassName?: string
|
||||
}
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, TableProps>(
|
||||
({ className, containerClassName, onScroll, ...props }, ref) => (
|
||||
<div
|
||||
className={cn('w-full overflow-auto', containerClassName)}
|
||||
// REMINDER: we are not scrolling the table, but the container
|
||||
{...{ onScroll }}
|
||||
>
|
||||
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||
</div>
|
||||
)
|
||||
)
|
||||
Table.displayName = 'Table'
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = 'TableHeader'
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||
))
|
||||
TableBody.displayName = 'TableBody'
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn('bg-primary text-primary-foreground font-medium', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = 'TableFooter'
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
TableRow.displayName = 'TableRow'
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = 'TableHead'
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = 'TableCell'
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption ref={ref} className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} />
|
||||
))
|
||||
TableCaption.displayName = 'TableCaption'
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, TooltipPortal } from 'ui'
|
||||
import { cn } from 'ui'
|
||||
|
||||
interface TextWithTooltipProps {
|
||||
text: string | number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TextWithTooltip({ text, className }: TextWithTooltipProps) {
|
||||
const [isTruncated, setIsTruncated] = useState<boolean>(false)
|
||||
const textRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const checkTruncation = () => {
|
||||
if (textRef.current) {
|
||||
const { scrollWidth, clientWidth } = textRef.current
|
||||
setIsTruncated(scrollWidth > clientWidth)
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
checkTruncation()
|
||||
})
|
||||
|
||||
if (textRef.current) {
|
||||
resizeObserver.observe(textRef.current)
|
||||
}
|
||||
|
||||
checkTruncation()
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={100} disableHoverableContent>
|
||||
<Tooltip>
|
||||
<TooltipTrigger disabled={!isTruncated} asChild>
|
||||
<div
|
||||
ref={textRef}
|
||||
className={cn('truncate', !isTruncated && 'pointer-events-none', className)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>{text}</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
import type { Column } from '@tanstack/react-table'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||
|
||||
import { Button, type ButtonProps } from 'ui'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue> extends ButtonProps {
|
||||
column: Column<TData, TValue>
|
||||
title: string
|
||||
}
|
||||
|
||||
export function DataTableColumnHeader<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
className,
|
||||
...props
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||
if (!column.getCanSort()) {
|
||||
return <div className={cn(className)}>{title}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
column.toggleSorting(undefined)
|
||||
}}
|
||||
className={cn(
|
||||
'text-xs',
|
||||
'py-0 px-0 h-7 hover:bg-transparent flex gap-2 items-center justify-between w-full',
|
||||
className
|
||||
)}
|
||||
iconRight={
|
||||
<span className="flex flex-col">
|
||||
<ChevronUp
|
||||
className={cn(
|
||||
'-mb-1 hover:text-foreground-lighter',
|
||||
column.getIsSorted() === 'asc' ? 'text-foreground' : 'text-foreground-muted'
|
||||
)}
|
||||
/>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'-mt-1 hover:text-foreground-lighter',
|
||||
column.getIsSorted() === 'desc' ? 'text-foreground' : 'text-foreground-muted'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<span>{title}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
Reusable Column Components
|
||||
|
||||
This folder is dedicated for components that can be used as within the `columns.tsx` structure.
|
||||
|
||||
Components:
|
||||
|
||||
- `DataTableColumnTimestamp`: A hover card with the _Unix timestamp_, _UTC_, _timezone_ and _relative_ (x days ago) values
|
||||
- `DataTableLevelIndicator`: A simple colored square to indicate `"error" | "warning" | "success" | "info"` data
|
||||
|
||||
Usage Example:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { DataTableColumnLevelIndicator } from "@/components/data-table/data-table-column/data-table-column-level-indicator";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
export type ColumnSchema = {
|
||||
// ...
|
||||
};
|
||||
|
||||
export const columns: ColumnDef<ColumnSchema>[] = [
|
||||
{
|
||||
accessorKey: "level",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
const level = row.getValue("level") as (typeof LEVELS)[number];
|
||||
return <DataTableColumnLevelIndicator level={level} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
export function DataTableColumnLatency({ value }: { value: number }) {
|
||||
return (
|
||||
<div className="font-mono">
|
||||
{new Intl.NumberFormat("en-US", { maximumFractionDigits: 3 }).format(
|
||||
value,
|
||||
)}
|
||||
<span className="text-muted-foreground">ms</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { _LEVELS } from 'components/interfaces/DataTableDemo/constants/levels'
|
||||
import { getLevelColor } from 'components/interfaces/DataTableDemo/lib/request/level'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export function DataTableColumnLevelIndicator({
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
value: (typeof _LEVELS)[number]
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center', className)}>
|
||||
<div className={cn('h-2.5 w-2.5 rounded-[2px]', getLevelColor(value).bg)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { regions } from 'components/interfaces/DataTableDemo/constants/region'
|
||||
|
||||
interface DataTableColumnRegionProps {
|
||||
value: string
|
||||
showFlag?: boolean
|
||||
reverse?: boolean
|
||||
}
|
||||
|
||||
export function DataTableColumnRegion({
|
||||
value,
|
||||
showFlag = false,
|
||||
reverse = false,
|
||||
}: DataTableColumnRegionProps) {
|
||||
const region = regions[value] ?? { label: value, flag: '' }
|
||||
return reverse ? (
|
||||
<>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{showFlag && region.flag} {region.label}
|
||||
</span>{' '}
|
||||
<span>{value}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{value}</span>{' '}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{showFlag && region.flag} {region.label}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { getStatusColor } from 'components/interfaces/DataTableDemo/lib/request/status-code'
|
||||
import { cn } from 'ui'
|
||||
import { Minus } from 'lucide-react'
|
||||
import { color } from 'framer-motion'
|
||||
|
||||
export function DataTableColumnStatusCode({
|
||||
value,
|
||||
level,
|
||||
className,
|
||||
}: {
|
||||
value?: number | string
|
||||
level?: string
|
||||
className?: string
|
||||
}) {
|
||||
// console.log('level', level, level === 'warning' ? value : 'no warning')
|
||||
const colors = getStatusColor(level)
|
||||
if (!value) {
|
||||
return <Minus className="h-4 w-4 text-muted-foreground/50" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center relative', className)}>
|
||||
<div
|
||||
className={cn(
|
||||
'px-1 py-[0.03rem] rounded-md',
|
||||
'flex items-center justify-center relative font-mono',
|
||||
colors.text,
|
||||
colors.bg,
|
||||
colors.border
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
|
||||
import { useCopyToClipboard } from 'components/interfaces/DataTableDemo/hooks/use-copy-to-clipboard'
|
||||
import { cn } from 'ui'
|
||||
import { UTCDate } from '@date-fns/utc'
|
||||
import { HoverCardPortal } from '@radix-ui/react-hover-card'
|
||||
import { format, formatDistanceToNowStrict } from 'date-fns'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import type { ComponentPropsWithoutRef } from 'react'
|
||||
|
||||
type HoverCardContentProps = ComponentPropsWithoutRef<typeof HoverCardContent>
|
||||
|
||||
interface DataTableColumnTimestampProps {
|
||||
date: Date
|
||||
side?: HoverCardContentProps['side']
|
||||
sideOffset?: HoverCardContentProps['sideOffset']
|
||||
align?: HoverCardContentProps['align']
|
||||
alignOffset?: HoverCardContentProps['alignOffset']
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function DataTableColumnTimestamp({
|
||||
date,
|
||||
side = 'right',
|
||||
align = 'start',
|
||||
alignOffset = -4,
|
||||
sideOffset,
|
||||
className,
|
||||
}: DataTableColumnTimestampProps) {
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
|
||||
return (
|
||||
<HoverCard openDelay={0} closeDelay={0}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className={cn('whitespace-nowrap font-mono', className)}>
|
||||
{format(date, 'LLL dd, y HH:mm:ss')}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
{/* REMINDER: allows us to port the content to the document.body, which is helpful when using opacity-50 on the row element */}
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent className="z-10 w-auto p-2" {...{ side, align, alignOffset, sideOffset }}>
|
||||
<dl className="flex flex-col gap-1">
|
||||
<Row value={String(date.getTime())} label="Timestamp" />
|
||||
<Row value={format(new UTCDate(date), 'LLL dd, y HH:mm:ss')} label="UTC" />
|
||||
<Row value={format(date, 'LLL dd, y HH:mm:ss')} label={timezone} />
|
||||
<Row value={formatDistanceToNowStrict(date, { addSuffix: true })} label="Relative" />
|
||||
</dl>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ value, label }: { value: string; label: string }) {
|
||||
const { copy, isCopied } = useCopyToClipboard()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex items-center justify-between gap-4 text-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
copy(value)
|
||||
}}
|
||||
>
|
||||
<dt className="text-muted-foreground">{label}</dt>
|
||||
<dd className="flex items-center gap-1 truncate font-mono">
|
||||
<span className="invisible group-hover:visible">
|
||||
{!isCopied ? <Copy className="h-3 w-3" /> : <Check className="h-3 w-3" />}
|
||||
</span>
|
||||
{value}
|
||||
</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { InputWithAddons } from 'components/interfaces/DataTableDemo/components/custom/input-with-addons'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
import { Checkbox_Shadcn_ as Checkbox } from 'ui'
|
||||
import { Label_Shadcn_ as Label } from 'ui'
|
||||
import { Skeleton } from 'ui'
|
||||
import { formatCompactNumber } from 'components/interfaces/DataTableDemo/lib/format'
|
||||
import { cn } from 'ui'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import type { DataTableCheckboxFilterField } from './types'
|
||||
|
||||
export function DataTableFilterCheckbox<TData>({
|
||||
value: _value,
|
||||
options,
|
||||
component,
|
||||
}: DataTableCheckboxFilterField<TData>) {
|
||||
const value = _value as string
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const { table, columnFilters, isLoading, getFacetedUniqueValues } = useDataTable()
|
||||
const column = table.getColumn(value)
|
||||
// REMINDER: avoid using column?.getFilterValue()
|
||||
const filterValue = columnFilters.find((i) => i.id === value)?.value
|
||||
const facetedValue = getFacetedUniqueValues?.(table, value) || column?.getFacetedUniqueValues()
|
||||
|
||||
const Component = component
|
||||
|
||||
// filter out the options based on the input value
|
||||
const filterOptions = options?.filter(
|
||||
(option) => inputValue === '' || option.label.toLowerCase().includes(inputValue.toLowerCase())
|
||||
)
|
||||
|
||||
// CHECK: it could be filterValue or searchValue
|
||||
const filters = filterValue ? (Array.isArray(filterValue) ? filterValue : [filterValue]) : []
|
||||
|
||||
// REMINDER: if no options are defined, while fetching data, we should show a skeleton
|
||||
if (isLoading && !filterOptions?.length)
|
||||
return (
|
||||
<div className="grid divide-y rounded-lg border border-border">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className="flex items-center justify-between gap-2 px-2 py-2.5">
|
||||
<Skeleton className="h-4 w-4 rounded-sm" />
|
||||
<Skeleton className="h-4 w-full rounded-sm" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!filterOptions?.length) return null
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
{options && options.length > 4 ? (
|
||||
<InputWithAddons
|
||||
placeholder="Search"
|
||||
leading={<Search className="mt-0.5 h-4 w-4" />}
|
||||
containerClassName="h-9 rounded-lg"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
/>
|
||||
) : null}
|
||||
{/* FIXME: due to the added max-h and overflow-y-auto, the hover state and border is laying on top of the scroll bar */}
|
||||
<div className="max-h-[200px] overflow-y-auto rounded-lg border border-border empty:border-none">
|
||||
{filterOptions
|
||||
// TODO: we shoudn't sort the options here, instead filterOptions should be sorted by default
|
||||
// .sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((option, index) => {
|
||||
const checked = filters.includes(option.value)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={String(option.value)}
|
||||
className={cn(
|
||||
'group relative flex items-center space-x-2 px-2 py-2.5 hover:bg-accent/50',
|
||||
index !== filterOptions.length - 1 ? 'border-b' : undefined
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={`${value}-${option.value}`}
|
||||
checked={checked}
|
||||
onCheckedChange={(checked) => {
|
||||
const newValue = checked
|
||||
? [...(filters || []), option.value]
|
||||
: filters?.filter((value) => option.value !== value)
|
||||
column?.setFilterValue(newValue?.length ? newValue : undefined)
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`${value}-${option.value}`}
|
||||
className="flex w-full items-center justify-center gap-1 truncate text-foreground/70 group-hover:text-accent-foreground"
|
||||
>
|
||||
{Component ? (
|
||||
<Component {...option} />
|
||||
) : (
|
||||
<span className="truncate font-normal">{option.label}</span>
|
||||
)}
|
||||
<span className="ml-auto flex items-center justify-center font-mono text-xs">
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-4 w-4" />
|
||||
) : facetedValue?.has(option.value) ? (
|
||||
formatCompactNumber(facetedValue.get(option.value) || 0)
|
||||
) : null}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => column?.setFilterValue([option.value])}
|
||||
className={cn(
|
||||
'absolute inset-y-0 right-0 hidden font-normal text-muted-foreground backdrop-blur-sm hover:text-foreground group-hover:block',
|
||||
'rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
|
||||
)}
|
||||
>
|
||||
<span className="px-2">only</span>
|
||||
</button>
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-400
@@ -1,400 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
import {
|
||||
Command_Shadcn_ as Command,
|
||||
CommandEmpty_Shadcn_ as CommandEmpty,
|
||||
CommandGroup_Shadcn_ as CommandGroup,
|
||||
CommandInput_Shadcn_ as CommandInput,
|
||||
CommandItem_Shadcn_ as CommandItem,
|
||||
CommandList_Shadcn_ as CommandList,
|
||||
CommandSeparator_Shadcn_ as CommandSeparator,
|
||||
} from 'ui'
|
||||
import { Separator } from 'ui'
|
||||
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
|
||||
import { useLocalStorage } from 'components/interfaces/DataTableDemo/hooks/use-local-storage'
|
||||
import { formatCompactNumber } from 'components/interfaces/DataTableDemo/lib/format'
|
||||
import { cn } from 'ui'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { LoaderCircle, Search, X } from 'lucide-react'
|
||||
import { ParserBuilder } from 'nuqs'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { DataTableFilterField } from '../types'
|
||||
import {
|
||||
columnFiltersParser,
|
||||
getFieldOptions,
|
||||
getFilterValue,
|
||||
getWordByCaretPosition,
|
||||
replaceInputByFieldType,
|
||||
} from './utils'
|
||||
|
||||
// FIXME: there is an issue on cmdk if I wanna only set a single slider value...
|
||||
|
||||
interface DataTableFilterCommandProps {
|
||||
// TODO: maybe use generics for the parser
|
||||
searchParamsParser: Record<string, ParserBuilder<any>>
|
||||
}
|
||||
|
||||
export function DataTableFilterCommand({ searchParamsParser }: DataTableFilterCommandProps) {
|
||||
const { table, isLoading, filterFields: _filterFields, getFacetedUniqueValues } = useDataTable()
|
||||
const columnFilters = table.getState().columnFilters
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const [currentWord, setCurrentWord] = useState<string>('')
|
||||
const filterFields = useMemo(
|
||||
() => _filterFields?.filter((i) => !i.commandDisabled),
|
||||
[_filterFields]
|
||||
)
|
||||
const columnParser = useMemo(
|
||||
() => columnFiltersParser({ searchParamsParser, filterFields }),
|
||||
[searchParamsParser, filterFields]
|
||||
)
|
||||
const [inputValue, setInputValue] = useState<string>(columnParser.serialize(columnFilters))
|
||||
const [lastSearches, setLastSearches] = useLocalStorage<
|
||||
{
|
||||
search: string
|
||||
timestamp: number
|
||||
}[]
|
||||
>('data-table-command', [])
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: we could check for ARRAY_DELIMITER or SLIDER_DELIMITER to auto-set filter when typing
|
||||
if (currentWord !== '' && open) return
|
||||
// reset
|
||||
if (currentWord !== '' && !open) setCurrentWord('')
|
||||
// avoid recursion
|
||||
if (inputValue.trim() === '' && !open) return
|
||||
|
||||
const searchParams = columnParser.parse(inputValue)
|
||||
|
||||
const currentFilters = table.getState().columnFilters
|
||||
const currentEnabledFilters = currentFilters.filter((filter) => {
|
||||
const field = _filterFields?.find((field) => field.value === filter.id)
|
||||
return !field?.commandDisabled
|
||||
})
|
||||
const currentDisabledFilters = currentFilters.filter((filter) => {
|
||||
const field = _filterFields?.find((field) => field.value === filter.id)
|
||||
return field?.commandDisabled
|
||||
})
|
||||
|
||||
const commandDisabledFilterKeys = currentDisabledFilters.reduce(
|
||||
(prev, curr) => {
|
||||
prev[curr.id] = curr.value
|
||||
return prev
|
||||
},
|
||||
{} as Record<string, unknown>
|
||||
)
|
||||
|
||||
for (const key of Object.keys(searchParams)) {
|
||||
const value = searchParams[key as keyof typeof searchParams]
|
||||
table.getColumn(key)?.setFilterValue(value)
|
||||
}
|
||||
const currentFiltersToReset = currentEnabledFilters.filter((filter) => {
|
||||
return !(filter.id in searchParams)
|
||||
})
|
||||
for (const filter of currentFiltersToReset) {
|
||||
table.getColumn(filter.id)?.setFilterValue(undefined)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputValue, open, currentWord])
|
||||
|
||||
useEffect(() => {
|
||||
// REMINDER: only update the input value if the command is closed (avoids jumps while open)
|
||||
if (!open) {
|
||||
setInputValue(columnParser.serialize(columnFilters))
|
||||
}
|
||||
}, [columnFilters, filterFields, open])
|
||||
|
||||
useHotKey(() => setOpen((open) => !open), 'k')
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
inputRef?.current?.focus()
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'group flex w-full items-center rounded-lg border border-input bg-background px-3 text-muted-foreground ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 hover:bg-accent/50 hover:text-accent-foreground',
|
||||
open ? 'hidden' : 'visible'
|
||||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircle className="mr-2 h-4 w-4 shrink-0 animate-spin text-muted-foreground opacity-50 group-hover:text-popover-foreground" />
|
||||
) : (
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 text-muted-foreground opacity-50 group-hover:text-popover-foreground" />
|
||||
)}
|
||||
<span className="h-11 w-full max-w-sm truncate py-3 text-left text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 md:max-w-xl lg:max-w-4xl xl:max-w-5xl">
|
||||
{inputValue.trim() ? (
|
||||
<span className="text-foreground">{inputValue}</span>
|
||||
) : (
|
||||
<span>Search data table...</span>
|
||||
)}
|
||||
</span>
|
||||
<Kbd className="ml-auto text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>K</span>
|
||||
</Kbd>
|
||||
</button>
|
||||
<Command
|
||||
className={cn(
|
||||
'overflow-visible rounded-lg border border-border shadow-md [&>div]:border-none bg',
|
||||
open ? 'visible' : 'hidden'
|
||||
)}
|
||||
filter={(value, search, keywords) =>
|
||||
getFilterValue({ value, search, keywords, currentWord })
|
||||
}
|
||||
// loop
|
||||
>
|
||||
<CommandInput
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onValueChange={setInputValue}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') inputRef?.current?.blur()
|
||||
}}
|
||||
onBlur={() => {
|
||||
setOpen(false)
|
||||
// FIXME: doesnt reflect the jumps
|
||||
// FIXME: will save non-existing searches
|
||||
// TODO: extract into function
|
||||
const search = inputValue.trim()
|
||||
if (!search) return
|
||||
const timestamp = Date.now()
|
||||
const searchIndex = lastSearches.findIndex((item) => item.search === search)
|
||||
if (searchIndex !== -1) {
|
||||
lastSearches[searchIndex].timestamp = timestamp
|
||||
setLastSearches(lastSearches)
|
||||
return
|
||||
}
|
||||
setLastSearches([...lastSearches, { search, timestamp }])
|
||||
return
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const caretPosition = e.currentTarget?.selectionStart || -1
|
||||
const value = e.currentTarget?.value || ''
|
||||
const word = getWordByCaretPosition({ value, caretPosition })
|
||||
setCurrentWord(word)
|
||||
}}
|
||||
placeholder="Search data table..."
|
||||
className="text-foreground"
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="absolute top-2 z-50 w-full overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-md outline-none animate-in">
|
||||
{/* default height is 300px but in case of more, we'd like to tease the user */}
|
||||
<CommandList className="max-h-[310px] bg-surface-100">
|
||||
<CommandGroup heading="Filter">
|
||||
{filterFields.map((field) => {
|
||||
if (typeof field.value !== 'string') return null
|
||||
if (inputValue.includes(`${field.value}:`)) return null
|
||||
// TBD: should we handle this in the component?
|
||||
return (
|
||||
<CommandItem
|
||||
key={field.value}
|
||||
value={field.value}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onSelect={(value) => {
|
||||
setInputValue((prev) => {
|
||||
if (currentWord.trim() === '') {
|
||||
const input = `${prev}${value}`
|
||||
return `${input}:`
|
||||
}
|
||||
// lots of cheat
|
||||
const isStarting = currentWord === prev
|
||||
const prefix = isStarting ? '' : ' '
|
||||
const input = prev.replace(`${prefix}${currentWord}`, `${prefix}${value}`)
|
||||
return `${input}:`
|
||||
})
|
||||
setCurrentWord(`${value}:`)
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
{field.value}
|
||||
<CommandItemSuggestions field={field} />
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Query">
|
||||
{filterFields?.map((field) => {
|
||||
if (typeof field.value !== 'string') return null
|
||||
if (!currentWord.includes(`${field.value}:`)) return null
|
||||
|
||||
const column = table.getColumn(field.value)
|
||||
const facetedValue =
|
||||
getFacetedUniqueValues?.(table, field.value) || column?.getFacetedUniqueValues()
|
||||
|
||||
const options = getFieldOptions({ field })
|
||||
|
||||
return options.map((optionValue) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={`${String(field.value)}:${optionValue}`}
|
||||
value={`${String(field.value)}:${optionValue}`}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onSelect={(value) => {
|
||||
setInputValue((prev) =>
|
||||
replaceInputByFieldType({
|
||||
prev,
|
||||
currentWord,
|
||||
optionValue,
|
||||
value,
|
||||
field,
|
||||
})
|
||||
)
|
||||
setCurrentWord('')
|
||||
}}
|
||||
>
|
||||
{`${optionValue}`}
|
||||
{facetedValue?.has(optionValue) ? (
|
||||
<span className="ml-auto font-mono text-muted-foreground">
|
||||
{formatCompactNumber(facetedValue.get(optionValue) || 0)}
|
||||
</span>
|
||||
) : null}
|
||||
</CommandItem>
|
||||
)
|
||||
})
|
||||
})}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Suggestions">
|
||||
{lastSearches
|
||||
?.sort((a, b) => b.timestamp - a.timestamp)
|
||||
.slice(0, 5)
|
||||
.map((item) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={`suggestion:${item.search}`}
|
||||
value={`suggestion:${item.search}`}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onSelect={(value) => {
|
||||
const search = value.replace('suggestion:', '')
|
||||
setInputValue(`${search} `)
|
||||
setCurrentWord('')
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
{item.search}
|
||||
<span className="ml-auto truncate text-muted-foreground/80 group-aria-[selected=true]:block">
|
||||
{formatDistanceToNow(item.timestamp, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
// TODO: extract into function
|
||||
setLastSearches(lastSearches.filter((i) => i.search !== item.search))
|
||||
}}
|
||||
className="ml-1 hidden rounded-md p-0.5 hover:bg-background group-aria-[selected=true]:block"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
</CommandList>
|
||||
<div
|
||||
className="bg-surface-100 flex flex-wrap justify-between gap-3 border-t bg-accent/50 px-2 py-1.5 text-sm text-accent-foreground"
|
||||
cmdk-footer=""
|
||||
>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<span>
|
||||
Use <Kbd variant="outline">↑</Kbd> <Kbd variant="outline">↓</Kbd> to navigate
|
||||
</span>
|
||||
<span>
|
||||
<Kbd variant="outline">Enter</Kbd> to query
|
||||
</span>
|
||||
<span>
|
||||
<Kbd variant="outline">Esc</Kbd> to close
|
||||
</span>
|
||||
<Separator orientation="vertical" className="my-auto h-3" />
|
||||
<span>
|
||||
Union: <Kbd variant="outline">regions:a,b</Kbd>
|
||||
</span>
|
||||
<span>
|
||||
Range: <Kbd variant="outline">p95:59-340</Kbd>
|
||||
</span>
|
||||
</div>
|
||||
{lastSearches.length ? (
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-accent-foreground"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onClick={() => setLastSearches([])}
|
||||
>
|
||||
Clear suggestions
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Command>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// function CommandItemType<TData>
|
||||
|
||||
function CommandItemSuggestions<TData>({ field }: { field: DataTableFilterField<TData> }) {
|
||||
const { table, getFacetedMinMaxValues, getFacetedUniqueValues } = useDataTable()
|
||||
const value = field.value as string
|
||||
switch (field.type) {
|
||||
case 'checkbox': {
|
||||
return (
|
||||
<span className="ml-1 hidden truncate text-muted-foreground/80 group-aria-[selected=true]:block">
|
||||
{getFacetedUniqueValues
|
||||
? Array.from(getFacetedUniqueValues(table, value)?.keys() || [])
|
||||
.map((value) => `[${value}]`)
|
||||
.join(' ')
|
||||
: field.options?.map(({ value }) => `[${value}]`).join(' ')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
case 'slider': {
|
||||
const [min, max] = getFacetedMinMaxValues?.(table, value) || [field.min, field.max]
|
||||
return (
|
||||
<span className="ml-1 hidden truncate text-muted-foreground/80 group-aria-[selected=true]:block">
|
||||
[{min} - {max}]
|
||||
</span>
|
||||
)
|
||||
}
|
||||
case 'input': {
|
||||
return (
|
||||
<span className="ml-1 hidden truncate text-muted-foreground/80 group-aria-[selected=true]:block">
|
||||
[{`${String(field.value)}`} input]
|
||||
</span>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
-262
@@ -1,262 +0,0 @@
|
||||
import {
|
||||
ARRAY_DELIMITER,
|
||||
RANGE_DELIMITER,
|
||||
SLIDER_DELIMITER,
|
||||
} from 'components/interfaces/DataTableDemo/lib/delimiters'
|
||||
import { isArrayOfDates } from 'components/interfaces/DataTableDemo/lib/is-array'
|
||||
import { ColumnFiltersState } from '@tanstack/react-table'
|
||||
import { ParserBuilder } from 'nuqs'
|
||||
import type { DataTableFilterField } from '../types'
|
||||
|
||||
/**
|
||||
* Extracts the word from the given string at the specified caret position.
|
||||
*/
|
||||
export function getWordByCaretPosition({
|
||||
value,
|
||||
caretPosition,
|
||||
}: {
|
||||
value: string
|
||||
caretPosition: number
|
||||
}) {
|
||||
let start = caretPosition
|
||||
let end = caretPosition
|
||||
|
||||
while (start > 0 && value[start - 1] !== ' ') start--
|
||||
while (end < value.length && value[end] !== ' ') end++
|
||||
|
||||
const word = value.substring(start, end)
|
||||
return word
|
||||
}
|
||||
|
||||
export function replaceInputByFieldType<TData>({
|
||||
prev,
|
||||
currentWord,
|
||||
optionValue,
|
||||
value,
|
||||
field,
|
||||
}: {
|
||||
prev: string
|
||||
currentWord: string
|
||||
optionValue?: string | number | boolean | undefined // FIXME: use DataTableFilterField<TData>["options"][number];
|
||||
value: string
|
||||
field: DataTableFilterField<TData>
|
||||
}) {
|
||||
switch (field.type) {
|
||||
case 'checkbox': {
|
||||
if (currentWord.includes(ARRAY_DELIMITER)) {
|
||||
const words = currentWord.split(ARRAY_DELIMITER)
|
||||
words[words.length - 1] = `${optionValue}`
|
||||
const input = prev.replace(currentWord, words.join(ARRAY_DELIMITER))
|
||||
return `${input.trim()} `
|
||||
}
|
||||
}
|
||||
case 'slider': {
|
||||
if (currentWord.includes(SLIDER_DELIMITER)) {
|
||||
const words = currentWord.split(SLIDER_DELIMITER)
|
||||
words[words.length - 1] = `${optionValue}`
|
||||
const input = prev.replace(currentWord, words.join(SLIDER_DELIMITER))
|
||||
return `${input.trim()} `
|
||||
}
|
||||
}
|
||||
case 'timerange': {
|
||||
if (currentWord.includes(RANGE_DELIMITER)) {
|
||||
const words = currentWord.split(RANGE_DELIMITER)
|
||||
words[words.length - 1] = `${optionValue}`
|
||||
const input = prev.replace(currentWord, words.join(RANGE_DELIMITER))
|
||||
return `${input.trim()} `
|
||||
}
|
||||
}
|
||||
default: {
|
||||
const input = prev.replace(currentWord, value)
|
||||
return `${input.trim()} `
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getFieldOptions<TData>({ field }: { field: DataTableFilterField<TData> }) {
|
||||
switch (field.type) {
|
||||
case 'slider': {
|
||||
return field.options?.length
|
||||
? field.options
|
||||
.map(({ value }) => value)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.filter(notEmpty)
|
||||
: Array.from({ length: field.max - field.min + 1 }, (_, i) => field.min + i) || []
|
||||
}
|
||||
default: {
|
||||
return field.options?.map(({ value }) => value).filter(notEmpty) || []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getFilterValue({
|
||||
value,
|
||||
search,
|
||||
currentWord,
|
||||
}: {
|
||||
value: string
|
||||
search: string
|
||||
keywords?: string[] | undefined
|
||||
currentWord: string
|
||||
}): number {
|
||||
/**
|
||||
* @example value "suggestion:public:true regions,ams,gru,fra"
|
||||
*/
|
||||
if (value.startsWith('suggestion:')) {
|
||||
const rawValue = value.toLowerCase().replace('suggestion:', '')
|
||||
if (rawValue.includes(search)) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
/** */
|
||||
if (value.toLowerCase().includes(currentWord.toLowerCase())) return 1
|
||||
|
||||
/**
|
||||
* @example checkbox [filter, query] = ["regions", "ams,gru,fra"]
|
||||
* @example slider [filter, query] = ["p95", "0-3000"]
|
||||
* @example input [filter, query] = ["name", "api"]
|
||||
*/
|
||||
const [filter, query] = currentWord.toLowerCase().split(':')
|
||||
if (query && value.startsWith(`${filter}:`)) {
|
||||
if (query.includes(ARRAY_DELIMITER)) {
|
||||
/**
|
||||
* array of n elements
|
||||
* @example queries = ["ams", "gru", "fra"]
|
||||
*/
|
||||
const queries = query.split(ARRAY_DELIMITER)
|
||||
const rawValue = value.toLowerCase().replace(`${filter}:`, '')
|
||||
if (queries.some((item, i) => item === rawValue && i !== queries.length - 1)) return 0
|
||||
if (queries.some((item) => rawValue.includes(item))) return 1
|
||||
}
|
||||
if (query.includes(SLIDER_DELIMITER)) {
|
||||
/**
|
||||
* range between 2 elements
|
||||
* @example queries = ["0", "3000"]
|
||||
*/
|
||||
const queries = query.split(SLIDER_DELIMITER)
|
||||
const rawValue = value.toLowerCase().replace(`${filter}:`, '')
|
||||
|
||||
const rawValueAsNumber = Number.parseInt(rawValue)
|
||||
const queryAsNumber = Number.parseInt(queries[0])
|
||||
|
||||
if (queryAsNumber < rawValueAsNumber) {
|
||||
if (rawValue.includes(queries[1])) return 1
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
const rawValue = value.toLowerCase().replace(`${filter}:`, '')
|
||||
if (rawValue.includes(query)) return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export function getFieldValueByType<TData>({
|
||||
field,
|
||||
value,
|
||||
}: {
|
||||
field?: DataTableFilterField<TData>
|
||||
value: unknown
|
||||
}) {
|
||||
if (!field) return null
|
||||
|
||||
switch (field.type) {
|
||||
case 'slider': {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(SLIDER_DELIMITER)
|
||||
}
|
||||
return value
|
||||
}
|
||||
case 'checkbox': {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(ARRAY_DELIMITER)
|
||||
}
|
||||
// REMINER: inversed logic
|
||||
if (typeof value === 'string') {
|
||||
return value.split(ARRAY_DELIMITER)
|
||||
}
|
||||
return value
|
||||
}
|
||||
case 'timerange': {
|
||||
if (Array.isArray(value)) {
|
||||
if (isArrayOfDates(value)) {
|
||||
return value.map((date) => date.getTime()).join(RANGE_DELIMITER)
|
||||
}
|
||||
return value.join(RANGE_DELIMITER)
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.getTime()
|
||||
}
|
||||
return value
|
||||
}
|
||||
default: {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
|
||||
return value !== null && value !== undefined
|
||||
}
|
||||
|
||||
export function columnFiltersParser<TData>({
|
||||
searchParamsParser,
|
||||
filterFields,
|
||||
}: {
|
||||
searchParamsParser: Record<string, ParserBuilder<any>>
|
||||
filterFields: DataTableFilterField<TData>[]
|
||||
}) {
|
||||
return {
|
||||
parse: (inputValue: string) => {
|
||||
// Use regex to properly extract field:value pairs
|
||||
// This properly handles spaces within values
|
||||
const filterPairs: Record<string, string> = {}
|
||||
|
||||
// This regex properly extracts field:value pairs where:
|
||||
// 1. Values can contain spaces (both normal spaces and URL-encoded + characters)
|
||||
// 2. It finds word characters followed by a colon as the field name
|
||||
// 3. It then captures everything (including spaces and + characters) until
|
||||
// it finds another field:value pattern or the end of string
|
||||
// This solves the issue with values like "edge function" or "edge+function" in URLs
|
||||
const regex = /(\w+):([^]*?)(?=\s+\w+:|$)/g
|
||||
let match
|
||||
|
||||
console.log('DataTableFilterCommand parsing input:', inputValue)
|
||||
|
||||
while ((match = regex.exec(inputValue)) !== null) {
|
||||
const [_, fieldName, fieldValue] = match
|
||||
if (fieldName && fieldValue) {
|
||||
filterPairs[fieldName] = fieldValue.trim()
|
||||
console.log(`Parsed filter pair: ${fieldName} = ${fieldValue.trim()}`)
|
||||
}
|
||||
}
|
||||
|
||||
const searchParams = Object.entries(filterPairs).reduce(
|
||||
(prev, [key, value]) => {
|
||||
const parser = searchParamsParser[key]
|
||||
if (!parser) return prev
|
||||
|
||||
prev[key] = parser.parse(value)
|
||||
return prev
|
||||
},
|
||||
{} as Record<string, unknown>
|
||||
)
|
||||
|
||||
return searchParams
|
||||
},
|
||||
serialize: (columnFilters: ColumnFiltersState) => {
|
||||
const values = columnFilters.reduce((prev, curr) => {
|
||||
const { commandDisabled } = filterFields?.find((field) => curr.id === field.value) || {
|
||||
commandDisabled: true,
|
||||
} // if column filter is not found, disable the command by default
|
||||
const parser = searchParamsParser[curr.id]
|
||||
|
||||
if (commandDisabled || !parser) return prev
|
||||
|
||||
return `${prev}${curr.id}:${parser.serialize(curr.value)} `
|
||||
}, '')
|
||||
|
||||
return values
|
||||
},
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerClose,
|
||||
DrawerTrigger,
|
||||
} from 'ui'
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
|
||||
import { Button } from 'ui'
|
||||
import { FilterIcon } from 'lucide-react'
|
||||
import { DataTableFilterControls } from './data-table-filter-controls'
|
||||
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
|
||||
import React from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
|
||||
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
|
||||
import { useMediaQuery } from 'components/interfaces/DataTableDemo/hooks/use-media-query'
|
||||
|
||||
export function DataTableFilterControlsDrawer() {
|
||||
const triggerButtonRef = React.useRef<HTMLButtonElement>(null)
|
||||
const isMobile = useMediaQuery('(max-width: 640px)')
|
||||
|
||||
useHotKey(() => {
|
||||
triggerButtonRef.current?.click()
|
||||
}, 'b')
|
||||
|
||||
return (
|
||||
<Drawer>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
ref={isMobile ? triggerButtonRef : null}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<FilterIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
Toggle controls with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>B</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DrawerContent className="max-h-[calc(100dvh-4rem)]">
|
||||
<VisuallyHidden>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Filters</DrawerTitle>
|
||||
<DrawerDescription>Adjust your table filters</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
</VisuallyHidden>
|
||||
<div className="px-4 flex-1 overflow-y-auto">
|
||||
<DataTableFilterControls />
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline" className="w-full">
|
||||
Close
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Accordion_Shadcn_ as Accordion,
|
||||
AccordionContent_Shadcn_ as AccordionContent,
|
||||
AccordionItem_Shadcn_ as AccordionItem,
|
||||
AccordionTrigger_Shadcn_ as AccordionTrigger,
|
||||
} from 'ui'
|
||||
import type React from 'react'
|
||||
import { DataTableFilterResetButton } from './data-table-filter-reset-button'
|
||||
import { DataTableFilterCheckbox } from './data-table-filter-checkbox'
|
||||
import { DataTableFilterSlider } from './data-table-filter-slider'
|
||||
import { DataTableFilterInput } from './data-table-filter-input'
|
||||
import { DataTableFilterTimerange } from './data-table-filter-timerange'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
|
||||
// FIXME: use @container (especially for the slider element) to restructure elements
|
||||
|
||||
// TODO: only pass the columns to generate the filters!
|
||||
// https://tanstack.com/table/v8/docs/framework/react/examples/filters
|
||||
|
||||
export function DataTableFilterControls() {
|
||||
const { filterFields } = useDataTable()
|
||||
return (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={filterFields
|
||||
?.filter(({ defaultOpen }) => defaultOpen)
|
||||
?.map(({ value }) => value as string)}
|
||||
>
|
||||
{filterFields?.map((field) => {
|
||||
const value = field.value as string
|
||||
return (
|
||||
<AccordionItem key={value} value={value} className="border-none">
|
||||
<AccordionTrigger className="w-full px-2 py-0 hover:no-underline data-[state=closed]:text-muted-foreground data-[state=open]:text-foreground focus-within:data-[state=closed]:text-foreground hover:data-[state=closed]:text-foreground">
|
||||
<div className="flex w-full items-center justify-between gap-2 truncate py-2 pr-2">
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<p className="text-sm font-medium">{field.label}</p>
|
||||
{value !== field.label.toLowerCase() && !field.commandDisabled ? (
|
||||
<p className="mt-px truncate font-mono text-[10px] text-muted-foreground">
|
||||
{value}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<DataTableFilterResetButton {...field} />
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{/* REMINDER: avoid the focus state to be cut due to overflow-hidden */}
|
||||
{/* REMINDER: need to move within here because of accordion height animation */}
|
||||
<div className="p-1">
|
||||
{(() => {
|
||||
switch (field.type) {
|
||||
case 'checkbox': {
|
||||
return <DataTableFilterCheckbox {...field} />
|
||||
}
|
||||
case 'slider': {
|
||||
return <DataTableFilterSlider {...field} />
|
||||
}
|
||||
case 'input': {
|
||||
return <DataTableFilterInput {...field} />
|
||||
}
|
||||
case 'timerange': {
|
||||
return <DataTableFilterTimerange {...field} />
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)
|
||||
})}
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { DataTableInputFilterField } from './types'
|
||||
import { InputWithAddons } from 'components/interfaces/DataTableDemo/components/custom/input-with-addons'
|
||||
import { Label_Shadcn_ as Label } from 'ui'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'components/interfaces/DataTableDemo/hooks/use-debounce'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
|
||||
function getFilter(filterValue: unknown) {
|
||||
return typeof filterValue === 'string' ? filterValue : null
|
||||
}
|
||||
|
||||
export function DataTableFilterInput<TData>({ value: _value }: DataTableInputFilterField<TData>) {
|
||||
const value = _value as string
|
||||
const { table, columnFilters } = useDataTable()
|
||||
const column = table.getColumn(value)
|
||||
const filterValue = columnFilters.find((i) => i.id === value)?.value
|
||||
const filters = getFilter(filterValue)
|
||||
const [input, setInput] = useState<string | null>(filters)
|
||||
|
||||
const debouncedInput = useDebounce(input, 500)
|
||||
|
||||
useEffect(() => {
|
||||
const newValue = debouncedInput?.trim() === '' ? null : debouncedInput
|
||||
if (debouncedInput === null) return
|
||||
column?.setFilterValue(newValue)
|
||||
}, [debouncedInput])
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInput?.trim() !== filters) {
|
||||
setInput(filters)
|
||||
}
|
||||
}, [filters])
|
||||
|
||||
return (
|
||||
<div className="grid w-full gap-1.5">
|
||||
<Label htmlFor={value} className="sr-only px-2 text-muted-foreground">
|
||||
{value}
|
||||
</Label>
|
||||
<InputWithAddons
|
||||
placeholder="Search"
|
||||
leading={<Search className="mt-0.5 h-4 w-4" />}
|
||||
containerClassName="h-9 rounded-lg"
|
||||
name={value}
|
||||
id={value}
|
||||
value={input || ''}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { DataTableFilterField } from './types'
|
||||
import { Button } from 'ui'
|
||||
import { X } from 'lucide-react'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
|
||||
export function DataTableFilterResetButton<TData>({ value: _value }: DataTableFilterField<TData>) {
|
||||
const { columnFilters, table } = useDataTable()
|
||||
const value = _value as string
|
||||
const column = table.getColumn(value)
|
||||
const filterValue = columnFilters.find((f) => f.id === value)?.value
|
||||
|
||||
// TODO: check if we could useMemo
|
||||
const filters = filterValue ? (Array.isArray(filterValue) ? filterValue : [filterValue]) : []
|
||||
|
||||
if (filters.length === 0) return null
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="outline"
|
||||
className="h-5 rounded-full px-1.5 py-1 font-mono text-[10px]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
column?.setFilterValue(undefined)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.code === 'Enter') {
|
||||
column?.setFilterValue(undefined)
|
||||
}
|
||||
}}
|
||||
icon={<X className="h-2.5 w-2.5 text-muted-foreground" />}
|
||||
asChild
|
||||
>
|
||||
{/* REMINDER: `AccordionTrigger` is also a button(!) and we get Hydration error when rendering button within button */}
|
||||
<div role="button" tabIndex={0}>
|
||||
<span>{filters.length}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { DataTableSliderFilterField } from './types'
|
||||
import { InputWithAddons } from 'components/interfaces/DataTableDemo/components/custom/input-with-addons'
|
||||
import { Label_Shadcn_ as Label } from 'ui'
|
||||
import { Slider } from 'components/interfaces/DataTableDemo/components/custom/slider'
|
||||
import { isArrayOfNumbers } from 'components/interfaces/DataTableDemo/lib/is-array'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'components/interfaces/DataTableDemo/hooks/use-debounce'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
|
||||
function getFilter(filterValue: unknown) {
|
||||
return typeof filterValue === 'number'
|
||||
? [filterValue, filterValue]
|
||||
: Array.isArray(filterValue) && isArrayOfNumbers(filterValue)
|
||||
? filterValue.length === 1
|
||||
? [filterValue[0], filterValue[0]]
|
||||
: filterValue
|
||||
: null
|
||||
}
|
||||
|
||||
// TODO: discuss if we even need the `defaultMin` and `defaultMax`
|
||||
export function DataTableFilterSlider<TData>({
|
||||
value: _value,
|
||||
min: defaultMin,
|
||||
max: defaultMax,
|
||||
}: DataTableSliderFilterField<TData>) {
|
||||
const value = _value as string
|
||||
const { table, columnFilters, getFacetedMinMaxValues } = useDataTable()
|
||||
const column = table.getColumn(value)
|
||||
const filterValue = columnFilters.find((i) => i.id === value)?.value
|
||||
const filters = getFilter(filterValue)
|
||||
const [input, setInput] = useState<number[] | null>(filters)
|
||||
const [min, max] = getFacetedMinMaxValues?.(table, value) ||
|
||||
column?.getFacetedMinMaxValues() || [defaultMin, defaultMax]
|
||||
|
||||
const debouncedInput = useDebounce(input, 500)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInput?.length === 2) {
|
||||
column?.setFilterValue(debouncedInput)
|
||||
}
|
||||
}, [debouncedInput])
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInput?.length !== 2) {
|
||||
} else if (!filters) {
|
||||
setInput(null)
|
||||
} else if (debouncedInput[0] !== filters[0] || debouncedInput[1] !== filters[1]) {
|
||||
setInput(filters)
|
||||
}
|
||||
}, [filters])
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="grid w-full gap-1.5">
|
||||
<Label htmlFor={`min-${value}`} className="px-2 text-muted-foreground">
|
||||
Min.
|
||||
</Label>
|
||||
<InputWithAddons
|
||||
placeholder="from"
|
||||
trailing="ms"
|
||||
containerClassName="mb-2 h-9 rounded-lg"
|
||||
type="number"
|
||||
name={`min-${value}`}
|
||||
id={`min-${value}`}
|
||||
value={`${input?.[0] ?? min}`}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={(e) => setInput((prev) => [Number(e.target.value), prev?.[1] || max])}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full gap-1.5">
|
||||
<Label htmlFor={`max-${value}`} className="px-2 text-muted-foreground">
|
||||
Max.
|
||||
</Label>
|
||||
<InputWithAddons
|
||||
placeholder="to"
|
||||
trailing="ms"
|
||||
containerClassName="mb-2 h-9 rounded-lg"
|
||||
type="number"
|
||||
name={`max-${value}`}
|
||||
id={`max-${value}`}
|
||||
value={`${input?.[1] ?? max}`}
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={(e) => setInput((prev) => [prev?.[0] || min, Number(e.target.value)])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
value={input?.length === 2 ? input : [min, max]}
|
||||
onValueChange={(values) => setInput(values)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import type { DataTableTimerangeFilterField } from './types'
|
||||
import { isArrayOfDates } from 'components/interfaces/DataTableDemo/lib/is-array'
|
||||
import { DatePickerWithRange } from 'components/interfaces/DataTableDemo/components/custom/date-picker-with-range'
|
||||
import type { DateRange } from 'react-day-picker'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
|
||||
export function DataTableFilterTimerange<TData>({
|
||||
value: _value,
|
||||
presets,
|
||||
}: DataTableTimerangeFilterField<TData>) {
|
||||
const value = _value as string
|
||||
const { table, columnFilters } = useDataTable()
|
||||
const column = table.getColumn(value)
|
||||
const filterValue = columnFilters.find((i) => i.id === value)?.value
|
||||
|
||||
const date: DateRange | undefined = useMemo(
|
||||
() =>
|
||||
filterValue instanceof Date
|
||||
? { from: filterValue, to: undefined }
|
||||
: Array.isArray(filterValue) && isArrayOfDates(filterValue)
|
||||
? { from: filterValue?.[0], to: filterValue?.[1] }
|
||||
: undefined,
|
||||
[filterValue]
|
||||
)
|
||||
|
||||
const setDate = (date: DateRange | undefined) => {
|
||||
if (!date) return // TODO: remove from search params if columnFilter is removed
|
||||
if (date.from && !date.to) {
|
||||
column?.setFilterValue([date.from])
|
||||
}
|
||||
if (date.to && date.from) {
|
||||
column?.setFilterValue([date.from, date.to])
|
||||
}
|
||||
}
|
||||
|
||||
return <DatePickerWithRange {...{ date, setDate, presets }} />
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Select_Shadcn_ as Select,
|
||||
SelectContent_Shadcn_ as SelectContent,
|
||||
SelectItem_Shadcn_ as SelectItem,
|
||||
SelectTrigger_Shadcn_ as SelectTrigger,
|
||||
SelectValue_Shadcn_ as SelectValue,
|
||||
} from 'ui'
|
||||
import { Button } from 'ui'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export function DataTablePagination() {
|
||||
const { table, pagination, columnFilters } = useDataTable()
|
||||
const pageCount = useMemo(() => table.getPageCount(), [columnFilters])
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end space-x-4 md:space-x-6 lg:space-x-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium">Rows per page</p>
|
||||
<Select
|
||||
value={`${pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value))
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue placeholder={pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center justify-center text-sm font-medium">
|
||||
Page {pagination.pageIndex + 1} of {pageCount}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => {
|
||||
table.setPageIndex(0)
|
||||
}}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<ChevronsLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
table.previousPage()
|
||||
}}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
table.nextPage()
|
||||
}}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => {
|
||||
table.setPageIndex(table.getPageCount() - 1)
|
||||
}}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<ChevronsRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
import { DataTableFilterField } from 'components/interfaces/DataTableDemo/components/data-table/types'
|
||||
import type {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
SortingState,
|
||||
Table,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table'
|
||||
import { createContext, useContext, useMemo } from 'react'
|
||||
import { ControlsProvider } from 'components/interfaces/DataTableDemo/providers/controls'
|
||||
|
||||
// REMINDER: read about how to move controlled state out of the useReactTable hook
|
||||
// https://github.com/TanStack/table/discussions/4005#discussioncomment-7303569
|
||||
|
||||
interface DataTableStateContextType {
|
||||
columnFilters: ColumnFiltersState
|
||||
sorting: SortingState
|
||||
rowSelection: RowSelectionState
|
||||
columnOrder: string[]
|
||||
columnVisibility: VisibilityState
|
||||
pagination: PaginationState
|
||||
enableColumnOrdering: boolean
|
||||
}
|
||||
|
||||
interface DataTableBaseContextType<TData = unknown, TValue = unknown> {
|
||||
table: Table<TData>
|
||||
filterFields: DataTableFilterField<TData>[]
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
isLoading?: boolean
|
||||
getFacetedUniqueValues?: (table: Table<TData>, columnId: string) => Map<string, number>
|
||||
getFacetedMinMaxValues?: (table: Table<TData>, columnId: string) => undefined | [number, number]
|
||||
}
|
||||
|
||||
interface DataTableContextType<TData = unknown, TValue = unknown>
|
||||
extends DataTableStateContextType,
|
||||
DataTableBaseContextType<TData, TValue> {}
|
||||
|
||||
export const DataTableContext = createContext<DataTableContextType<any, any> | null>(null)
|
||||
|
||||
export function DataTableProvider<TData, TValue>({
|
||||
children,
|
||||
...props
|
||||
}: Partial<DataTableStateContextType> &
|
||||
DataTableBaseContextType<TData, TValue> & {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
...props,
|
||||
columnFilters: props.columnFilters ?? [],
|
||||
sorting: props.sorting ?? [],
|
||||
rowSelection: props.rowSelection ?? {},
|
||||
columnOrder: props.columnOrder ?? [],
|
||||
columnVisibility: props.columnVisibility ?? {},
|
||||
pagination: props.pagination ?? { pageIndex: 0, pageSize: 10 },
|
||||
enableColumnOrdering: props.enableColumnOrdering ?? false,
|
||||
}),
|
||||
[
|
||||
props.columnFilters,
|
||||
props.sorting,
|
||||
props.rowSelection,
|
||||
props.columnOrder,
|
||||
props.columnVisibility,
|
||||
props.pagination,
|
||||
props.table,
|
||||
props.filterFields,
|
||||
props.columns,
|
||||
props.enableColumnOrdering,
|
||||
props.isLoading,
|
||||
props.getFacetedUniqueValues,
|
||||
props.getFacetedMinMaxValues,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<DataTableContext.Provider value={value}>
|
||||
<ControlsProvider>{children}</ControlsProvider>
|
||||
</DataTableContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useDataTable<TData, TValue>() {
|
||||
const context = useContext(DataTableContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useDataTable must be used within a DataTableProvider')
|
||||
}
|
||||
|
||||
return context as DataTableContextType<TData, TValue>
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { X } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
|
||||
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
|
||||
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
|
||||
export function DataTableResetButton() {
|
||||
const { table } = useDataTable()
|
||||
useHotKey(table.resetColumnFilters, 'Escape')
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => table.resetColumnFilters()}
|
||||
icon={<X className="h-4 w-4" />}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>
|
||||
Reset filters with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>Esc</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { cn } from 'ui'
|
||||
import { Table } from '@tanstack/react-table'
|
||||
import { DataTableSheetRowAction } from './data-table-sheet-row-action'
|
||||
import { DataTableFilterField, SheetField } from '../types'
|
||||
import { SheetDetailsContentSkeleton } from './data-table-sheet-skeleton'
|
||||
|
||||
interface DataTableSheetContentProps<TData, TMeta> extends React.HTMLAttributes<HTMLDListElement> {
|
||||
data?: TData
|
||||
table: Table<TData>
|
||||
fields: SheetField<TData, TMeta>[]
|
||||
filterFields: DataTableFilterField<TData>[]
|
||||
// totalRows: number;
|
||||
// filterRows: number;
|
||||
// totalRowsFetched: number;
|
||||
metadata?: TMeta & {
|
||||
totalRows: number
|
||||
filterRows: number
|
||||
totalRowsFetched: number
|
||||
}
|
||||
}
|
||||
|
||||
export function DataTableSheetContent<TData, TMeta>({
|
||||
data,
|
||||
table,
|
||||
className,
|
||||
fields,
|
||||
filterFields,
|
||||
metadata,
|
||||
...props
|
||||
}: DataTableSheetContentProps<TData, TMeta>) {
|
||||
if (!data) return <SheetDetailsContentSkeleton fields={fields} />
|
||||
|
||||
return (
|
||||
<dl className={cn('divide-y', className)} {...props}>
|
||||
{fields.map((field) => {
|
||||
if (field.condition && !field.condition(data)) return null
|
||||
|
||||
const Component = field.component
|
||||
const value = String(data[field.id])
|
||||
|
||||
return (
|
||||
<div key={field.id.toString()}>
|
||||
{field.type === 'readonly' ? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex gap-4 my-1 py-1 text-sm justify-between items-center w-full',
|
||||
field.className
|
||||
)}
|
||||
>
|
||||
<dt className="shrink-0 text-muted-foreground">{field.label}</dt>
|
||||
<dd className="font-mono w-full text-right">
|
||||
{Component ? <Component {...data} metadata={metadata} /> : value}
|
||||
</dd>
|
||||
</div>
|
||||
) : (
|
||||
<DataTableSheetRowAction
|
||||
fieldValue={field.id}
|
||||
filterFields={filterFields}
|
||||
value={value}
|
||||
table={table}
|
||||
className={cn(
|
||||
'flex gap-4 my-1 py-1 text-sm justify-between items-center w-full',
|
||||
field.className
|
||||
)}
|
||||
>
|
||||
<dt className="shrink-0 text-muted-foreground">{field.label}</dt>
|
||||
<dd className="font-mono w-full text-right">
|
||||
{Component ? <Component {...data} metadata={metadata} /> : value}
|
||||
</dd>
|
||||
</DataTableSheetRowAction>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
|
||||
export const MemoizedDataTableSheetContent = React.memo(DataTableSheetContent, (prev, next) => {
|
||||
// REMINDER: only check if data is the same, rest is useless
|
||||
return prev.data === next.data
|
||||
}) as typeof DataTableSheetContent
|
||||
-174
@@ -1,174 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
import { ChevronDown, ChevronUp, X } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Separator,
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
|
||||
export interface DataTableSheetDetailsProps {
|
||||
title?: React.ReactNode
|
||||
titleClassName?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function DataTableSheetDetails({
|
||||
title,
|
||||
titleClassName,
|
||||
children,
|
||||
}: DataTableSheetDetailsProps) {
|
||||
const { table, rowSelection, isLoading } = useDataTable()
|
||||
|
||||
const selectedRowKey = Object.keys(rowSelection)?.[0]
|
||||
|
||||
const selectedRow = React.useMemo(() => {
|
||||
if (isLoading && !selectedRowKey) return
|
||||
return table.getCoreRowModel().flatRows.find((row) => row.id === selectedRowKey)
|
||||
}, [selectedRowKey, isLoading])
|
||||
|
||||
const index = table.getCoreRowModel().flatRows.findIndex((row) => row.id === selectedRow?.id)
|
||||
|
||||
const nextId = React.useMemo(
|
||||
() => table.getCoreRowModel().flatRows[index + 1]?.id,
|
||||
[index, isLoading]
|
||||
)
|
||||
|
||||
const prevId = React.useMemo(
|
||||
() => table.getCoreRowModel().flatRows[index - 1]?.id,
|
||||
[index, isLoading]
|
||||
)
|
||||
|
||||
const onPrev = React.useCallback(() => {
|
||||
if (prevId) table.setRowSelection({ [prevId]: true })
|
||||
}, [prevId, isLoading])
|
||||
|
||||
const onNext = React.useCallback(() => {
|
||||
if (nextId) table.setRowSelection({ [nextId]: true })
|
||||
}, [nextId, isLoading])
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (!selectedRowKey) return
|
||||
|
||||
// REMINDER: prevent dropdown navigation inside of sheet to change row selection
|
||||
const activeElement = document.activeElement
|
||||
const isMenuActive = activeElement?.closest('[role="menu"]')
|
||||
|
||||
if (isMenuActive) return
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
onPrev()
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
onNext()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', down)
|
||||
return () => document.removeEventListener('keydown', down)
|
||||
}, [selectedRowKey, onNext, onPrev])
|
||||
|
||||
return (
|
||||
// <Sheet
|
||||
// open={!!selectedRowKey}
|
||||
// onOpenChange={() => {
|
||||
// // REMINDER: focus back to the row that was selected
|
||||
// // We need to manually focus back due to missing Trigger component
|
||||
// const el = selectedRowKey ? document.getElementById(selectedRowKey) : null
|
||||
// table.resetRowSelection()
|
||||
|
||||
// // REMINDER: when navigating between tabs in the sheet and exit the sheet, the tab gets lost
|
||||
// // We need a minimal delay to allow the sheet to close before focusing back to the row
|
||||
// setTimeout(() => el?.focus(), 0)
|
||||
// }}
|
||||
// >
|
||||
// <SheetContent
|
||||
// // onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
// className="overflow-y-auto p-0 sm:max-w-md"
|
||||
// hideClose
|
||||
// >
|
||||
// <SheetHeader className="sticky top-0 z-10 border-b bg-background p-4">
|
||||
<div className="relative bg-sidebar">
|
||||
<div className="flex items-center justify-between gap-2 px-5 py-1">
|
||||
<h5 className={cn(titleClassName, 'truncate text-left')}>
|
||||
{isLoading && !selectedRowKey ? <Skeleton className="h-7 w-36" /> : title}
|
||||
</h5>
|
||||
<div className="flex h-7 items-center gap-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
disabled={!prevId}
|
||||
onClick={onPrev}
|
||||
className="px-0"
|
||||
icon={<ChevronUp />}
|
||||
>
|
||||
<span className="sr-only">Previous</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Navigate <Kbd variant="outline">↑</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
disabled={!nextId}
|
||||
onClick={onNext}
|
||||
className="px-0"
|
||||
icon={<ChevronDown />}
|
||||
>
|
||||
<span className="sr-only">Next</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Navigate <Kbd variant="outline">↓</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Separator orientation="vertical" className="mx-1" />
|
||||
{/* <SheetClose autoFocus={true} asChild> */}
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
onClick={() => table.resetRowSelection()}
|
||||
className="px-0"
|
||||
icon={<X />}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
{/* </SheetClose> */}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
import { DropdownMenu } from 'ui'
|
||||
import { Copy } from 'lucide-react'
|
||||
import { DropdownMenuContent, DropdownMenuSeparator } from 'ui'
|
||||
import { DropdownMenuGroup, DropdownMenuItem } from 'ui'
|
||||
import { DropdownMenuTrigger } from 'ui'
|
||||
import { CalendarSearch, ChevronLeft, ChevronRight, Equal, Search } from 'lucide-react'
|
||||
import { CalendarDays } from 'lucide-react'
|
||||
import { startOfDay } from 'date-fns'
|
||||
import { startOfHour } from 'date-fns'
|
||||
import { useCopyToClipboard } from 'components/interfaces/DataTableDemo/hooks/use-copy-to-clipboard'
|
||||
import { endOfDay } from 'date-fns'
|
||||
import { Table } from '@tanstack/react-table'
|
||||
import { CalendarClock } from 'lucide-react'
|
||||
import { endOfHour } from 'date-fns'
|
||||
import { cn } from 'ui'
|
||||
import { DataTableFilterField } from '../types'
|
||||
|
||||
interface DataTableSheetRowActionProps<TData, TFields extends DataTableFilterField<TData>>
|
||||
extends React.ComponentPropsWithRef<typeof DropdownMenuTrigger> {
|
||||
fieldValue: TFields['value']
|
||||
filterFields: TFields[]
|
||||
value: string | number
|
||||
table: Table<TData>
|
||||
}
|
||||
|
||||
export function DataTableSheetRowAction<TData, TFields extends DataTableFilterField<TData>>({
|
||||
fieldValue,
|
||||
filterFields,
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
table,
|
||||
onKeyDown,
|
||||
...props
|
||||
}: DataTableSheetRowActionProps<TData, TFields>) {
|
||||
const { copy, isCopied } = useCopyToClipboard()
|
||||
const field = filterFields.find((field) => field.value === fieldValue)
|
||||
const column = table.getColumn(fieldValue.toString())
|
||||
|
||||
if (!field || !column) return null
|
||||
|
||||
function renderOptions() {
|
||||
if (!field) return null
|
||||
switch (field.type) {
|
||||
case 'checkbox':
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
// FIXME:
|
||||
const filterValue = column?.getFilterValue() as undefined | Array<unknown>
|
||||
const newValue = filterValue?.includes(value)
|
||||
? filterValue
|
||||
: [...(filterValue || []), value]
|
||||
|
||||
column?.setFilterValue(newValue)
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Search size={16} />
|
||||
Include
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
case 'input':
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => column?.setFilterValue(value)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Search size={16} />
|
||||
Include
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
case 'slider':
|
||||
return (
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={() => column?.setFilterValue([0, value])}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{/* FIXME: change icon as it is not clear */}
|
||||
<ChevronLeft size={16} />
|
||||
Less or equal than
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => column?.setFilterValue([value, 5000])}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{/* FIXME: change icon as it is not clear */}
|
||||
<ChevronRight size={16} />
|
||||
Greater or equal than
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => column?.setFilterValue([value])}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Equal size={16} />
|
||||
Equal to
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
)
|
||||
case 'timerange':
|
||||
const date = new Date(value)
|
||||
return (
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={() => column?.setFilterValue([date])}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<CalendarSearch size={16} />
|
||||
Exact timestamp
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const start = startOfHour(date)
|
||||
const end = endOfHour(date)
|
||||
column?.setFilterValue([start, end])
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<CalendarClock size={16} />
|
||||
Same hour
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const start = startOfDay(date)
|
||||
const end = endOfDay(date)
|
||||
column?.setFilterValue([start, end])
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<CalendarDays size={16} />
|
||||
Same day
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
'rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
'relative',
|
||||
className
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
// REMINDER: default behavior is to open the dropdown menu
|
||||
// But because we use it to navigate between rows, we need to prevent it
|
||||
// and only use "Enter" to select the option
|
||||
e.preventDefault()
|
||||
}
|
||||
onKeyDown?.(e)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{isCopied ? (
|
||||
<div className="absolute inset-0 bg-background/70 place-content-center">Value copied</div>
|
||||
) : null}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" side="left">
|
||||
{renderOptions()}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => copy(String(value), { timeout: 1000 })}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Copy size={16} />
|
||||
Copy value
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import { Skeleton } from 'ui'
|
||||
import { cn } from 'ui'
|
||||
import { SheetField } from '../types'
|
||||
|
||||
interface SheetDetailsContentSkeletonProps<TData, TMeta> {
|
||||
fields: SheetField<TData, TMeta>[]
|
||||
}
|
||||
|
||||
export function SheetDetailsContentSkeleton<TData, TMeta>({
|
||||
fields,
|
||||
}: SheetDetailsContentSkeletonProps<TData, TMeta>) {
|
||||
return (
|
||||
<dl className="divide-y">
|
||||
{fields.map((field) => (
|
||||
<div
|
||||
key={field.id.toString()}
|
||||
className="flex gap-4 py-2 text-sm justify-between items-center"
|
||||
>
|
||||
<dt className="shrink-0 text-muted-foreground">{field.label}</dt>
|
||||
<div>
|
||||
<Skeleton className={cn('h-5 w-52', field.skeletonClassName)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
import { Skeleton } from 'ui'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from 'components/interfaces/DataTableDemo/components/custom/table'
|
||||
|
||||
interface DataTableSkeletonProps {
|
||||
/**
|
||||
* Number of rows to render
|
||||
* @default 10
|
||||
*/
|
||||
rows?: number
|
||||
}
|
||||
|
||||
export function DataTableSkeleton({ rows = 10 }: DataTableSkeletonProps) {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/50">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead>
|
||||
<Skeleton className="my-1.5 h-4 w-24" />
|
||||
</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">
|
||||
<Skeleton className="my-1.5 h-4 w-32" />
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell">
|
||||
<Skeleton className="my-1.5 h-4 w-16" />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Skeleton className="my-1.5 h-4 w-20" />
|
||||
</TableHead>
|
||||
<TableHead className="flex items-center justify-end" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{new Array(rows).fill(0).map((_, i) => (
|
||||
<TableRow key={i} className="hover:bg-transparent">
|
||||
<TableCell>
|
||||
<Skeleton className="my-1.5 h-4 w-full max-w-[10rem]" />
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Skeleton className="my-1.5 h-4 w-full max-w-[13rem]" />
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
<Skeleton className="my-1.5 h-4 w-24" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="my-1.5 h-4 w-full max-w-[10rem]" />
|
||||
</TableCell>
|
||||
<TableCell className="flex justify-end">
|
||||
<Skeleton className="my-1.5 h-5 w-5" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
import { Button } from 'ui'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
|
||||
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
|
||||
import { formatCompactNumber } from 'components/interfaces/DataTableDemo/lib/format'
|
||||
import { useControls } from 'components/interfaces/DataTableDemo/providers/controls'
|
||||
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { DataTableFilterControlsDrawer } from './data-table-filter-controls-drawer'
|
||||
import { DataTableResetButton } from './data-table-reset-button'
|
||||
import { DataTableViewOptions } from './data-table-view-options'
|
||||
|
||||
interface DataTableToolbarProps {
|
||||
renderActions?: () => React.ReactNode
|
||||
}
|
||||
|
||||
export function DataTableToolbar({ renderActions }: DataTableToolbarProps) {
|
||||
const { table, isLoading, columnFilters } = useDataTable()
|
||||
const { open, setOpen } = useControls()
|
||||
useHotKey(() => setOpen((prev) => !prev), 'b')
|
||||
const filters = table.getState().columnFilters
|
||||
|
||||
const rows = useMemo(
|
||||
() => ({
|
||||
total: table.getCoreRowModel().rows.length,
|
||||
filtered: table.getFilteredRowModel().rows.length,
|
||||
}),
|
||||
[isLoading, columnFilters]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="outline"
|
||||
icon={<PanelLeftClose className="h-4 w-4" />}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
className="hidden gap-2 sm:flex"
|
||||
>
|
||||
{open ? (
|
||||
<>
|
||||
<span className="hidden md:block">Hide Controls</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="hidden md:block">Show Controls</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
Toggle controls with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>B</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="block sm:hidden">
|
||||
<DataTableFilterControlsDrawer />
|
||||
</div>
|
||||
<div>
|
||||
<p className="hidden text-sm text-muted-foreground sm:block">
|
||||
<span className="font-mono font-medium">{formatCompactNumber(rows.filtered)}</span> of{' '}
|
||||
<span className="font-mono font-medium">{formatCompactNumber(rows.total)}</span> row(s){' '}
|
||||
<span className="sr-only sm:not-sr-only">filtered</span>
|
||||
</p>
|
||||
<p className="block text-sm text-muted-foreground sm:hidden">
|
||||
<span className="font-mono font-medium">{formatCompactNumber(rows.filtered)}</span>{' '}
|
||||
row(s)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{filters.length ? <DataTableResetButton /> : null}
|
||||
{renderActions?.()}
|
||||
<DataTableViewOptions />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Check, GripVertical, Settings2 } from 'lucide-react'
|
||||
|
||||
import { Button } from 'ui'
|
||||
import {
|
||||
Command_Shadcn_ as Command,
|
||||
CommandEmpty_Shadcn_ as CommandEmpty,
|
||||
CommandGroup_Shadcn_ as CommandGroup,
|
||||
CommandInput_Shadcn_ as CommandInput,
|
||||
CommandItem_Shadcn_ as CommandItem,
|
||||
CommandList_Shadcn_ as CommandList,
|
||||
} from 'ui'
|
||||
import {
|
||||
Popover_Shadcn_ as Popover,
|
||||
PopoverContent_Shadcn_ as PopoverContent,
|
||||
PopoverTrigger_Shadcn_ as PopoverTrigger,
|
||||
} from 'ui'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { cn } from 'ui'
|
||||
import {
|
||||
Sortable,
|
||||
SortableDragHandle,
|
||||
SortableItem,
|
||||
} from 'components/interfaces/DataTableDemo/components/custom/sortable'
|
||||
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
|
||||
export function DataTableViewOptions() {
|
||||
const { table, enableColumnOrdering } = useDataTable()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [drag, setDrag] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const columnOrder = table.getState().columnOrder
|
||||
|
||||
const sortedColumns = useMemo(
|
||||
() =>
|
||||
table.getAllColumns().sort((a, b) => {
|
||||
return columnOrder.indexOf(a.id) - columnOrder.indexOf(b.id)
|
||||
}),
|
||||
[columnOrder]
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button type="outline" size="tiny" role="combobox" aria-expanded={open} className="h-9 w-9">
|
||||
<Settings2 className="h-4 w-4" />
|
||||
<span className="sr-only">View</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" align="end" className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput value={search} onValueChange={setSearch} placeholder="Search options..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No option found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<Sortable
|
||||
value={sortedColumns.map((c) => ({ id: c.id }))}
|
||||
onValueChange={(items) => table.setColumnOrder(items.map((c) => c.id))}
|
||||
overlay={<div className="h-8 w-full rounded-md bg-muted/60" />}
|
||||
onDragStart={() => setDrag(true)}
|
||||
onDragEnd={() => setDrag(false)}
|
||||
onDragCancel={() => setDrag(false)}
|
||||
>
|
||||
{sortedColumns
|
||||
.filter(
|
||||
(column) => typeof column.accessorFn !== 'undefined' && column.getCanHide()
|
||||
)
|
||||
.map((column) => (
|
||||
<SortableItem key={column.id} value={column.id} asChild>
|
||||
<CommandItem
|
||||
value={column.id}
|
||||
onSelect={() => column.toggleVisibility(!column.getIsVisible())}
|
||||
className={'capitalize'}
|
||||
disabled={drag}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
column.getIsVisible()
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<Check className={cn('h-4 w-4')} />
|
||||
</div>
|
||||
<span>{column.columnDef.meta?.label || column.id}</span>
|
||||
{enableColumnOrdering && !search ? (
|
||||
<SortableDragHandle
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="ml-auto size-5 text-muted-foreground hover:text-foreground focus:bg-muted focus:text-foreground"
|
||||
>
|
||||
<GripVertical className="size-4" aria-hidden="true" />
|
||||
</SortableDragHandle>
|
||||
) : null}
|
||||
</CommandItem>
|
||||
</SortableItem>
|
||||
))}
|
||||
</Sortable>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { JSX } from "react";
|
||||
|
||||
export type SearchParams = {
|
||||
[key: string]: string | string[] | undefined;
|
||||
};
|
||||
|
||||
export type DatePreset = {
|
||||
label: string;
|
||||
from: Date;
|
||||
to: Date;
|
||||
shortcut: string;
|
||||
};
|
||||
|
||||
// TODO: we could type the value(!) especially when using enums
|
||||
export type Option = {
|
||||
label: string;
|
||||
value: string | boolean | number | undefined;
|
||||
};
|
||||
|
||||
export type Input = {
|
||||
type: "input";
|
||||
options?: Option[];
|
||||
};
|
||||
|
||||
export type Checkbox = {
|
||||
type: "checkbox";
|
||||
component?: (props: Option) => JSX.Element | null;
|
||||
options?: Option[];
|
||||
};
|
||||
|
||||
export type Slider = {
|
||||
type: "slider";
|
||||
min: number;
|
||||
max: number;
|
||||
// if options is undefined, we will provide all the steps between min and max
|
||||
options?: Option[];
|
||||
};
|
||||
|
||||
export type Timerange = {
|
||||
type: "timerange";
|
||||
options?: Option[]; // required for TS
|
||||
presets?: DatePreset[];
|
||||
};
|
||||
|
||||
export type Base<TData> = {
|
||||
label: string;
|
||||
value: keyof TData;
|
||||
/**
|
||||
* Defines if the accordion in the filter bar is open by default
|
||||
*/
|
||||
defaultOpen?: boolean;
|
||||
/**
|
||||
* Defines if the command input is disabled for this field
|
||||
*/
|
||||
commandDisabled?: boolean;
|
||||
};
|
||||
|
||||
export type DataTableCheckboxFilterField<TData> = Base<TData> & Checkbox;
|
||||
export type DataTableSliderFilterField<TData> = Base<TData> & Slider;
|
||||
export type DataTableInputFilterField<TData> = Base<TData> & Input;
|
||||
export type DataTableTimerangeFilterField<TData> = Base<TData> & Timerange;
|
||||
|
||||
export type DataTableFilterField<TData> =
|
||||
| DataTableCheckboxFilterField<TData>
|
||||
| DataTableSliderFilterField<TData>
|
||||
| DataTableInputFilterField<TData>
|
||||
| DataTableTimerangeFilterField<TData>;
|
||||
|
||||
/** ----------------------------------------- */
|
||||
|
||||
export type SheetField<TData, TMeta = Record<string, unknown>> = {
|
||||
id: keyof TData;
|
||||
label: string;
|
||||
// FIXME: rethink that! I dont think we need this as there is no input type
|
||||
// REMINDER: readonly if we only want to copy the value (e.g. uuid)
|
||||
// TODO: we might have some values that are not in the data but can be computed
|
||||
type: "readonly" | "input" | "checkbox" | "slider" | "timerange";
|
||||
component?: (
|
||||
// REMINDER: this is used to pass additional data like the `InfiniteQueryMeta`
|
||||
props: TData & {
|
||||
metadata?: {
|
||||
totalRows: number;
|
||||
filterRows: number;
|
||||
totalRowsFetched: number;
|
||||
} & TMeta;
|
||||
}
|
||||
) => JSX.Element | null | string;
|
||||
condition?: (props: TData) => boolean;
|
||||
className?: string;
|
||||
skeletonClassName?: string;
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
// TODO: check if we can move to /data-table-filter-command/utils.ts
|
||||
import type { ColumnFiltersState } from "@tanstack/react-table";
|
||||
import { z } from "zod";
|
||||
import type { DataTableFilterField } from "./types";
|
||||
import {
|
||||
ARRAY_DELIMITER,
|
||||
RANGE_DELIMITER,
|
||||
SLIDER_DELIMITER,
|
||||
} from "@/lib/delimiters";
|
||||
|
||||
export function deserialize<T extends z.AnyZodObject>(schema: T) {
|
||||
const castToSchema = z.preprocess((val) => {
|
||||
if (typeof val !== "string") return val;
|
||||
return val
|
||||
.trim()
|
||||
.split(" ")
|
||||
.reduce((prev, curr) => {
|
||||
const [name, value] = curr.split(":");
|
||||
if (!value || !name) return prev;
|
||||
prev[name] = value;
|
||||
return prev;
|
||||
}, {} as Record<string, unknown>);
|
||||
}, schema);
|
||||
return (value: string) => castToSchema.safeParse(value);
|
||||
}
|
||||
|
||||
// export function serialize<T extends z.AnyZodObject>(schema: T) {
|
||||
// return (value: z.infer<T>) =>
|
||||
// schema
|
||||
// .transform((val) => {
|
||||
// Object.keys(val).reduce((prev, curr) => {
|
||||
// if (Array.isArray(val[curr])) {
|
||||
// return `${prev}${curr}:${val[curr].join(",")} `;
|
||||
// }
|
||||
// return `${prev}${curr}:${val[curr]} `;
|
||||
// }, "");
|
||||
// })
|
||||
// .safeParse(value);
|
||||
// }
|
||||
|
||||
export function serializeColumFilters<TData>(
|
||||
columnFilters: ColumnFiltersState,
|
||||
filterFields?: DataTableFilterField<TData>[]
|
||||
) {
|
||||
return columnFilters.reduce((prev, curr) => {
|
||||
const { type, commandDisabled } = filterFields?.find(
|
||||
(field) => curr.id === field.value
|
||||
) || { commandDisabled: true }; // if column filter is not found, disable the command by default
|
||||
|
||||
if (commandDisabled) return prev;
|
||||
|
||||
if (Array.isArray(curr.value)) {
|
||||
if (type === "slider") {
|
||||
return `${prev}${curr.id}:${curr.value.join(SLIDER_DELIMITER)} `;
|
||||
}
|
||||
if (type === "checkbox") {
|
||||
return `${prev}${curr.id}:${curr.value.join(ARRAY_DELIMITER)} `;
|
||||
}
|
||||
if (type === "timerange") {
|
||||
return `${prev}${curr.id}:${curr.value.join(RANGE_DELIMITER)} `;
|
||||
}
|
||||
}
|
||||
|
||||
return `${prev}${curr.id}:${curr.value} `;
|
||||
}, "");
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
export function Bluesky(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>Bluesky</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
export function Github(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>GitHub</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
export function X(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<title>X</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { ModeToggle } from '@/components/theme/toggle-mode'
|
||||
import { Button } from 'ui'
|
||||
import NextLink from 'next/link'
|
||||
import { Link } from '@/components/custom/link'
|
||||
import { Github } from '@/components/icons/github'
|
||||
import { X } from '@/components/icons/x'
|
||||
import { Bluesky } from '@/components/icons/bluesky'
|
||||
|
||||
export function SocialsFooter() {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex justify-center items-center gap-2 p-1">
|
||||
<Button variant="ghost" size="sm" className="w-9 px-0" asChild>
|
||||
<NextLink href="https://github.com/openstatusHQ/data-table-filters">
|
||||
<Github className="h-4 w-4" />
|
||||
</NextLink>
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="w-9 px-0" asChild>
|
||||
<NextLink href="https://twitter.com/openstatusHQ">
|
||||
<X className="h-4 w-4" />
|
||||
</NextLink>
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="w-9 px-0" asChild>
|
||||
<NextLink href="https://bsky.app/profile/openstatus.dev">
|
||||
<Bluesky className="h-4 w-4" />
|
||||
</NextLink>
|
||||
</Button>
|
||||
<ModeToggle className="[&>svg]:h-4 [&>svg]:w-4" />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-center text-sm">
|
||||
Powered by <Link href="https://openstatus.dev">OpenStatus</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { ArrowRight, ChevronRight } from "lucide-react";
|
||||
import { Link } from "@/components/custom/link";
|
||||
|
||||
export function TopBanner() {
|
||||
return (
|
||||
<div className="group bg-muted/50 text-foreground border-b">
|
||||
<div className="container mx-auto px-4 py-2 sm:px-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-8">
|
||||
<p className="flex items-center justify-center gap-2">
|
||||
<Link href="/infinite" className="no-underline">
|
||||
<span className="font-semibold">New Example: </span>
|
||||
<span className="underline underline-offset-4 decoration-muted-foreground group-hover:decoration-foreground">
|
||||
Infinite Scroll
|
||||
</span>{" "}
|
||||
<span className="mr-1">Now Available, Inspired by Vercel</span>
|
||||
<ArrowRight className="relative mb-[1px] inline h-4 w-0 transition-all group-hover:w-4" />
|
||||
<ChevronRight className="relative mb-[1px] inline h-4 w-4 transition-all group-hover:w-0" />
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type { ThemeProviderProps } from "next-themes";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
import { Button, ButtonProps } from 'ui'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ui'
|
||||
import { Laptop, Moon, Sun } from 'lucide-react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export function ModeToggle({ className, ...props }: ButtonProps) {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="text" size="small" className={cn('w-9 px-0', className)} {...props}>
|
||||
<Sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
<Sun className="mr-2 h-4 w-4" />
|
||||
<span>Light</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
<Moon className="mr-2 h-4 w-4" />
|
||||
<span>Dark</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
<Laptop className="mr-2 h-4 w-4" />
|
||||
<span>System</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
|
||||
))
|
||||
AccordionItem.displayName = 'AccordionItem'
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -1,33 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -1,49 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -1,59 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { DayPicker } from 'react-day-picker'
|
||||
|
||||
import { cn } from 'ui'
|
||||
import { buttonVariants } from 'ui'
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
||||
|
||||
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn('p-3', className)}
|
||||
classNames={{
|
||||
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
|
||||
month: 'space-y-4',
|
||||
caption: 'flex justify-center pt-1 relative items-center',
|
||||
caption_label: 'text-sm font-medium',
|
||||
nav: 'space-x-1 flex items-center',
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
|
||||
),
|
||||
nav_button_previous: 'absolute left-1',
|
||||
nav_button_next: 'absolute right-1',
|
||||
table: 'w-full border-collapse space-y-1',
|
||||
head_row: 'flex',
|
||||
head_cell: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
|
||||
row: 'flex w-full mt-2',
|
||||
cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
|
||||
day: cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-9 w-9 p-0 font-normal aria-selected:opacity-100'
|
||||
),
|
||||
day_range_end: 'day-range-end',
|
||||
day_selected:
|
||||
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
|
||||
day_today: 'bg-accent text-accent-foreground',
|
||||
day_outside:
|
||||
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
|
||||
day_disabled: 'text-muted-foreground opacity-50',
|
||||
day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
||||
day_hidden: 'invisible',
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
||||
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Calendar.displayName = 'Calendar'
|
||||
|
||||
export { Calendar }
|
||||
@@ -1,79 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -1,365 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
}
|
||||
>(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
})
|
||||
ChartContainer.displayName = "Chart"
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([_, config]) => config.theme || config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
@@ -1,155 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Drawer.displayName = "Drawer"
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
))
|
||||
DrawerContent.displayName = "DrawerContent"
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerHeader.displayName = "DrawerHeader"
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerFooter.displayName = "DrawerFooter"
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent gap-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 gap-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return <span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />
|
||||
}
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
@@ -1,25 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -1,26 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
@@ -1,160 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
@@ -1,124 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||
{children}
|
||||
<SheetPrimitive.Close
|
||||
autoFocus={true} // REMINDER: added
|
||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||
)
|
||||
SheetHeader.displayName = 'SheetHeader'
|
||||
|
||||
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = 'SheetFooter'
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -1,28 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -1,91 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||
</div>
|
||||
)
|
||||
)
|
||||
Table.displayName = 'Table'
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = 'TableHeader'
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||
))
|
||||
TableBody.displayName = 'TableBody'
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = 'TableFooter'
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
TableRow.displayName = 'TableRow'
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = 'TableHead'
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = 'TableCell'
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
|
||||
))
|
||||
TableCaption.displayName = 'TableCaption'
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||
@@ -1,55 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -1,6 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as VisuallyHiddenPrimitive from "@radix-ui/react-visually-hidden"
|
||||
|
||||
const VisuallyHidden = VisuallyHiddenPrimitive.Root
|
||||
|
||||
export { VisuallyHidden }
|
||||
@@ -1,41 +0,0 @@
|
||||
import { DatePreset } from "@/components/data-table/types";
|
||||
import { addDays, addHours, endOfDay, startOfDay } from "date-fns";
|
||||
|
||||
export const presets = [
|
||||
{
|
||||
label: "Today",
|
||||
from: startOfDay(new Date()),
|
||||
to: endOfDay(new Date()),
|
||||
shortcut: "d", // day
|
||||
},
|
||||
{
|
||||
label: "Yesterday",
|
||||
from: startOfDay(addDays(new Date(), -1)),
|
||||
to: endOfDay(addDays(new Date(), -1)),
|
||||
shortcut: "y",
|
||||
},
|
||||
{
|
||||
label: "Last hour",
|
||||
from: addHours(new Date(), -1),
|
||||
to: new Date(),
|
||||
shortcut: "h",
|
||||
},
|
||||
{
|
||||
label: "Last 7 days",
|
||||
from: startOfDay(addDays(new Date(), -7)),
|
||||
to: endOfDay(new Date()),
|
||||
shortcut: "w",
|
||||
},
|
||||
{
|
||||
label: "Last 14 days",
|
||||
from: startOfDay(addDays(new Date(), -14)),
|
||||
to: endOfDay(new Date()),
|
||||
shortcut: "b", // bi-weekly
|
||||
},
|
||||
{
|
||||
label: "Last 30 days",
|
||||
from: startOfDay(addDays(new Date(), -30)),
|
||||
to: endOfDay(new Date()),
|
||||
shortcut: "m",
|
||||
},
|
||||
] satisfies DatePreset[];
|
||||
@@ -1,4 +0,0 @@
|
||||
export const LEVELS = ["success", "warning", "error"] as const;
|
||||
|
||||
// FIXME: improve
|
||||
export const _LEVELS = [...LEVELS, "info"] as const;
|
||||
@@ -1 +0,0 @@
|
||||
export const METHODS = ["GET", "POST", "PUT", "DELETE"] as const;
|
||||
@@ -1,49 +0,0 @@
|
||||
export const REGIONS = ["ams", "fra", "gru", "hkg", "iad", "syd"] as const;
|
||||
|
||||
export const VERCEL_EDGE_REGIONS = [
|
||||
"hnd1",
|
||||
"sin1",
|
||||
"cpt1",
|
||||
"fra1",
|
||||
"hkg1",
|
||||
"syd1",
|
||||
"gru1",
|
||||
"dub1",
|
||||
"sfo1",
|
||||
"cdg1",
|
||||
"icn1",
|
||||
"kix1",
|
||||
"iad1",
|
||||
"arn1",
|
||||
"bom1",
|
||||
"lhr1",
|
||||
"cle1",
|
||||
] as const;
|
||||
|
||||
export const regions: Record<string, { label: string; flag: string }> = {
|
||||
// REGIONS
|
||||
ams: { label: "Amsterdam", flag: "🇳🇱" },
|
||||
fra: { label: "Frankfurt", flag: "🇩🇪" },
|
||||
gru: { label: "Sao Paulo", flag: "🇧🇷" },
|
||||
hkg: { label: "Hong Kong", flag: "🇭🇰" },
|
||||
iad: { label: "Washington D.C.", flag: "🇺🇸" },
|
||||
syd: { label: "Sydney", flag: "🇦🇺" },
|
||||
// VERCEL EDGE REGIONS
|
||||
hnd1: { label: "Tokyo", flag: "🇯🇵" },
|
||||
sin1: { label: "Singapore", flag: "🇸🇬" },
|
||||
cpt1: { label: "Cape Town", flag: "🇿🇦" },
|
||||
fra1: { label: "Paris", flag: "🇫🇷" },
|
||||
hkg1: { label: "Hong Kong", flag: "🇭🇰" },
|
||||
syd1: { label: "Sydney", flag: "🇦🇺" },
|
||||
gru1: { label: "Sao Paulo", flag: "🇧🇷" },
|
||||
dub1: { label: "Dublin", flag: "🇮🇪" },
|
||||
sfo1: { label: "San Francisco", flag: "🇺🇸" },
|
||||
cdg1: { label: "Paris", flag: "🇫🇷" },
|
||||
icn1: { label: "Seoul", flag: "🇰🇷" },
|
||||
kix1: { label: "Osaka", flag: "🇯🇵" },
|
||||
iad1: { label: "Washington D.C.", flag: "🇺🇸" },
|
||||
arn1: { label: "Stockholm", flag: "🇸🇪" },
|
||||
bom1: { label: "Mumbai", flag: "🇮🇳" },
|
||||
lhr1: { label: "London", flag: "🇬🇧" },
|
||||
cle1: { label: "Cleveland", flag: "🇺🇸" },
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
export const TAGS = ["web", "api", "enterprise", "app"] as const;
|
||||
|
||||
export const tagColor: Record<string, Record<"badge" | "dot", string>> = {
|
||||
api: {
|
||||
badge:
|
||||
"text-[#10b981] bg-[#10b981]/10 border-[#10b981]/20 hover:bg-[#10b981]/10",
|
||||
dot: "bg-[#10b981]",
|
||||
},
|
||||
web: {
|
||||
badge:
|
||||
"text-[#0ea5e9] bg-[#0ea5e9]/10 border-[#0ea5e9]/20 hover:bg-[#0ea5e9]/10",
|
||||
dot: "bg-[#0ea5e9]",
|
||||
},
|
||||
enterprise: {
|
||||
badge:
|
||||
"text-[#ec4899] bg-[#ec4899]/10 border-[#ec4899]/20 hover:bg-[#ec4899]/10",
|
||||
dot: "bg-[#ec4899]",
|
||||
},
|
||||
app: {
|
||||
badge:
|
||||
"text-[#f97316] bg-[#f97316]/10 border-[#f97316]/20 hover:bg-[#f97316]/10",
|
||||
dot: "bg-[#f97316]",
|
||||
},
|
||||
};
|
||||
@@ -1,158 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { DataTableColumnHeader } from '../components/data-table/data-table-column-header'
|
||||
import { Badge } from '../components/ui/badge'
|
||||
import { tagColor } from '../constants/tag'
|
||||
import { isArrayOfDates, isArrayOfNumbers } from '../lib/is-array'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { format, isSameDay } from 'date-fns'
|
||||
import { Check, Minus } from 'lucide-react'
|
||||
import type { ColumnSchema } from 'components/interfaces/DataTableDemo/default/types'
|
||||
|
||||
export const columns: ColumnDef<ColumnSchema>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'url',
|
||||
header: 'URL',
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('url')
|
||||
return <div className="max-w-[200px] truncate">{`${value}`}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'regions',
|
||||
header: 'Regions',
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('regions')
|
||||
if (Array.isArray(value)) {
|
||||
return <div className="text-muted-foreground">{value.join(', ')}</div>
|
||||
}
|
||||
return <div className="text-muted-foreground">{`${value}`}</div>
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
const array = row.getValue(id) as string[]
|
||||
if (typeof value === 'string') return array.includes(value)
|
||||
// up to the user to define either `.some` or `.every`
|
||||
if (Array.isArray(value)) return value.some((i) => array.includes(i))
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'tags',
|
||||
header: 'Tags',
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('tags') as string | string[]
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{value.map((v) => (
|
||||
<Badge key={v} className={tagColor[v].badge}>
|
||||
{v}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <Badge className={tagColor[value].badge}>{value}</Badge>
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
const array = row.getValue(id) as string[]
|
||||
if (typeof value === 'string') return array.includes(value)
|
||||
// up to the user to define either `.some` or `.every`
|
||||
if (Array.isArray(value)) return value.some((i) => array.includes(i))
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'p95',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="P95" />,
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('p95')
|
||||
if (typeof value === 'undefined') {
|
||||
return <Minus className="h-4 w-4 text-muted-foreground/50" />
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<span className="font-mono">{`${value}`}</span> ms
|
||||
</div>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
const rowValue = row.getValue(id) as number
|
||||
if (typeof value === 'number') return value === Number(rowValue)
|
||||
if (Array.isArray(value) && isArrayOfNumbers(value)) {
|
||||
if (value.length === 1) {
|
||||
return value[0] === rowValue
|
||||
} else {
|
||||
const sorted = value.sort((a, b) => a - b)
|
||||
return sorted[0] <= rowValue && rowValue <= sorted[1]
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'active',
|
||||
header: 'Active',
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('active')
|
||||
if (value) return <Check className="h-4 w-4" />
|
||||
return <Minus className="h-4 w-4 text-muted-foreground/50" />
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
const rowValue = row.getValue(id)
|
||||
if (typeof value === 'string') return value === String(rowValue)
|
||||
if (typeof value === 'boolean') return value === rowValue
|
||||
if (Array.isArray(value)) return value.includes(rowValue)
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'public',
|
||||
header: 'Public',
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('public')
|
||||
if (value) return <Check className="h-4 w-4" />
|
||||
return <Minus className="h-4 w-4 text-muted-foreground/50" />
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
const rowValue = row.getValue(id)
|
||||
if (typeof value === 'string') return value === String(rowValue)
|
||||
if (typeof value === 'boolean') return value === rowValue
|
||||
if (Array.isArray(value)) return value.includes(rowValue)
|
||||
return false
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Date" />,
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue('date')
|
||||
return (
|
||||
<div className="text-xs text-muted-foreground" suppressHydrationWarning>
|
||||
{format(new Date(`${value}`), 'LLL dd, y HH:mm')}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
const rowValue = row.getValue(id)
|
||||
if (value instanceof Date && rowValue instanceof Date) {
|
||||
return isSameDay(value, rowValue)
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (isArrayOfDates(value) && rowValue instanceof Date) {
|
||||
const sorted = value.sort((a, b) => a.getTime() - b.getTime())
|
||||
// TODO: check length
|
||||
return (
|
||||
sorted[0]?.getTime() <= rowValue.getTime() && rowValue.getTime() <= sorted[1]?.getTime()
|
||||
)
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,69 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { DataTableFilterField, Option } from '../components/data-table/types'
|
||||
import { REGIONS } from '../constants/region'
|
||||
import { tagColor, TAGS } from '../constants/tag'
|
||||
import { cn } from '../lib/utils'
|
||||
import { data } from './data'
|
||||
import type { ColumnSchema } from './types'
|
||||
|
||||
export const filterFields = [
|
||||
{
|
||||
label: 'Time Range',
|
||||
value: 'date',
|
||||
type: 'timerange',
|
||||
defaultOpen: true,
|
||||
commandDisabled: true,
|
||||
},
|
||||
{
|
||||
label: 'URL',
|
||||
value: 'url',
|
||||
type: 'input',
|
||||
options: data.map(({ url }) => ({ label: url, value: url })),
|
||||
},
|
||||
{
|
||||
label: 'Public',
|
||||
value: 'public',
|
||||
type: 'checkbox',
|
||||
options: [true, false].map((bool) => ({ label: `${bool}`, value: bool })),
|
||||
},
|
||||
{
|
||||
label: 'Active',
|
||||
value: 'active',
|
||||
type: 'checkbox',
|
||||
options: [true, false].map((bool) => ({ label: `${bool}`, value: bool })),
|
||||
},
|
||||
{
|
||||
label: 'P95',
|
||||
value: 'p95',
|
||||
type: 'slider',
|
||||
min: 0,
|
||||
max: 3000,
|
||||
options: data.map(({ p95 }) => ({ label: `${p95}`, value: p95 })),
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
label: 'Regions',
|
||||
value: 'regions',
|
||||
type: 'checkbox',
|
||||
options: REGIONS.map((region) => ({ label: region, value: region })),
|
||||
},
|
||||
{
|
||||
label: 'Tags',
|
||||
value: 'tags',
|
||||
type: 'checkbox',
|
||||
defaultOpen: true,
|
||||
// REMINDER: "use client" needs to be declared in the file - otherwise getting serialization error from Server Component
|
||||
component: (props: Option) => {
|
||||
if (typeof props.value === 'boolean') return null
|
||||
if (typeof props.value === 'undefined') return null
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span className="truncate font-normal">{props.value}</span>
|
||||
<span className={cn('h-2 w-2 rounded-full', tagColor[props.value].dot)} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
options: TAGS.map((tag) => ({ label: tag, value: tag })),
|
||||
},
|
||||
] satisfies DataTableFilterField<ColumnSchema>[]
|
||||
@@ -1,184 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '../components/custom/table'
|
||||
import { DataTableFilterCommand } from '../components/data-table/data-table-filter-command'
|
||||
import { DataTableFilterControls } from '../components/data-table/data-table-filter-controls'
|
||||
import { DataTablePagination } from '../components/data-table/data-table-pagination'
|
||||
import { DataTableProvider } from '../components/data-table/data-table-provider'
|
||||
import { DataTableToolbar } from '../components/data-table/data-table-toolbar'
|
||||
import type { DataTableFilterField } from '../components/data-table/types'
|
||||
import { useLocalStorage } from '../hooks/use-local-storage'
|
||||
import { cn } from '../lib/utils'
|
||||
import type {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
Table as TTable,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table'
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedMinMaxValues,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table'
|
||||
import { useQueryStates } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { searchParamsParser } from 'components/interfaces/DataTableDemo/default/search-params'
|
||||
|
||||
export interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
defaultColumnFilters?: ColumnFiltersState
|
||||
// TODO: add sortingColumnFilters
|
||||
filterFields?: DataTableFilterField<TData>[]
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
defaultColumnFilters = [],
|
||||
filterFields = [],
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(defaultColumnFilters)
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
})
|
||||
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>(
|
||||
'data-table-visibility',
|
||||
{}
|
||||
)
|
||||
const [_, setSearch] = useQueryStates(searchParamsParser)
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: { columnFilters, sorting, columnVisibility, pagination },
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFacetedMinMaxValues: getFacetedMinMaxValues(),
|
||||
// REMINDER: it doesn't support array of strings (WARNING: might not work for other types)
|
||||
getFacetedUniqueValues: (table: TTable<TData>, columnId: string) => () => {
|
||||
const facets = getFacetedUniqueValues<TData>()(table, columnId)()
|
||||
const customFacets = new Map()
|
||||
for (const [key, value] of facets as any) {
|
||||
if (Array.isArray(key)) {
|
||||
for (const k of key) {
|
||||
const prevValue = customFacets.get(k) || 0
|
||||
customFacets.set(k, prevValue + value)
|
||||
}
|
||||
} else {
|
||||
const prevValue = customFacets.get(key) || 0
|
||||
customFacets.set(key, prevValue + value)
|
||||
}
|
||||
}
|
||||
return customFacets
|
||||
},
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
const columnFiltersWithNullable = filterFields.map((field) => {
|
||||
const filterValue = columnFilters.find((filter) => filter.id === field.value)
|
||||
if (!filterValue) return { id: field.value, value: null }
|
||||
return { id: field.value, value: filterValue.value }
|
||||
})
|
||||
|
||||
const search = columnFiltersWithNullable.reduce(
|
||||
(prev, curr) => {
|
||||
prev[curr.id as string] = curr.value
|
||||
return prev
|
||||
},
|
||||
{} as Record<string, unknown>
|
||||
)
|
||||
|
||||
setSearch(search)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [columnFilters])
|
||||
|
||||
return (
|
||||
<DataTableProvider
|
||||
table={table}
|
||||
columns={columns}
|
||||
filterFields={filterFields}
|
||||
columnFilters={columnFilters}
|
||||
sorting={sorting}
|
||||
pagination={pagination}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-3 sm:flex-row">
|
||||
<div
|
||||
className={cn(
|
||||
'hidden w-full p-1 sm:block sm:min-w-52 sm:max-w-52 sm:self-start md:min-w-64 md:max-w-64',
|
||||
'group-data-[expanded=false]/controls:hidden'
|
||||
)}
|
||||
>
|
||||
<DataTableFilterControls />
|
||||
</div>
|
||||
<div className="flex max-w-full flex-1 flex-col gap-4 overflow-hidden p-1">
|
||||
<DataTableFilterCommand searchParamsParser={searchParamsParser} />
|
||||
<DataTableToolbar />
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/50">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="hover:bg-transparent">
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DataTablePagination />
|
||||
</div>
|
||||
</div>
|
||||
</DataTableProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
import { subDays, subHours, subMinutes } from "date-fns";
|
||||
import type { ColumnSchema } from "./types";
|
||||
|
||||
export const data = [
|
||||
{
|
||||
name: "Edge Api",
|
||||
url: "edge-api.acme.com/health",
|
||||
p95: 140,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["ams", "gru", "syd"],
|
||||
tags: ["api", "enterprise"],
|
||||
date: subHours(new Date(), 1),
|
||||
},
|
||||
{
|
||||
name: "Lambda Api",
|
||||
url: "lambda-api.acme.com/health",
|
||||
p95: 203,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["ams", "gru", "syd"],
|
||||
tags: ["api"],
|
||||
date: subHours(new Date(), 10),
|
||||
},
|
||||
{
|
||||
name: "Storybook",
|
||||
url: "storybook.acme.com",
|
||||
p95: 1252,
|
||||
public: false,
|
||||
active: true,
|
||||
regions: ["iad"],
|
||||
tags: ["web"],
|
||||
date: subMinutes(new Date(), 10),
|
||||
},
|
||||
{
|
||||
name: "Marketing",
|
||||
url: "acme.com",
|
||||
p95: 659,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["hkg", "fra", "iad"],
|
||||
tags: ["web"],
|
||||
date: subDays(new Date(), 1),
|
||||
},
|
||||
{
|
||||
name: "App",
|
||||
url: "app.acme.com",
|
||||
p95: 1301,
|
||||
public: false,
|
||||
active: true,
|
||||
regions: ["iad", "fra"],
|
||||
tags: ["app"],
|
||||
date: subHours(new Date(), 13),
|
||||
},
|
||||
{
|
||||
name: "Demo",
|
||||
url: "demo.acme.com",
|
||||
p95: 2420,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["iad"],
|
||||
tags: ["web", "enterprise"],
|
||||
date: subDays(new Date(), 4),
|
||||
},
|
||||
{
|
||||
name: "Documentation",
|
||||
url: "docs.acme.com",
|
||||
p95: 943,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["ams"],
|
||||
tags: ["api", "web"],
|
||||
date: subDays(new Date(), 6),
|
||||
},
|
||||
{
|
||||
name: "Boilerplate",
|
||||
url: "boilerplate.acme.com",
|
||||
p95: undefined,
|
||||
public: true,
|
||||
active: false,
|
||||
regions: ["gru", "fra"],
|
||||
tags: ["web"],
|
||||
date: subDays(new Date(), 10),
|
||||
},
|
||||
{
|
||||
name: "Dashboard",
|
||||
url: "app.acme.com/dashboard",
|
||||
p95: 967,
|
||||
public: false,
|
||||
active: true,
|
||||
regions: ["iad", "fra"],
|
||||
tags: ["web"],
|
||||
date: subHours(new Date(), 28),
|
||||
},
|
||||
{
|
||||
name: "E2E Testing",
|
||||
url: "staging-cypress-e2e.acme.com",
|
||||
p95: 1954,
|
||||
public: false,
|
||||
active: true,
|
||||
regions: ["iad"],
|
||||
tags: ["web"],
|
||||
date: subDays(new Date(), 12),
|
||||
},
|
||||
{
|
||||
name: "Web App",
|
||||
url: "web-app.acme.com",
|
||||
p95: 1043,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["iad"],
|
||||
tags: ["web"],
|
||||
date: subDays(new Date(), 15),
|
||||
},
|
||||
{
|
||||
name: "Admin Panel",
|
||||
url: "admin.acme.com",
|
||||
p95: 1342,
|
||||
public: false,
|
||||
active: true,
|
||||
regions: ["gru", "syd"],
|
||||
tags: ["web"],
|
||||
date: subHours(new Date(), 5),
|
||||
},
|
||||
{
|
||||
name: "API Gateway",
|
||||
url: "api-gateway.acme.com/health",
|
||||
p95: 190,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["ams", "hkg"],
|
||||
tags: ["api", "enterprise"],
|
||||
date: subHours(new Date(), 3),
|
||||
},
|
||||
{
|
||||
name: "Analytics Service",
|
||||
url: "analytics.acme.com",
|
||||
p95: 810,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["iad", "fra", "hkg"],
|
||||
tags: ["app", "enterprise"],
|
||||
date: subDays(new Date(), 2),
|
||||
},
|
||||
{
|
||||
name: "Support Portal",
|
||||
url: "support.acme.com",
|
||||
p95: 752,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["gru", "iad"],
|
||||
tags: ["web"],
|
||||
date: subMinutes(new Date(), 30),
|
||||
},
|
||||
{
|
||||
name: "User Management",
|
||||
url: "user-mgmt.acme.com",
|
||||
p95: 980,
|
||||
public: false,
|
||||
active: true,
|
||||
regions: ["gru", "syd", "fra"],
|
||||
tags: ["app"],
|
||||
date: subDays(new Date(), 7),
|
||||
},
|
||||
{
|
||||
name: "Payment Gateway",
|
||||
url: "payments.acme.com",
|
||||
p95: 156,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["ams", "hkg", "syd"],
|
||||
tags: ["api", "enterprise"],
|
||||
date: subHours(new Date(), 8),
|
||||
},
|
||||
{
|
||||
name: "Notification Service",
|
||||
url: "notify.acme.com",
|
||||
p95: 345,
|
||||
public: false,
|
||||
active: true,
|
||||
regions: ["iad"],
|
||||
tags: ["api"],
|
||||
date: subDays(new Date(), 11),
|
||||
},
|
||||
{
|
||||
name: "File Storage",
|
||||
url: "storage.acme.com",
|
||||
p95: 1220,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["gru", "hkg"],
|
||||
tags: ["web", "enterprise"],
|
||||
date: subDays(new Date(), 3),
|
||||
},
|
||||
{
|
||||
name: "CDN",
|
||||
url: "cdn.acme.com",
|
||||
p95: 89,
|
||||
public: true,
|
||||
active: true,
|
||||
regions: ["ams", "iad", "hkg"],
|
||||
tags: ["web"],
|
||||
date: subDays(new Date(), 2),
|
||||
},
|
||||
{
|
||||
name: "Auth Service",
|
||||
url: "auth.acme.com",
|
||||
p95: 542,
|
||||
public: false,
|
||||
active: true,
|
||||
regions: ["gru", "syd"],
|
||||
tags: ["api"],
|
||||
date: subHours(new Date(), 16),
|
||||
},
|
||||
] satisfies ColumnSchema[];
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { SocialsFooter } from '../components/layout/socials-footer'
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="container mx-auto flex min-h-screen flex-col gap-4 p-4 sm:p-8">
|
||||
<div className="flex flex-col gap-4 sm:gap-8 w-full max-w-7xl mx-auto relative min-h-full h-full rounded-lg border border-border/50 bg-background/50 p-4 backdrop-blur-[2px] sm:p-8">
|
||||
{children}
|
||||
</div>
|
||||
<SocialsFooter />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { columns } from './columns'
|
||||
import { filterFields } from './constants'
|
||||
import { data } from './data'
|
||||
import { DataTable } from './data-table'
|
||||
import { useSearchParams } from './search-params'
|
||||
import { Skeleton } from './skeleton'
|
||||
|
||||
export default function TableDemo() {
|
||||
const [search] = useSearchParams()
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={<Skeleton />}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
filterFields={filterFields}
|
||||
defaultColumnFilters={Object.entries(search)
|
||||
.map(([key, value]) => ({
|
||||
id: key,
|
||||
value,
|
||||
}))
|
||||
.filter(({ value }) => value ?? undefined)}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { REGIONS } from '../constants/region'
|
||||
import { TAGS } from '../constants/tag'
|
||||
import { ARRAY_DELIMITER, RANGE_DELIMITER, SLIDER_DELIMITER } from '../lib/delimiters'
|
||||
import {
|
||||
createParser,
|
||||
parseAsArrayOf,
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
parseAsString,
|
||||
parseAsStringLiteral,
|
||||
parseAsTimestamp,
|
||||
useQueryState,
|
||||
useQueryStates,
|
||||
} from 'nuqs'
|
||||
|
||||
export const parseAsSort = createParser({
|
||||
parse(queryValue) {
|
||||
const [id, desc] = queryValue.split('.')
|
||||
if (!id && !desc) return null
|
||||
return { id, desc: desc === 'desc' }
|
||||
},
|
||||
serialize(value) {
|
||||
return `${value.id}.${value.desc ? 'desc' : 'asc'}`
|
||||
},
|
||||
})
|
||||
|
||||
export const searchParamsParser = {
|
||||
// FILTERS
|
||||
url: parseAsString,
|
||||
// TODO: limit to two array entries
|
||||
p95: parseAsArrayOf(parseAsInteger, SLIDER_DELIMITER),
|
||||
public: parseAsArrayOf(parseAsBoolean, ARRAY_DELIMITER),
|
||||
active: parseAsArrayOf(parseAsBoolean, ARRAY_DELIMITER),
|
||||
regions: parseAsArrayOf(parseAsStringLiteral(REGIONS), ARRAY_DELIMITER),
|
||||
tags: parseAsArrayOf(parseAsStringLiteral(TAGS), ARRAY_DELIMITER),
|
||||
date: parseAsArrayOf(parseAsTimestamp, RANGE_DELIMITER),
|
||||
} as const
|
||||
|
||||
export function useSearchParams() {
|
||||
return useQueryStates(searchParamsParser)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { DataTableSkeleton } from '../components/data-table/data-table-skeleton'
|
||||
import { Skeleton as DefaultSkeleton } from 'ui'
|
||||
|
||||
export function Skeleton() {
|
||||
return (
|
||||
<div className="flex w-full h-full flex-col gap-3 sm:flex-row">
|
||||
<div className="w-full p-1 sm:min-w-52 sm:max-w-52 sm:self-start md:min-w-64 md:max-w-64">
|
||||
<div className="-m-1 h-full p-1">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="h-11 w-full flex items-center">
|
||||
<DefaultSkeleton className="h-6 w-12" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
<DefaultSkeleton className="h-7 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex max-w-full flex-1 flex-col gap-4 p-1">
|
||||
<DefaultSkeleton className="h-11 w-full border border-border" />
|
||||
<div className="flex justify-between items-center h-9">
|
||||
<DefaultSkeleton className="h-full max-w-36 w-full" />
|
||||
<DefaultSkeleton className="h-full max-w-20 w-full" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<DataTableSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { REGIONS } from '../constants/region'
|
||||
import { TAGS } from '../constants/tag'
|
||||
|
||||
export type ColumnSchema = {
|
||||
name: string
|
||||
url: string
|
||||
public: boolean
|
||||
active: boolean
|
||||
regions: (typeof REGIONS)[number][]
|
||||
tags: (typeof TAGS)[number][]
|
||||
date: Date
|
||||
p95?: number | undefined
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function useCopyToClipboard() {
|
||||
const [text, setText] = useState<string | null>(null);
|
||||
|
||||
const copy = useCallback(
|
||||
async (
|
||||
text: string,
|
||||
{ timeout, withToast }: { timeout?: number; withToast?: boolean } = {
|
||||
timeout: 3000,
|
||||
withToast: false,
|
||||
}
|
||||
) => {
|
||||
if (!navigator?.clipboard) {
|
||||
console.warn("Clipboard not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setText(text);
|
||||
|
||||
if (timeout) {
|
||||
setTimeout(() => {
|
||||
setText(null);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
if (withToast) {
|
||||
toast.success("Copied to clipboard");
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("Copy failed", error);
|
||||
setText(null);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return { text, copy, isCopied: text !== null };
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
// consider using https://github.com/xnimorz/use-debounce
|
||||
export function useDebounce<T>(value: T, delay?: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function useHotKey(callback: () => void, key: string): void {
|
||||
useEffect(() => {
|
||||
function handler(e: KeyboardEvent) {
|
||||
if (e.key === key && (e.metaKey || e.ctrlKey)) {
|
||||
// e.preventDefault();
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handler);
|
||||
};
|
||||
}, [key]);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
|
||||
function getItemFromLocalStorage(key: string) {
|
||||
const item = window?.localStorage.getItem(key);
|
||||
if (item) return JSON.parse(item);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, React.Dispatch<React.SetStateAction<T>>] {
|
||||
const [storedValue, setStoredValue] = useState(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
// initialize
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = getItemFromLocalStorage(key);
|
||||
if (stored !== null) setStoredValue(stored);
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
const setValue: React.Dispatch<React.SetStateAction<T>> = useCallback(
|
||||
(value) => {
|
||||
if (value instanceof Function) {
|
||||
setStoredValue((prev: T) => {
|
||||
const newValue = value(prev);
|
||||
// Save to localStorage
|
||||
window.localStorage.setItem(key, JSON.stringify(newValue));
|
||||
return newValue;
|
||||
});
|
||||
} else {
|
||||
setStoredValue(value);
|
||||
// Save to localStorage
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
return setStoredValue;
|
||||
},
|
||||
[key, setStoredValue]
|
||||
);
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
export function useMediaQuery(query: string) {
|
||||
const [value, setValue] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
function onChange(event: MediaQueryListEvent) {
|
||||
setValue(event.matches)
|
||||
}
|
||||
|
||||
const result = matchMedia(query)
|
||||
result.addEventListener("change", onChange)
|
||||
setValue(result.matches)
|
||||
|
||||
return () => result.removeEventListener("change", onChange)
|
||||
}, [query])
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
### Non-standard components
|
||||
|
||||
All components within the `_components` folder are non-standard and are specific for the `/infinite` example.
|
||||
|
||||
The goal is to create a collection of components that can be used in any project. The components will be mostly used for very specific use cases either in the `filterFields[number].component` (see `constants.tsx`), `columns[0].cell` (see `columns.tsx`) or in the `DataTableSheetDetails` (`popover-percentile.tsx`).
|
||||
|
||||
The components are not part of the `@/components` folder because they are not generic enough to be used in other projects.
|
||||
|
||||
For more generic components, we use the `@/components/custom` and `@/components/data-table/{data-table-column}` folders.
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { format } from 'date-fns'
|
||||
|
||||
import { cn } from 'ui'
|
||||
import { TimestampInfo } from 'ui-patterns'
|
||||
|
||||
interface HoverCardTimestampProps {
|
||||
date: Date
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function HoverCardTimestamp({ date, className }: HoverCardTimestampProps) {
|
||||
return (
|
||||
<div className="relative w-fit">
|
||||
<TimestampInfo utcTimestamp={date.getTime()}>
|
||||
<div className={cn('font-mono flex gap-1.5', className)}>
|
||||
<span className="text-foreground/50 uppercase">{format(date, 'MMM')}</span>
|
||||
<span className="text-foreground/50 uppercase"> {format(date, 'dd')} </span>
|
||||
<span>
|
||||
{format(date, 'HH:mm:ss')}
|
||||
<span className="text-foreground/50">.{format(date, 'SS')}</span>
|
||||
</span>
|
||||
</div>
|
||||
</TimestampInfo>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@/components/custom/table";
|
||||
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
|
||||
import { Check } from "lucide-react";
|
||||
import { Copy } from "lucide-react";
|
||||
|
||||
interface KeyValueTableProps {
|
||||
data: Record<string, string>;
|
||||
}
|
||||
export function KeyValueTable({ data }: KeyValueTableProps) {
|
||||
return (
|
||||
<div className="mx-auto max-w-lg">
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-background">
|
||||
<Table>
|
||||
<TableBody>
|
||||
{Object.entries(data).map(([key, value]) => {
|
||||
return <RowAction key={key} label={key} value={value} />;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RowAction({ label, value }: { label: string; value: string }) {
|
||||
const { copy, isCopied } = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className="group *:border-border hover:bg-transparent [&>:not(:last-child)]:border-r text-left"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(value);
|
||||
}}
|
||||
>
|
||||
<TableCell className="bg-muted/50 py-1 font-medium font-mono">
|
||||
{label}
|
||||
</TableCell>
|
||||
<TableCell className="relative py-1 font-mono">
|
||||
{value}
|
||||
<div className="absolute top-1.5 right-1.5 invisible group-hover:visible backdrop-blur-sm bg-background p-0.5 rounded-sm border border-border">
|
||||
{!isCopied ? (
|
||||
<Copy className="h-3 w-3" />
|
||||
) : (
|
||||
<Check className="h-3 w-3" />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
import { Button } from 'ui'
|
||||
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
|
||||
import { cn } from 'ui'
|
||||
import type { FetchPreviousPageOptions } from '@tanstack/react-query'
|
||||
import { CirclePause, CirclePlay } from 'lucide-react'
|
||||
import { useQueryStates } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { searchParamsParser } from '../search-params'
|
||||
|
||||
const REFRESH_INTERVAL = 10_000
|
||||
|
||||
interface LiveButtonProps {
|
||||
fetchPreviousPage?: (options?: FetchPreviousPageOptions | undefined) => Promise<unknown>
|
||||
}
|
||||
|
||||
export function LiveButton({ fetchPreviousPage }: LiveButtonProps) {
|
||||
const [{ live, date, sort }, setSearch] = useQueryStates(searchParamsParser)
|
||||
const { table } = useDataTable()
|
||||
useHotKey(handleClick, 'j')
|
||||
|
||||
React.useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
|
||||
async function fetchData() {
|
||||
if (live) {
|
||||
await fetchPreviousPage?.()
|
||||
timeoutId = setTimeout(fetchData, REFRESH_INTERVAL)
|
||||
} else {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}, [live, fetchPreviousPage])
|
||||
|
||||
// REMINDER: make sure to reset live when date is set
|
||||
// TODO: test properly
|
||||
React.useEffect(() => {
|
||||
if ((date || sort) && live) {
|
||||
setSearch((prev) => ({ ...prev, live: null }))
|
||||
}
|
||||
}, [date, sort])
|
||||
|
||||
function handleClick() {
|
||||
setSearch((prev) => ({
|
||||
...prev,
|
||||
live: !prev.live,
|
||||
date: null,
|
||||
sort: null,
|
||||
}))
|
||||
table.getColumn('date')?.setFilterValue(undefined)
|
||||
table.resetSorting()
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn(live && 'border-info text-info hover:text-info')}
|
||||
onClick={handleClick}
|
||||
type={live ? 'primary' : 'default'}
|
||||
size="small"
|
||||
icon={
|
||||
live ? <CirclePause className="mr-2 h-4 w-4" /> : <CirclePlay className="mr-2 h-4 w-4" />
|
||||
}
|
||||
>
|
||||
Live
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { TableCell, TableRow } from 'ui'
|
||||
import { DataTableColumnLevelIndicator } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-level-indicator'
|
||||
import { columns } from '../columns'
|
||||
|
||||
export function LiveRow() {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className="w-[--header-level-size] min-w-[--header-level-size] max-w-[--header-level-size] border-b border-l border-r border-t border-info border-r-info/50">
|
||||
<DataTableColumnLevelIndicator value="info" />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
colSpan={columns.length - 1}
|
||||
className="border-b border-r border-t border-info font-medium text-info"
|
||||
>
|
||||
Live Mode
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
'use client'
|
||||
import { ColumnSchema } from '../schema'
|
||||
import { FunctionSquare } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
import { cn } from 'ui'
|
||||
import {
|
||||
formatCompactNumber,
|
||||
formatMilliseconds,
|
||||
} from 'components/interfaces/DataTableDemo/lib/format'
|
||||
import {
|
||||
Percentile,
|
||||
getPercentileColor,
|
||||
} from 'components/interfaces/DataTableDemo/lib/request/percentile'
|
||||
import {
|
||||
Popover_Shadcn_ as Popover,
|
||||
PopoverContent_Shadcn_ as PopoverContent,
|
||||
PopoverTrigger_Shadcn_ as PopoverTrigger,
|
||||
} from 'ui'
|
||||
|
||||
interface PopoverPercentileProps {
|
||||
data?: ColumnSchema
|
||||
percentiles?: Record<Percentile, number>
|
||||
filterRows: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PopoverPercentile({
|
||||
data,
|
||||
percentiles,
|
||||
filterRows,
|
||||
className,
|
||||
}: PopoverPercentileProps) {
|
||||
let percentileArray = percentiles
|
||||
? Object.entries(percentiles).map(([percentile, latency]) => [parseInt(percentile), latency])
|
||||
: []
|
||||
|
||||
data?.percentile ? percentileArray.push([data.percentile, data.latency]) : null
|
||||
percentileArray.sort((a, b) => a[0] - b[0])
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
'font-mono flex items-center gap-1 rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FunctionSquare
|
||||
className={cn(
|
||||
'h-4 w-4',
|
||||
data?.percentile ? getPercentileColor(data.percentile).text : 'text-muted-foreground'
|
||||
)}
|
||||
/>
|
||||
{!data?.percentile ? 'N/A' : `P${Math.round(data.percentile)}`}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-40 flex flex-col gap-2 p-2 text-xs" align="end">
|
||||
<p>
|
||||
Calculated from filtered result of{' '}
|
||||
<span className="font-medium font-mono">{formatCompactNumber(filterRows)}</span> rows.
|
||||
</p>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{percentileArray.map(([key, value]) => {
|
||||
const active = data?.percentile && data.percentile === key && value === data.latency
|
||||
return (
|
||||
<div
|
||||
key={`${key}-${value}`}
|
||||
className={cn(
|
||||
'flex items-center justify-between px-1 py-0.5 rounded-md',
|
||||
active && data.percentile
|
||||
? `border ${getPercentileColor(data.percentile).border}`
|
||||
: null
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn('font-mono', !active && 'text-muted-foreground')}
|
||||
>{`P${Math.round(key)}`}</div>
|
||||
<div className="font-mono">
|
||||
{formatMilliseconds(Math.round(value))}
|
||||
<span className="text-muted-foreground">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import { Button } from 'ui'
|
||||
import { useDataTable } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
|
||||
import { LoaderCircle, RefreshCcw } from 'lucide-react'
|
||||
|
||||
interface RefreshButtonProps {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function RefreshButton({ onClick }: RefreshButtonProps) {
|
||||
const { isLoading } = useDataTable()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="outline"
|
||||
size="small"
|
||||
disabled={isLoading}
|
||||
onClick={onClick}
|
||||
className="h-9 w-9 px-0"
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { TimingPhase, timingPhases } from 'components/interfaces/DataTableDemo/lib/request/timing'
|
||||
import {
|
||||
getTimingColor,
|
||||
getTimingLabel,
|
||||
getTimingPercentage,
|
||||
} from 'components/interfaces/DataTableDemo/lib/request/timing'
|
||||
import { cn } from 'ui'
|
||||
import { formatMilliseconds } from 'components/interfaces/DataTableDemo/lib/format'
|
||||
|
||||
export function SheetTimingPhases({
|
||||
latency,
|
||||
timing,
|
||||
className,
|
||||
}: {
|
||||
latency: number
|
||||
timing: Record<TimingPhase, number>
|
||||
className?: string
|
||||
}) {
|
||||
const timingPercentage = getTimingPercentage(timing, latency)
|
||||
return (
|
||||
<div className={cn('space-y-1 w-full text-left', className)}>
|
||||
{timingPhases.map((phase) => (
|
||||
<div key={phase} className="grid grid-cols-3 gap-2 text-xs justify-between items-center">
|
||||
<div className="text-foreground uppercase truncate font-mono">
|
||||
{getTimingLabel(phase)}
|
||||
</div>
|
||||
<div className="flex gap-2 col-span-2">
|
||||
<div className="font-mono text-muted-foreground mr-8">{timingPercentage[phase]}</div>
|
||||
<div className="flex flex-1 gap-2 items-center justify-end">
|
||||
<div className="font-mono">
|
||||
{formatMilliseconds(timing[phase])}
|
||||
<span className="text-muted-foreground">ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(getTimingColor(phase), 'h-4')}
|
||||
style={{ width: `${(timing[phase] / latency) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
import { Kbd } from 'components/interfaces/DataTableDemo/components/custom/kbd'
|
||||
import { Link } from 'components/interfaces/DataTableDemo/components/custom/link'
|
||||
import { Bluesky } from 'components/interfaces/DataTableDemo/components/icons/bluesky'
|
||||
import { Github } from 'components/interfaces/DataTableDemo/components/icons/github'
|
||||
import { X } from 'components/interfaces/DataTableDemo/components/icons/x'
|
||||
import { ModeToggle } from 'components/interfaces/DataTableDemo/components/theme/toggle-mode'
|
||||
import { Button } from 'ui'
|
||||
import {
|
||||
Popover_Shadcn_ as Popover,
|
||||
PopoverContent_Shadcn_ as PopoverContent,
|
||||
PopoverTrigger_Shadcn_ as PopoverTrigger,
|
||||
} from 'ui'
|
||||
import { Book, Command } from 'lucide-react'
|
||||
import NextLink from 'next/link'
|
||||
|
||||
export function SocialsFooter() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="grid w-full grid-cols-3 items-center justify-center gap-2 p-1 md:grid-cols-6">
|
||||
<Button type="text" size="small" className="h-8 w-8 px-0" asChild>
|
||||
<NextLink href="https://github.com/openstatusHQ/data-table-filters">
|
||||
<Github className="h-4 w-4" />
|
||||
</NextLink>
|
||||
</Button>
|
||||
<Button type="text" size="small" className="h-8 w-8 px-0" asChild>
|
||||
<NextLink href="https://twitter.com/openstatusHQ">
|
||||
<X className="h-4 w-4" />
|
||||
</NextLink>
|
||||
</Button>
|
||||
<Button type="text" size="small" className="h-8 w-8 px-0" asChild>
|
||||
<NextLink href="https://bsky.app/profile/openstatus.dev">
|
||||
<Bluesky className="h-4 w-4" />
|
||||
</NextLink>
|
||||
</Button>
|
||||
<ModeToggle className="h-8 w-8 [&>svg]:h-4 [&>svg]:w-4" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button type="text" size="small" className="h-8 w-8 px-0">
|
||||
<Command className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto px-2 py-1">
|
||||
<HotkeyOverview />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button type="text" size="small" className="h-8 w-8 px-0" asChild>
|
||||
<NextLink href="/guide">
|
||||
<Book className="h-4 w-4" />
|
||||
</NextLink>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
Powered by{' '}
|
||||
<Link href="https://openstatus.dev" hideArrow>
|
||||
OpenStatus
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-center text-[10px] text-muted-foreground">
|
||||
The project is in active development. For feedback, please{' '}
|
||||
<Link
|
||||
href="https://github.com/openstatusHQ/data-table-filters/issues/new"
|
||||
className="text-muted-foreground"
|
||||
hideArrow
|
||||
>
|
||||
open an issue
|
||||
</Link>{' '}
|
||||
on GitHub.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const hotkeys = [
|
||||
{ key: 'K', description: 'Toggle command input' },
|
||||
{ key: 'B', description: 'Toggle sidebar controls' },
|
||||
{
|
||||
key: 'U',
|
||||
description: 'Undo column state (order, visibility)',
|
||||
},
|
||||
{
|
||||
key: 'J',
|
||||
description: 'Toggle live mode',
|
||||
},
|
||||
{ key: 'Esc', description: 'Reset table filters' },
|
||||
{
|
||||
key: '.',
|
||||
description: 'Reset element focus to start',
|
||||
},
|
||||
]
|
||||
|
||||
function HotkeyOverview() {
|
||||
return (
|
||||
<ul className="divide-y">
|
||||
{hotkeys.map((props) => {
|
||||
return (
|
||||
<li key={props.key} className="grid grid-cols-4 gap-2 py-0.5">
|
||||
<span className="col-span-1 text-left">
|
||||
<Kbd className="ml-1">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>{props.key}</span>
|
||||
</Kbd>
|
||||
</span>
|
||||
<span className="col-span-3 place-content-center text-xs text-muted-foreground">
|
||||
{props.description}
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user