mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
migration
This commit is contained in:
@@ -1,36 +1,35 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useState} from "react";
|
||||
import {Loader2} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export type VariantButton = {
|
||||
secondary: string
|
||||
default: string
|
||||
outline: string
|
||||
ghost: string
|
||||
link: string
|
||||
destructive: string
|
||||
}
|
||||
secondary: string;
|
||||
default: string;
|
||||
outline: string;
|
||||
ghost: string;
|
||||
link: string;
|
||||
destructive: string;
|
||||
};
|
||||
|
||||
export type ButtonWithConfirmProps = {
|
||||
icon?: any,
|
||||
text: string,
|
||||
variant?: keyof VariantButton,
|
||||
className?: string,
|
||||
onClick?: () => void,
|
||||
isPending?: boolean
|
||||
icon?: any;
|
||||
text: string;
|
||||
variant?: keyof VariantButton;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
isPending?: boolean;
|
||||
};
|
||||
|
||||
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||
|
||||
const [isConfirming, setIsConfirming] = useState(false)
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (isConfirming) {
|
||||
props.onClick()
|
||||
if (isConfirming && props.onClick) {
|
||||
props.onClick();
|
||||
} else {
|
||||
setIsConfirming(true);
|
||||
}
|
||||
@@ -38,12 +37,9 @@ export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||
variant={props.variant ? props.variant : "default"}
|
||||
className={props.className}
|
||||
>
|
||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
||||
{isConfirming ? "Are you sure ?" : `${props.text}`}
|
||||
<>
|
||||
{props.icon ? props.icon : null}
|
||||
</>
|
||||
<>{props.icon ? props.icon : null}</>
|
||||
</Button>
|
||||
)
|
||||
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,60 +1,57 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {ButtonHTMLAttributes} from "react";
|
||||
import {Loader2} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ButtonHTMLAttributes } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
export type VariantButton = {
|
||||
secondary: string
|
||||
default: string
|
||||
outline: string
|
||||
ghost: string
|
||||
link: string
|
||||
destructive: string
|
||||
}
|
||||
secondary: string;
|
||||
default: string;
|
||||
outline: string;
|
||||
ghost: string;
|
||||
link: string;
|
||||
destructive: string;
|
||||
};
|
||||
export type sizeButton = {
|
||||
default :string,
|
||||
icon: string
|
||||
sm: string
|
||||
lg: string
|
||||
}
|
||||
default: string;
|
||||
icon: string;
|
||||
sm: string;
|
||||
lg: string;
|
||||
};
|
||||
|
||||
export type ButtonWithConfirmProps = {
|
||||
icon?: any,
|
||||
text: string,
|
||||
variant?: keyof VariantButton,
|
||||
className?: string,
|
||||
onClick: () => void,
|
||||
isPending?: boolean
|
||||
size: keyof sizeButton
|
||||
icon?: any;
|
||||
text: string;
|
||||
variant?: keyof VariantButton;
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
isPending?: boolean;
|
||||
size: keyof sizeButton;
|
||||
};
|
||||
|
||||
export const ButtonWithLoading = ({
|
||||
icon,
|
||||
text,
|
||||
variant,
|
||||
className,
|
||||
onClick,
|
||||
isPending,
|
||||
size,
|
||||
...props // catch all remaining props
|
||||
}: ButtonWithConfirmProps & ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
icon,
|
||||
text,
|
||||
variant,
|
||||
className,
|
||||
onClick,
|
||||
isPending,
|
||||
size,
|
||||
...props // catch all remaining props
|
||||
}: ButtonWithConfirmProps & ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onClick()
|
||||
onClick();
|
||||
}}
|
||||
variant={variant ? variant : "default"}
|
||||
className={className}
|
||||
{...props} // forward the remaining props to the Button component
|
||||
size={size || "default"}
|
||||
|
||||
>
|
||||
{isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
||||
{isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
||||
{text}
|
||||
<>
|
||||
{icon ? icon : null}
|
||||
</>
|
||||
<>{icon ? icon : null}</>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,46 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import {CheckIcon, ClipboardIcon, Copy} from "lucide-react";
|
||||
import {Button, buttonVariants} from "@/components/ui/button";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {useTranslations} from "use-intl";
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { CheckIcon, Copy } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export async function copyToClipboardWithMeta(value: string) {
|
||||
navigator.clipboard.writeText(value)
|
||||
navigator.clipboard.writeText(value);
|
||||
}
|
||||
|
||||
|
||||
export type CopyButtonProps = {
|
||||
value: string,
|
||||
className: string
|
||||
}
|
||||
|
||||
value: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const CopyButton = (props: CopyButtonProps) => {
|
||||
const { value } = props;
|
||||
|
||||
const {value} = props
|
||||
|
||||
const [hasCopied, setHasCopied] = useState(false)
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setHasCopied(false)
|
||||
}, 2000)
|
||||
}, [hasCopied])
|
||||
setHasCopied(false);
|
||||
}, 2000);
|
||||
}, [hasCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
// className={cn(buttonVariants({size: "sm"}))}
|
||||
onClick={() => {
|
||||
copyToClipboardWithMeta(value)
|
||||
setHasCopied(true)
|
||||
copyToClipboardWithMeta(value);
|
||||
setHasCopied(true);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<span className="mr-2">Copy</span>
|
||||
{hasCopied ? <CheckIcon/> : <Copy size="18"/>}
|
||||
{hasCopied ? <CheckIcon /> : <Copy size="18" />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,46 +1,44 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React, {useState} from 'react'
|
||||
import {cn} from "@/lib/utils";
|
||||
import React, { ComponentType, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import {PaginationNavigation} from "@/components/wrappers/common/pagination/pagination-navigation";
|
||||
import { PaginationNavigation } from "@/components/wrappers/common/pagination/pagination-navigation";
|
||||
|
||||
export type cardsWithPaginationProps = {
|
||||
className?: string
|
||||
organizationSlug?:string
|
||||
data: Array<{}>;
|
||||
cardItem: React.ComponentType;
|
||||
cardsPerPage?: number
|
||||
numberOfColumns?: number
|
||||
maxVisiblePages?: number
|
||||
extendedProps?: any
|
||||
interface CardsWithPaginationProps<T> {
|
||||
className?: string;
|
||||
data: any[];
|
||||
organizationSlug?: string;
|
||||
cardItem: ComponentType<{ data: T; organizationSlug?: string; extendedProps?: any }>;
|
||||
cardsPerPage?: number;
|
||||
numberOfColumns?: number;
|
||||
maxVisiblePages?: number;
|
||||
extendedProps?: any;
|
||||
}
|
||||
|
||||
export function CardsWithPagination<T>(props: CardsWithPaginationProps<T>) {
|
||||
const { className, organizationSlug, data, cardItem, cardsPerPage = 5, numberOfColumns = 1, maxVisiblePages = 3 } = props;
|
||||
|
||||
export const CardsWithPagination = (props: cardsWithPaginationProps) => {
|
||||
const CardItem = cardItem;
|
||||
|
||||
const {className,organizationSlug, data, cardItem, cardsPerPage = 5, numberOfColumns = 1, maxVisiblePages = 3} = props
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = Math.ceil(data.length / cardsPerPage);
|
||||
|
||||
const CardItem = cardItem
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const totalPages = Math.ceil(data.length / cardsPerPage)
|
||||
|
||||
const indexOfLastCard = currentPage * cardsPerPage
|
||||
const indexOfFirstCard = indexOfLastCard - cardsPerPage
|
||||
const currentCards = data.slice(indexOfFirstCard, indexOfLastCard)
|
||||
const indexOfLastCard = currentPage * cardsPerPage;
|
||||
const indexOfFirstCard = indexOfLastCard - cardsPerPage;
|
||||
const currentCards = data.slice(indexOfFirstCard, indexOfLastCard);
|
||||
|
||||
const goToPage = (pageNumber: number) => {
|
||||
setCurrentPage(pageNumber)
|
||||
}
|
||||
setCurrentPage(pageNumber);
|
||||
};
|
||||
|
||||
const goToPrevPage = () => {
|
||||
goToPage(Math.max(1, currentPage - 1))
|
||||
}
|
||||
goToPage(Math.max(1, currentPage - 1));
|
||||
};
|
||||
|
||||
const goToNextPage = () => {
|
||||
goToPage(Math.min(totalPages, currentPage + 1))
|
||||
}
|
||||
goToPage(Math.min(totalPages, currentPage + 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col h-full justify-between", className)}>
|
||||
@@ -56,7 +54,8 @@ export const CardsWithPagination = (props: cardsWithPaginationProps) => {
|
||||
goToPage={goToPage}
|
||||
goToPrevPage={goToPrevPage}
|
||||
goToNextPage={goToNextPage}
|
||||
maxVisiblePages={maxVisiblePages}/>
|
||||
maxVisiblePages={maxVisiblePages}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,74 +1,55 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {useEffect, useState} from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {Check, ChevronDown} from "lucide-react"
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
|
||||
import {cn} from "@/lib/utils"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList} from "@/components/ui/command"
|
||||
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"
|
||||
import {FormControl} from "@/components/ui/form";
|
||||
import {SidebarMenuButton} from "@/components/ui/sidebar";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {CreateOrganizationModal} from "@/components/wrappers/dashboard/organization/create-organisation-modal";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { FormControl } from "@/components/ui/form";
|
||||
import { SidebarMenuButton } from "@/components/ui/sidebar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { CreateOrganizationModal } from "@/components/wrappers/dashboard/organization/create-organisation-modal";
|
||||
|
||||
export type comboBoxProps = {
|
||||
values: Array<{ value: string, label: string }>
|
||||
defaultValue?: string
|
||||
onValueChange?: any
|
||||
searchField?: boolean
|
||||
sideBar?: boolean
|
||||
}
|
||||
|
||||
values: Array<{ value: string; label: string }>;
|
||||
defaultValue?: string;
|
||||
onValueChange?: any;
|
||||
searchField?: boolean;
|
||||
sideBar?: boolean;
|
||||
};
|
||||
|
||||
export function ComboBox(props: comboBoxProps) {
|
||||
const { values: choices, defaultValue: defaultChoice = "", onValueChange, searchField = false, sideBar = false } = props;
|
||||
|
||||
const {
|
||||
values: choices,
|
||||
defaultValue: defaultChoice = "",
|
||||
onValueChange,
|
||||
searchField = false,
|
||||
sideBar = false
|
||||
} = props;
|
||||
|
||||
const [value, setValue] = useState<string>()
|
||||
const [value, setValue] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
setValue(defaultChoice)
|
||||
}, [defaultChoice])
|
||||
setValue(defaultChoice);
|
||||
}, [defaultChoice]);
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
{sideBar ?
|
||||
{sideBar ? (
|
||||
<SidebarMenuButton>
|
||||
{value
|
||||
? choices.find((choice) => choice.value === value)?.label
|
||||
: "Select choice..."}
|
||||
<ChevronDown className="ml-auto"/>
|
||||
{value ? choices.find((choice) => choice.value === value)?.label : "Select choice..."}
|
||||
<ChevronDown className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
:
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{value
|
||||
? choices.find((choice) => choice.value === value)?.label
|
||||
: "Select choice..."}
|
||||
<ChevronDown className="opacity-50"/>
|
||||
) : (
|
||||
<Button variant="outline" role="combobox" aria-expanded={open} className="w-full justify-between">
|
||||
{value ? choices.find((choice) => choice.value === value)?.label : "Select choice..."}
|
||||
<ChevronDown className="opacity-50" />
|
||||
</Button>
|
||||
}
|
||||
|
||||
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 popover-content-width-full">
|
||||
<Command>
|
||||
{searchField ? <CommandInput placeholder="Search choice..." className="h-9"/> : null}
|
||||
{searchField ? <CommandInput placeholder="Search choice..." className="h-9" /> : null}
|
||||
<CommandList>
|
||||
<CommandEmpty>No choice found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
@@ -77,75 +58,54 @@ export function ComboBox(props: comboBoxProps) {
|
||||
key={choice.value}
|
||||
value={choice.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue)
|
||||
onValueChange(currentValue)
|
||||
setOpen(false)
|
||||
setValue(currentValue);
|
||||
onValueChange(currentValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{choice.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
value === choice.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<Check className={cn("ml-auto", value === choice.value ? "opacity-100" : "opacity-0")} />
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
<Separator/>
|
||||
{sideBar ?
|
||||
<Separator />
|
||||
{sideBar ? (
|
||||
<CreateOrganizationModal>
|
||||
<SidebarMenuButton>
|
||||
+ Create new organization
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton>+ Create new organization</SidebarMenuButton>
|
||||
</CreateOrganizationModal>
|
||||
: null}
|
||||
) : null}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export type comboBoxFormItemProps = comboBoxProps & {
|
||||
value: any
|
||||
name: any
|
||||
onChange: any
|
||||
value: any;
|
||||
name: any;
|
||||
onChange: any;
|
||||
};
|
||||
|
||||
/** Combobox to use when working with zodForm. */
|
||||
export function ComboBoxFormItem(props: comboBoxFormItemProps) {
|
||||
const { values: choices, searchField = false, value, name, onChange } = props;
|
||||
|
||||
const {values: choices, searchField = false, value, name, onChange} = props;
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
!value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{value
|
||||
? choices.find(
|
||||
(choice) => choice.value === value
|
||||
)?.label
|
||||
: `Select ${name}`}
|
||||
<ChevronDown className="opacity-50"/>
|
||||
<Button variant="outline" role="combobox" aria-expanded={open} className={cn("w-full justify-between", !value && "text-muted-foreground")}>
|
||||
{value ? choices.find((choice) => choice.value === value)?.label : `Select ${name}`}
|
||||
<ChevronDown className="opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 popover-content-width-full">
|
||||
<Command>
|
||||
{searchField ? <CommandInput placeholder="Search choice..." className="h-9"/> : null}
|
||||
{searchField ? <CommandInput placeholder="Search choice..." className="h-9" /> : null}
|
||||
<CommandList>
|
||||
<CommandEmpty>No {name} found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
@@ -154,19 +114,12 @@ export function ComboBoxFormItem(props: comboBoxFormItemProps) {
|
||||
value={choice.label}
|
||||
key={choice.value}
|
||||
onSelect={() => {
|
||||
onChange(choice.value)
|
||||
setOpen(false)
|
||||
onChange(choice.value);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{choice.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
choice.value === value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<Check className={cn("ml-auto", choice.value === value ? "opacity-100" : "opacity-0")} />
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
@@ -174,5 +127,5 @@ export function ComboBoxFormItem(props: comboBoxFormItemProps) {
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import {cn} from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type connectionCircleProps = {
|
||||
date: Date
|
||||
}
|
||||
export type ConnectionCircleProps = {
|
||||
date?: Date | null;
|
||||
};
|
||||
|
||||
export const ConnectionCircle = ({ date }: ConnectionCircleProps) => {
|
||||
let style = "bg-gray-300 border-gray-400";
|
||||
|
||||
export const ConnectionCircle = ({date}: connectionCircleProps) => {
|
||||
if (date) {
|
||||
const now = Date.now();
|
||||
const timestamp = new Date(date).getTime();
|
||||
const interval = now - timestamp;
|
||||
|
||||
let style = "";
|
||||
const interval = new Date().getTime() - new Date(date).getTime()
|
||||
if (interval < 10000) {
|
||||
style = "bg-green-400 border-green-600"
|
||||
} else if (1000 <= interval && interval <= 20000) {
|
||||
style = "bg-orange-400 border-orange-600"
|
||||
} else {
|
||||
style = "bg-red-400 border-red-600"
|
||||
if (interval < 10000) {
|
||||
style = "bg-green-400 border-green-600";
|
||||
} else if (interval <= 20000) {
|
||||
style = "bg-orange-400 border-orange-600";
|
||||
} else {
|
||||
style = "bg-red-400 border-red-600";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div color="red" className={cn("w-5 h-5 rounded-3xl border-4", style)}/>
|
||||
)
|
||||
}
|
||||
return <div className={cn("w-5 h-5 rounded-full border-4", style)} />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { format } from "date-fns";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DateTimePickerProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function DateTimePicker({ name }: DateTimePickerProps) {
|
||||
const form = useFormContext();
|
||||
const value: Date | null = form.watch(name);
|
||||
|
||||
const handleDateSelect = (date: Date | undefined) => {
|
||||
if (date) {
|
||||
const current = form.getValues(name) ?? new Date();
|
||||
date.setHours(current.getHours());
|
||||
date.setMinutes(current.getMinutes());
|
||||
form.setValue(name, date);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTimeChange = (type: "hour" | "minute" | "ampm", val: string) => {
|
||||
const currentDate = form.getValues(name) ?? new Date();
|
||||
const newDate = new Date(currentDate);
|
||||
|
||||
if (type === "hour") {
|
||||
const hour = parseInt(val, 10);
|
||||
const isPM = newDate.getHours() >= 12;
|
||||
newDate.setHours((isPM ? 12 : 0) + (hour % 12));
|
||||
} else if (type === "minute") {
|
||||
newDate.setMinutes(parseInt(val, 10));
|
||||
} else if (type === "ampm") {
|
||||
const hours = newDate.getHours();
|
||||
if (val === "AM" && hours >= 12) {
|
||||
newDate.setHours(hours - 12);
|
||||
} else if (val === "PM" && hours < 12) {
|
||||
newDate.setHours(hours + 12);
|
||||
}
|
||||
}
|
||||
|
||||
form.setValue(name, newDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className={cn("w-full pl-3 text-left font-normal", !value && "text-muted-foreground")}>
|
||||
{value ? format(value, "MM/dd/yyyy hh:mm aa") : <span>MM/DD/YYYY hh:mm aa</span>}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<div className="sm:flex">
|
||||
<Calendar mode="single" selected={value ?? undefined} onSelect={handleDateSelect} initialFocus />
|
||||
<div className="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
|
||||
{/* Hours */}
|
||||
<ScrollArea className="w-64 sm:w-auto">
|
||||
<div className="flex sm:flex-col p-2">
|
||||
{Array.from({ length: 12 }, (_, i) => i + 1)
|
||||
.reverse()
|
||||
.map((hour) => (
|
||||
<Button
|
||||
key={hour}
|
||||
size="icon"
|
||||
variant={value && value.getHours() % 12 === hour % 12 ? "default" : "ghost"}
|
||||
className="sm:w-full shrink-0 aspect-square"
|
||||
onClick={() => handleTimeChange("hour", hour.toString())}
|
||||
>
|
||||
{hour}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" className="sm:hidden" />
|
||||
</ScrollArea>
|
||||
|
||||
{/* Minutes */}
|
||||
<ScrollArea className="w-64 sm:w-auto">
|
||||
<div className="flex sm:flex-col p-2">
|
||||
{Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (
|
||||
<Button
|
||||
key={minute}
|
||||
size="icon"
|
||||
variant={value && value.getMinutes() === minute ? "default" : "ghost"}
|
||||
className="sm:w-full shrink-0 aspect-square"
|
||||
onClick={() => handleTimeChange("minute", minute.toString())}
|
||||
>
|
||||
{minute.toString().padStart(2, "0")}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" className="sm:hidden" />
|
||||
</ScrollArea>
|
||||
|
||||
{/* AM/PM */}
|
||||
<ScrollArea>
|
||||
<div className="flex sm:flex-col p-2">
|
||||
{["AM", "PM"].map((ampm) => (
|
||||
<Button
|
||||
key={ampm}
|
||||
size="icon"
|
||||
variant={
|
||||
value && ((ampm === "AM" && value.getHours() < 12) || (ampm === "PM" && value.getHours() >= 12))
|
||||
? "default"
|
||||
: "ghost"
|
||||
}
|
||||
className="sm:w-full shrink-0 aspect-square"
|
||||
onClick={() => handleTimeChange("ampm", ampm)}
|
||||
>
|
||||
{ampm}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,770 +0,0 @@
|
||||
import {Button, buttonVariants} from '@/components/ui/button';
|
||||
import type {CalendarProps} from '@/components/ui/calendar';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Popover, PopoverContent, PopoverTrigger} from '@/components/ui/popover';
|
||||
import {cn} from '@/lib/utils';
|
||||
import {add, format} from 'date-fns';
|
||||
import {type Locale, enUS} from 'date-fns/locale';
|
||||
import {Calendar as CalendarIcon, ChevronLeft, ChevronRight} from 'lucide-react';
|
||||
import {Clock} from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import {useImperativeHandle, useRef} from 'react';
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {DayPicker} from 'react-day-picker';
|
||||
|
||||
// ---------- utils start ----------
|
||||
/**
|
||||
* regular expression to check for valid hour format (01-23)
|
||||
*/
|
||||
function isValidHour(value: string) {
|
||||
return /^(0[0-9]|1[0-9]|2[0-3])$/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* regular expression to check for valid 12 hour format (01-12)
|
||||
*/
|
||||
function isValid12Hour(value: string) {
|
||||
return /^(0[1-9]|1[0-2])$/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* regular expression to check for valid minute format (00-59)
|
||||
*/
|
||||
function isValidMinuteOrSecond(value: string) {
|
||||
return /^[0-5][0-9]$/.test(value);
|
||||
}
|
||||
|
||||
type GetValidNumberConfig = { max: number; min?: number; loop?: boolean };
|
||||
|
||||
function getValidNumber(value: string, {max, min = 0, loop = false}: GetValidNumberConfig) {
|
||||
let numericValue = parseInt(value, 10);
|
||||
|
||||
if (!Number.isNaN(numericValue)) {
|
||||
if (!loop) {
|
||||
if (numericValue > max) numericValue = max;
|
||||
if (numericValue < min) numericValue = min;
|
||||
} else {
|
||||
if (numericValue > max) numericValue = min;
|
||||
if (numericValue < min) numericValue = max;
|
||||
}
|
||||
return numericValue.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
return '00';
|
||||
}
|
||||
|
||||
function getValidHour(value: string) {
|
||||
if (isValidHour(value)) return value;
|
||||
return getValidNumber(value, {max: 23});
|
||||
}
|
||||
|
||||
function getValid12Hour(value: string) {
|
||||
if (isValid12Hour(value)) return value;
|
||||
return getValidNumber(value, {min: 1, max: 12});
|
||||
}
|
||||
|
||||
function getValidMinuteOrSecond(value: string) {
|
||||
if (isValidMinuteOrSecond(value)) return value;
|
||||
return getValidNumber(value, {max: 59});
|
||||
}
|
||||
|
||||
type GetValidArrowNumberConfig = {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
};
|
||||
|
||||
function getValidArrowNumber(value: string, {min, max, step}: GetValidArrowNumberConfig) {
|
||||
let numericValue = parseInt(value, 10);
|
||||
if (!Number.isNaN(numericValue)) {
|
||||
numericValue += step;
|
||||
return getValidNumber(String(numericValue), {min, max, loop: true});
|
||||
}
|
||||
return '00';
|
||||
}
|
||||
|
||||
function getValidArrowHour(value: string, step: number) {
|
||||
return getValidArrowNumber(value, {min: 0, max: 23, step});
|
||||
}
|
||||
|
||||
function getValidArrow12Hour(value: string, step: number) {
|
||||
return getValidArrowNumber(value, {min: 1, max: 12, step});
|
||||
}
|
||||
|
||||
function getValidArrowMinuteOrSecond(value: string, step: number) {
|
||||
return getValidArrowNumber(value, {min: 0, max: 59, step});
|
||||
}
|
||||
|
||||
function setMinutes(date: Date, value: string) {
|
||||
const minutes = getValidMinuteOrSecond(value);
|
||||
date.setMinutes(parseInt(minutes, 10));
|
||||
return date;
|
||||
}
|
||||
|
||||
function setSeconds(date: Date, value: string) {
|
||||
const seconds = getValidMinuteOrSecond(value);
|
||||
date.setSeconds(parseInt(seconds, 10));
|
||||
return date;
|
||||
}
|
||||
|
||||
function setHours(date: Date, value: string) {
|
||||
const hours = getValidHour(value);
|
||||
date.setHours(parseInt(hours, 10));
|
||||
return date;
|
||||
}
|
||||
|
||||
function set12Hours(date: Date, value: string, period: Period) {
|
||||
const hours = parseInt(getValid12Hour(value), 10);
|
||||
const convertedHours = convert12HourTo24Hour(hours, period);
|
||||
date.setHours(convertedHours);
|
||||
return date;
|
||||
}
|
||||
|
||||
type TimePickerType = 'minutes' | 'seconds' | 'hours' | '12hours';
|
||||
type Period = 'AM' | 'PM';
|
||||
|
||||
function setDateByType(date: Date, value: string, type: TimePickerType, period?: Period) {
|
||||
switch (type) {
|
||||
case 'minutes':
|
||||
return setMinutes(date, value);
|
||||
case 'seconds':
|
||||
return setSeconds(date, value);
|
||||
case 'hours':
|
||||
return setHours(date, value);
|
||||
case '12hours': {
|
||||
if (!period) return date;
|
||||
return set12Hours(date, value, period);
|
||||
}
|
||||
default:
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
function getDateByType(date: Date | null, type: TimePickerType) {
|
||||
if (!date) return '00';
|
||||
switch (type) {
|
||||
case 'minutes':
|
||||
return getValidMinuteOrSecond(String(date.getMinutes()));
|
||||
case 'seconds':
|
||||
return getValidMinuteOrSecond(String(date.getSeconds()));
|
||||
case 'hours':
|
||||
return getValidHour(String(date.getHours()));
|
||||
case '12hours':
|
||||
return getValid12Hour(String(display12HourValue(date.getHours())));
|
||||
default:
|
||||
return '00';
|
||||
}
|
||||
}
|
||||
|
||||
function getArrowByType(value: string, step: number, type: TimePickerType) {
|
||||
switch (type) {
|
||||
case 'minutes':
|
||||
return getValidArrowMinuteOrSecond(value, step);
|
||||
case 'seconds':
|
||||
return getValidArrowMinuteOrSecond(value, step);
|
||||
case 'hours':
|
||||
return getValidArrowHour(value, step);
|
||||
case '12hours':
|
||||
return getValidArrow12Hour(value, step);
|
||||
default:
|
||||
return '00';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handles value change of 12-hour input
|
||||
* 12:00 PM is 12:00
|
||||
* 12:00 AM is 00:00
|
||||
*/
|
||||
function convert12HourTo24Hour(hour: number, period: Period) {
|
||||
if (period === 'PM') {
|
||||
if (hour <= 11) {
|
||||
return hour + 12;
|
||||
}
|
||||
return hour;
|
||||
}
|
||||
|
||||
if (period === 'AM') {
|
||||
if (hour === 12) return 0;
|
||||
return hour;
|
||||
}
|
||||
return hour;
|
||||
}
|
||||
|
||||
/**
|
||||
* time is stored in the 24-hour form,
|
||||
* but needs to be displayed to the user
|
||||
* in its 12-hour representation
|
||||
*/
|
||||
function display12HourValue(hours: number) {
|
||||
if (hours === 0 || hours === 12) return '12';
|
||||
if (hours >= 22) return `${hours - 12}`;
|
||||
if (hours % 12 > 9) return `${hours}`;
|
||||
return `0${hours % 12}`;
|
||||
}
|
||||
|
||||
function genMonths(locale: Pick<Locale, 'options' | 'localize' | 'formatLong'>) {
|
||||
return Array.from({length: 12}, (_, i) => ({
|
||||
value: i,
|
||||
label: format(new Date(2021, i), 'MMMM', {locale}),
|
||||
}));
|
||||
}
|
||||
|
||||
function genYears(yearRange = 50) {
|
||||
const today = new Date();
|
||||
return Array.from({length: yearRange * 2 + 1}, (_, i) => ({
|
||||
value: today.getFullYear() - yearRange + i,
|
||||
label: (today.getFullYear() - yearRange + i).toString(),
|
||||
}));
|
||||
}
|
||||
|
||||
// ---------- utils end ----------
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
yearRange = 50,
|
||||
...props
|
||||
}: CalendarProps & { yearRange?: number }) {
|
||||
const MONTHS = React.useMemo(() => {
|
||||
let locale: Pick<Locale, 'options' | 'localize' | 'formatLong'> = enUS;
|
||||
const {options, localize, formatLong} = props.locale || {};
|
||||
if (options && localize && formatLong) {
|
||||
locale = {
|
||||
options,
|
||||
localize,
|
||||
formatLong,
|
||||
};
|
||||
}
|
||||
return genMonths(locale);
|
||||
}, []);
|
||||
|
||||
const YEARS = React.useMemo(() => genYears(yearRange), []);
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn('p-3', className)}
|
||||
classNames={{
|
||||
months: 'flex flex-col sm:flex-row space-y-4 sm:space-y-0 justify-center',
|
||||
month: 'flex flex-col items-center space-y-4',
|
||||
month_caption: 'flex justify-center pt-1 relative items-center',
|
||||
caption_label: 'text-sm font-medium',
|
||||
nav: 'space-x-1 flex items-center ',
|
||||
button_previous: cn(
|
||||
buttonVariants({variant: 'outline'}),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute left-5 top-5',
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({variant: 'outline'}),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute right-5 top-5',
|
||||
),
|
||||
month_grid: 'w-full border-collapse space-y-1',
|
||||
weekdays: cn('flex', props.showWeekNumber && 'justify-end'),
|
||||
weekday: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
|
||||
week: 'flex w-full mt-2',
|
||||
day: '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 rounded-1',
|
||||
day_button: cn(
|
||||
buttonVariants({variant: 'ghost'}),
|
||||
'h-9 w-9 p-0 font-normal aria-selected:opacity-100 rounded-l-md rounded-r-md',
|
||||
),
|
||||
range_end: 'day-range-end',
|
||||
selected:
|
||||
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground rounded-l-md rounded-r-md',
|
||||
today: 'bg-accent text-accent-foreground',
|
||||
outside:
|
||||
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
|
||||
disabled: 'text-muted-foreground opacity-50',
|
||||
range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
||||
hidden: 'invisible',
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Chevron: ({...props}) =>
|
||||
props.orientation === 'left' ? (
|
||||
<ChevronLeft className="h-4 w-4"/>
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4"/>
|
||||
),
|
||||
MonthCaption: ({calendarMonth}) => {
|
||||
return (
|
||||
<div className="inline-flex gap-2">
|
||||
<Select
|
||||
defaultValue={calendarMonth.date.getMonth().toString()}
|
||||
onValueChange={(value) => {
|
||||
const newDate = new Date(calendarMonth.date);
|
||||
newDate.setMonth(Number.parseInt(value, 10));
|
||||
props.onMonthChange?.(newDate);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-fit gap-1 border-none p-0 focus:bg-accent focus:text-accent-foreground">
|
||||
<SelectValue/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MONTHS.map((month) => (
|
||||
<SelectItem key={month.value} value={month.value.toString()}>
|
||||
{month.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
defaultValue={calendarMonth.date.getFullYear().toString()}
|
||||
onValueChange={(value) => {
|
||||
const newDate = new Date(calendarMonth.date);
|
||||
newDate.setFullYear(Number.parseInt(value, 10));
|
||||
props.onMonthChange?.(newDate);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-fit gap-1 border-none p-0 focus:bg-accent focus:text-accent-foreground">
|
||||
<SelectValue/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{YEARS.map((year) => (
|
||||
<SelectItem key={year.value} value={year.value.toString()}>
|
||||
{year.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Calendar.displayName = 'Calendar';
|
||||
|
||||
interface PeriodSelectorProps {
|
||||
period: Period;
|
||||
setPeriod?: (m: Period) => void;
|
||||
date?: Date | null;
|
||||
onDateChange?: (date: Date | undefined) => void;
|
||||
onRightFocus?: () => void;
|
||||
onLeftFocus?: () => void;
|
||||
}
|
||||
|
||||
const TimePeriodSelect = React.forwardRef<HTMLButtonElement, PeriodSelectorProps>(
|
||||
({period, setPeriod, date, onDateChange, onLeftFocus, onRightFocus}, ref) => {
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (e.key === 'ArrowRight') onRightFocus?.();
|
||||
if (e.key === 'ArrowLeft') onLeftFocus?.();
|
||||
};
|
||||
|
||||
const handleValueChange = (value: Period) => {
|
||||
setPeriod?.(value);
|
||||
|
||||
/**
|
||||
* trigger an update whenever the user switches between AM and PM;
|
||||
* otherwise user must manually change the hour each time
|
||||
*/
|
||||
if (date) {
|
||||
const tempDate = new Date(date);
|
||||
const hours = display12HourValue(date.getHours());
|
||||
onDateChange?.(
|
||||
setDateByType(tempDate, hours.toString(), '12hours', period === 'AM' ? 'PM' : 'AM'),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-10 items-center">
|
||||
<Select defaultValue={period} onValueChange={(value: Period) => handleValueChange(value)}>
|
||||
<SelectTrigger
|
||||
ref={ref}
|
||||
className="w-[65px] focus:bg-accent focus:text-accent-foreground"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<SelectValue/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="AM">AM</SelectItem>
|
||||
<SelectItem value="PM">PM</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TimePeriodSelect.displayName = 'TimePeriodSelect';
|
||||
|
||||
interface TimePickerInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
picker: TimePickerType;
|
||||
date?: Date | null;
|
||||
onDateChange?: (date: Date | undefined) => void;
|
||||
period?: Period;
|
||||
onRightFocus?: () => void;
|
||||
onLeftFocus?: () => void;
|
||||
}
|
||||
|
||||
const TimePickerInput = React.forwardRef<HTMLInputElement, TimePickerInputProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
type = 'tel',
|
||||
value,
|
||||
id,
|
||||
name,
|
||||
date = new Date(new Date().setHours(0, 0, 0, 0)),
|
||||
onDateChange,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
picker,
|
||||
period,
|
||||
onLeftFocus,
|
||||
onRightFocus,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [flag, setFlag] = React.useState<boolean>(false);
|
||||
const [prevIntKey, setPrevIntKey] = React.useState<string>('0');
|
||||
|
||||
/**
|
||||
* allow the user to enter the second digit within 2 seconds
|
||||
* otherwise start again with entering first digit
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (flag) {
|
||||
const timer = setTimeout(() => {
|
||||
setFlag(false);
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [flag]);
|
||||
|
||||
const calculatedValue = React.useMemo(() => {
|
||||
return getDateByType(date, picker);
|
||||
}, [date, picker]);
|
||||
|
||||
const calculateNewValue = (key: string) => {
|
||||
/*
|
||||
* If picker is '12hours' and the first digit is 0, then the second digit is automatically set to 1.
|
||||
* The second entered digit will break the condition and the value will be set to 10-12.
|
||||
*/
|
||||
if (picker === '12hours') {
|
||||
if (flag && calculatedValue.slice(1, 2) === '1' && prevIntKey === '0') return `0${key}`;
|
||||
}
|
||||
|
||||
return !flag ? `0${key}` : calculatedValue.slice(1, 2) + key;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Tab') return;
|
||||
e.preventDefault();
|
||||
if (e.key === 'ArrowRight') onRightFocus?.();
|
||||
if (e.key === 'ArrowLeft') onLeftFocus?.();
|
||||
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||
const step = e.key === 'ArrowUp' ? 1 : -1;
|
||||
const newValue = getArrowByType(calculatedValue, step, picker);
|
||||
if (flag) setFlag(false);
|
||||
const tempDate = date ? new Date(date) : new Date();
|
||||
onDateChange?.(setDateByType(tempDate, newValue, picker, period));
|
||||
}
|
||||
if (e.key >= '0' && e.key <= '9') {
|
||||
if (picker === '12hours') setPrevIntKey(e.key);
|
||||
|
||||
const newValue = calculateNewValue(e.key);
|
||||
if (flag) onRightFocus?.();
|
||||
setFlag((prev) => !prev);
|
||||
const tempDate = date ? new Date(date) : new Date();
|
||||
onDateChange?.(setDateByType(tempDate, newValue, picker, period));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
id={id || picker}
|
||||
name={name || picker}
|
||||
className={cn(
|
||||
'w-[48px] text-center font-mono text-base tabular-nums caret-transparent focus:bg-accent focus:text-accent-foreground [&::-webkit-inner-spin-button]:appearance-none',
|
||||
className,
|
||||
)}
|
||||
value={value || calculatedValue}
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
onChange?.(e);
|
||||
}}
|
||||
type={type}
|
||||
inputMode="decimal"
|
||||
onKeyDown={(e) => {
|
||||
onKeyDown?.(e);
|
||||
handleKeyDown(e);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TimePickerInput.displayName = 'TimePickerInput';
|
||||
|
||||
interface TimePickerProps {
|
||||
date?: Date | null;
|
||||
onChange?: (date: Date | undefined) => void;
|
||||
hourCycle?: 12 | 24;
|
||||
/**
|
||||
* Determines the smallest unit that is displayed in the datetime picker.
|
||||
* Default is 'second'.
|
||||
* */
|
||||
granularity?: Granularity;
|
||||
}
|
||||
|
||||
interface TimePickerRef {
|
||||
minuteRef: HTMLInputElement | null;
|
||||
hourRef: HTMLInputElement | null;
|
||||
secondRef: HTMLInputElement | null;
|
||||
}
|
||||
|
||||
const TimePicker = React.forwardRef<TimePickerRef, TimePickerProps>(
|
||||
({date, onChange, hourCycle = 24, granularity = 'second'}, ref) => {
|
||||
const minuteRef = React.useRef<HTMLInputElement>(null);
|
||||
const hourRef = React.useRef<HTMLInputElement>(null);
|
||||
const secondRef = React.useRef<HTMLInputElement>(null);
|
||||
const periodRef = React.useRef<HTMLButtonElement>(null);
|
||||
const [period, setPeriod] = React.useState<Period>(date && date.getHours() >= 12 ? 'PM' : 'AM');
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
minuteRef: minuteRef.current,
|
||||
hourRef: hourRef.current,
|
||||
secondRef: secondRef.current,
|
||||
periodRef: periodRef.current,
|
||||
}),
|
||||
[minuteRef, hourRef, secondRef],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<label htmlFor="datetime-picker-hour-input" className="cursor-pointer">
|
||||
<Clock className="mr-2 h-4 w-4"/>
|
||||
</label>
|
||||
<TimePickerInput
|
||||
picker={hourCycle === 24 ? 'hours' : '12hours'}
|
||||
date={date}
|
||||
id="datetime-picker-hour-input"
|
||||
onDateChange={onChange}
|
||||
ref={hourRef}
|
||||
period={period}
|
||||
onRightFocus={() => minuteRef?.current?.focus()}
|
||||
/>
|
||||
{(granularity === 'minute' || granularity === 'second') && (
|
||||
<>
|
||||
:
|
||||
<TimePickerInput
|
||||
picker="minutes"
|
||||
date={date}
|
||||
onDateChange={onChange}
|
||||
ref={minuteRef}
|
||||
onLeftFocus={() => hourRef?.current?.focus()}
|
||||
onRightFocus={() => secondRef?.current?.focus()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{granularity === 'second' && (
|
||||
<>
|
||||
:
|
||||
<TimePickerInput
|
||||
picker="seconds"
|
||||
date={date}
|
||||
onDateChange={onChange}
|
||||
ref={secondRef}
|
||||
onLeftFocus={() => minuteRef?.current?.focus()}
|
||||
onRightFocus={() => periodRef?.current?.focus()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{hourCycle === 12 && (
|
||||
<div className="grid gap-1 text-center">
|
||||
<TimePeriodSelect
|
||||
period={period}
|
||||
setPeriod={setPeriod}
|
||||
date={date}
|
||||
onDateChange={(date) => {
|
||||
onChange?.(date);
|
||||
if (date && date?.getHours() >= 12) {
|
||||
setPeriod('PM');
|
||||
} else {
|
||||
setPeriod('AM');
|
||||
}
|
||||
}}
|
||||
ref={periodRef}
|
||||
onLeftFocus={() => secondRef?.current?.focus()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
TimePicker.displayName = 'TimePicker';
|
||||
|
||||
type Granularity = 'day' | 'hour' | 'minute' | 'second';
|
||||
|
||||
type DateTimePickerProps = {
|
||||
value?: Date;
|
||||
onChange?: (date: Date | undefined) => void;
|
||||
disabled?: boolean;
|
||||
/** showing `AM/PM` or not. */
|
||||
hourCycle?: 12 | 24;
|
||||
placeholder?: string;
|
||||
/**
|
||||
* The year range will be: `This year + yearRange` and `this year - yearRange`.
|
||||
* Default is 50.
|
||||
* For example:
|
||||
* This year is 2024, The year dropdown will be 1974 to 2024 which is generated by `2024 - 50 = 1974` and `2024 + 50 = 2074`.
|
||||
* */
|
||||
yearRange?: number;
|
||||
/**
|
||||
* The format is derived from the `date-fns` documentation.
|
||||
* @reference https://date-fns.org/v3.6.0/docs/format
|
||||
**/
|
||||
displayFormat?: { hour24?: string; hour12?: string };
|
||||
/**
|
||||
* The granularity prop allows you to control the smallest unit that is displayed by DateTimePicker.
|
||||
* By default, the value is `second` which shows all time inputs.
|
||||
**/
|
||||
granularity?: Granularity;
|
||||
className?: string;
|
||||
} & Pick<CalendarProps, 'locale' | 'weekStartsOn' | 'showWeekNumber' | 'showOutsideDays'>;
|
||||
|
||||
type DateTimePickerRef = {
|
||||
value?: Date;
|
||||
} & Omit<HTMLButtonElement, 'value'>;
|
||||
|
||||
const DateTimePicker = React.forwardRef<Partial<DateTimePickerRef>, DateTimePickerProps>(
|
||||
(
|
||||
{
|
||||
locale = enUS,
|
||||
value,
|
||||
onChange,
|
||||
hourCycle = 24,
|
||||
yearRange = 50,
|
||||
disabled = false,
|
||||
displayFormat,
|
||||
granularity = 'second',
|
||||
placeholder = 'Pick a date',
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [month, setMonth] = React.useState<Date>(value ?? new Date());
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
/**
|
||||
* carry over the current time when a user clicks a new day
|
||||
* instead of resetting to 00:00
|
||||
*/
|
||||
const handleSelect = (newDay: Date | undefined) => {
|
||||
if (!newDay) return;
|
||||
if (!value) {
|
||||
onChange?.(newDay);
|
||||
setMonth(newDay);
|
||||
return;
|
||||
}
|
||||
const diff = newDay.getTime() - value.getTime();
|
||||
const diffInDays = diff / (1000 * 60 * 60 * 24);
|
||||
const newDateFull = add(value, {days: Math.ceil(diffInDays)});
|
||||
onChange?.(newDateFull);
|
||||
setMonth(newDateFull);
|
||||
};
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
...buttonRef.current,
|
||||
value,
|
||||
}),
|
||||
[value],
|
||||
);
|
||||
|
||||
const initHourFormat = {
|
||||
hour24:
|
||||
displayFormat?.hour24 ??
|
||||
`PPP HH:mm${!granularity || granularity === 'second' ? ':ss' : ''}`,
|
||||
hour12:
|
||||
displayFormat?.hour12 ??
|
||||
`PP hh:mm${!granularity || granularity === 'second' ? ':ss' : ''} b`,
|
||||
};
|
||||
|
||||
let loc = enUS;
|
||||
const {options, localize, formatLong} = locale;
|
||||
if (options && localize && formatLong) {
|
||||
loc = {
|
||||
...enUS,
|
||||
options,
|
||||
localize,
|
||||
formatLong,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild disabled={disabled}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!value && 'text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4"/>
|
||||
{value ? (
|
||||
format(value, hourCycle === 24 ? initHourFormat.hour24 : initHourFormat.hour12, {
|
||||
locale: loc,
|
||||
})
|
||||
) : (
|
||||
<span>{placeholder}</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={value}
|
||||
month={month}
|
||||
onSelect={(d) => handleSelect(d)}
|
||||
onMonthChange={handleSelect}
|
||||
yearRange={yearRange}
|
||||
locale={locale}
|
||||
{...props}
|
||||
/>
|
||||
{granularity !== 'day' && (
|
||||
<div className="border-t border-border p-3">
|
||||
<TimePicker
|
||||
onChange={onChange}
|
||||
date={value}
|
||||
hourCycle={hourCycle}
|
||||
granularity={granularity}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DateTimePicker.displayName = 'DateTimePicker';
|
||||
|
||||
export {DateTimePicker, TimePickerInput, TimePicker};
|
||||
export type {TimePickerType, DateTimePickerProps, DateTimePickerRef};
|
||||
@@ -1,28 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {
|
||||
Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle
|
||||
} from "@/components/ui/dialog"
|
||||
import {Input} from "@/components/ui/input"
|
||||
import {Label} from "@/components/ui/label"
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export type agentRegistrationDialogProps = {
|
||||
open: boolean,
|
||||
setOpen: (open: boolean) => void,
|
||||
}
|
||||
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const AgentRegistrationDialog = (props: agentRegistrationDialogProps) => {
|
||||
|
||||
const {open, setOpen} = props;
|
||||
const { open, setOpen } = props;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>New agent registered!</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -32,7 +25,7 @@ export const AgentRegistrationDialog = (props: agentRegistrationDialogProps) =>
|
||||
<Label htmlFor="name" className="text-right">
|
||||
EDGE KEY
|
||||
</Label>
|
||||
<Input id="name" value="Pedro Duarte" readOnly className="col-span-3"/>
|
||||
<Input id="name" value="Pedro Duarte" readOnly className="col-span-3" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
@@ -40,5 +33,5 @@ export const AgentRegistrationDialog = (props: agentRegistrationDialogProps) =>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dispatch, SetStateAction, createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import { useDropzone, DropzoneState, FileRejection, DropzoneOptions } from "react-dropzone";
|
||||
import { toast } from "sonner";
|
||||
import { Trash2 as RemoveIcon } from "lucide-react";
|
||||
|
||||
type DirectionOptions = "rtl" | "ltr" | undefined;
|
||||
|
||||
type FileUploaderContextType = {
|
||||
dropzoneState: DropzoneState;
|
||||
isLOF: boolean;
|
||||
isFileTooBig: boolean;
|
||||
removeFileFromSet: (index: number) => void;
|
||||
activeIndex: number;
|
||||
setActiveIndex: Dispatch<SetStateAction<number>>;
|
||||
orientation: "horizontal" | "vertical";
|
||||
direction: DirectionOptions;
|
||||
};
|
||||
|
||||
const FileUploaderContext = createContext<FileUploaderContextType | null>(null);
|
||||
|
||||
export const useFileUpload = () => {
|
||||
const context = useContext(FileUploaderContext);
|
||||
if (!context) {
|
||||
throw new Error("useFileUpload must be used within a FileUploaderProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
type FileUploaderProps = {
|
||||
value: File[] | null;
|
||||
reSelect?: boolean;
|
||||
onValueChange: (value: File[] | null) => void;
|
||||
dropzoneOptions: DropzoneOptions;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
};
|
||||
|
||||
/**
|
||||
* File upload Docs: {@link: https://localhost:3000/docs/file-upload}
|
||||
*/
|
||||
|
||||
export const FileUploader = forwardRef<HTMLDivElement, FileUploaderProps & React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, dropzoneOptions, value, onValueChange, reSelect, orientation = "vertical", children, dir, ...props }, ref) => {
|
||||
const [isFileTooBig, setIsFileTooBig] = useState(false);
|
||||
const [isLOF, setIsLOF] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState(-1);
|
||||
const {
|
||||
accept = {
|
||||
"image/*": [".jpg", ".jpeg", ".png", ".gif"],
|
||||
},
|
||||
maxFiles = 1,
|
||||
maxSize = 4 * 1024 * 1024,
|
||||
multiple = true,
|
||||
} = dropzoneOptions;
|
||||
|
||||
const reSelectAll = maxFiles === 1 ? true : reSelect;
|
||||
const direction: DirectionOptions = dir === "rtl" ? "rtl" : "ltr";
|
||||
|
||||
const removeFileFromSet = useCallback(
|
||||
(i: number) => {
|
||||
if (!value) return;
|
||||
const newFiles = value.filter((_, index) => index !== i);
|
||||
onValueChange(newFiles);
|
||||
},
|
||||
[value, onValueChange]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!value) return;
|
||||
|
||||
const moveNext = () => {
|
||||
const nextIndex = activeIndex + 1;
|
||||
setActiveIndex(nextIndex > value.length - 1 ? 0 : nextIndex);
|
||||
};
|
||||
|
||||
const movePrev = () => {
|
||||
const nextIndex = activeIndex - 1;
|
||||
setActiveIndex(nextIndex < 0 ? value.length - 1 : nextIndex);
|
||||
};
|
||||
|
||||
const prevKey = orientation === "horizontal" ? (direction === "ltr" ? "ArrowLeft" : "ArrowRight") : "ArrowUp";
|
||||
|
||||
const nextKey = orientation === "horizontal" ? (direction === "ltr" ? "ArrowRight" : "ArrowLeft") : "ArrowDown";
|
||||
|
||||
if (e.key === nextKey) {
|
||||
moveNext();
|
||||
} else if (e.key === prevKey) {
|
||||
movePrev();
|
||||
} else if (e.key === "Enter" || e.key === "Space") {
|
||||
if (activeIndex === -1) {
|
||||
dropzoneState.inputRef.current?.click();
|
||||
}
|
||||
} else if (e.key === "Delete" || e.key === "Backspace") {
|
||||
if (activeIndex !== -1) {
|
||||
removeFileFromSet(activeIndex);
|
||||
if (value.length - 1 === 0) {
|
||||
setActiveIndex(-1);
|
||||
return;
|
||||
}
|
||||
movePrev();
|
||||
}
|
||||
} else if (e.key === "Escape") {
|
||||
setActiveIndex(-1);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[value, activeIndex, removeFileFromSet]
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
||||
const files = acceptedFiles;
|
||||
|
||||
if (!files) {
|
||||
toast.error("file error , probably too big");
|
||||
return;
|
||||
}
|
||||
|
||||
const newValues: File[] = value ? [...value] : [];
|
||||
|
||||
if (reSelectAll) {
|
||||
newValues.splice(0, newValues.length);
|
||||
}
|
||||
|
||||
files.forEach((file) => {
|
||||
if (newValues.length < maxFiles) {
|
||||
newValues.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
onValueChange(newValues);
|
||||
|
||||
if (rejectedFiles.length > 0) {
|
||||
for (let i = 0; i < rejectedFiles.length; i++) {
|
||||
if (rejectedFiles[i].errors[0]?.code === "file-too-large") {
|
||||
toast.error(`File is too large. Max size is ${maxSize / 1024 / 1024}MB`);
|
||||
break;
|
||||
}
|
||||
if (rejectedFiles[i].errors[0]?.message) {
|
||||
toast.error(rejectedFiles[i].errors[0].message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[reSelectAll, value]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) return;
|
||||
if (value.length === maxFiles) {
|
||||
setIsLOF(true);
|
||||
return;
|
||||
}
|
||||
setIsLOF(false);
|
||||
}, [value, maxFiles]);
|
||||
|
||||
const opts = dropzoneOptions ? dropzoneOptions : { accept, maxFiles, maxSize, multiple };
|
||||
|
||||
const dropzoneState = useDropzone({
|
||||
...opts,
|
||||
onDrop,
|
||||
onDropRejected: () => setIsFileTooBig(true),
|
||||
onDropAccepted: () => setIsFileTooBig(false),
|
||||
});
|
||||
|
||||
return (
|
||||
<FileUploaderContext.Provider
|
||||
value={{
|
||||
dropzoneState,
|
||||
isLOF,
|
||||
isFileTooBig,
|
||||
removeFileFromSet,
|
||||
activeIndex,
|
||||
setActiveIndex,
|
||||
orientation,
|
||||
direction,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
tabIndex={0}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("grid w-full focus:outline-none", className, {
|
||||
"gap-2": value && value.length > 0,
|
||||
})}
|
||||
dir={dir}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</FileUploaderContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FileUploader.displayName = "FileUploader";
|
||||
|
||||
export const FileUploaderContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ children, className, ...props }, ref) => {
|
||||
const { orientation } = useFileUpload();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} aria-description="content file holder">
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid gap-4",
|
||||
orientation === "horizontal" ? "grid-rows-1 sm:grid-rows-2 lg:grid-rows-3" : "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
FileUploaderContent.displayName = "FileUploaderContent";
|
||||
|
||||
export const FileUploaderItem = forwardRef<HTMLDivElement, { index: number } & React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, index, children, ...props }, ref) => {
|
||||
const { removeFileFromSet, activeIndex, direction } = useFileUpload();
|
||||
const isSelected = index === activeIndex;
|
||||
return (
|
||||
<div ref={ref} className={cn("p-1 justify-between cursor-pointer relative", className, isSelected ? "bg-muted" : "")} {...props}>
|
||||
<div className="font-medium leading-none tracking-tight flex items-center gap-1.5 h-full w-full">{children}</div>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("absolute", direction === "rtl" ? "top-1 left-1" : "top-1 right-1")}
|
||||
onClick={() => removeFileFromSet(index)}
|
||||
>
|
||||
<span className="sr-only">remove item {index}</span>
|
||||
<RemoveIcon className="w-4 h-4 hover:stroke-destructive duration-200 ease-in-out" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FileUploaderItem.displayName = "FileUploaderItem";
|
||||
|
||||
export const FileInput = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, children, ...props }, ref) => {
|
||||
const { dropzoneState, isFileTooBig, isLOF } = useFileUpload();
|
||||
const rootProps = isLOF ? {} : dropzoneState.getRootProps();
|
||||
return (
|
||||
<div ref={ref} {...props} className={`relative w-full ${isLOF ? "opacity-50 cursor-not-allowed " : "cursor-pointer "}`}>
|
||||
<div
|
||||
className={cn(
|
||||
`w-full rounded-lg duration-300 ease-in-out
|
||||
${dropzoneState.isDragAccept ? "border-green-500" : dropzoneState.isDragReject || isFileTooBig ? "border-red-500" : "border-gray-300"}`,
|
||||
className
|
||||
)}
|
||||
{...rootProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<Input ref={dropzoneState.inputRef} disabled={isLOF} {...dropzoneState.getInputProps()} className={`${isLOF ? "cursor-not-allowed" : ""}`} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
FileInput.displayName = "FileInput";
|
||||
@@ -1,16 +1,12 @@
|
||||
import React from "react";
|
||||
import {cn} from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface ISVGProps extends React.SVGProps<SVGSVGElement> {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const LoadingSpinner = ({
|
||||
size = 24,
|
||||
className,
|
||||
...props
|
||||
}: ISVGProps) => {
|
||||
export const LoadingSpinner = ({ size = 24, className, ...props }: ISVGProps) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -28,4 +24,4 @@ export const LoadingSpinner = ({
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+37
-119
@@ -2,63 +2,37 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import {
|
||||
CheckIcon,
|
||||
XCircle,
|
||||
ChevronDown,
|
||||
XIcon,
|
||||
WandSparkles,
|
||||
} from "lucide-react";
|
||||
import { CheckIcon, XCircle, ChevronDown, XIcon, WandSparkles } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command";
|
||||
|
||||
/**
|
||||
* Variants for the multi-select component to handle different styles.
|
||||
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
|
||||
*/
|
||||
const multiSelectVariants = cva(
|
||||
"m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-foreground/10 text-foreground bg-card hover:bg-card/80",
|
||||
secondary:
|
||||
"border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
inverted: "inverted",
|
||||
},
|
||||
const multiSelectVariants = cva("m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
|
||||
secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
inverted: "inverted",
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Props for MultiSelect component
|
||||
*/
|
||||
interface MultiSelectProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof multiSelectVariants> {
|
||||
interface MultiSelectProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof multiSelectVariants> {
|
||||
/**
|
||||
* An array of option objects to be displayed in the multi-select component.
|
||||
* Each option object has a label, value, and an optional icon.
|
||||
@@ -119,10 +93,7 @@ interface MultiSelectProps
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MultiSelect = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
MultiSelectProps
|
||||
>(
|
||||
export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
(
|
||||
{
|
||||
options,
|
||||
@@ -139,14 +110,11 @@ export const MultiSelect = React.forwardRef<
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [selectedValues, setSelectedValues] =
|
||||
React.useState<string[]>(defaultValue);
|
||||
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
||||
const [isAnimating, setIsAnimating] = React.useState(false);
|
||||
|
||||
const handleInputKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
setIsPopoverOpen(true);
|
||||
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
||||
@@ -158,9 +126,7 @@ export const MultiSelect = React.forwardRef<
|
||||
};
|
||||
|
||||
const toggleOption = (option: string) => {
|
||||
const newSelectedValues = selectedValues.includes(option)
|
||||
? selectedValues.filter((value) => value !== option)
|
||||
: [...selectedValues, option];
|
||||
const newSelectedValues = selectedValues.includes(option) ? selectedValues.filter((value) => value !== option) : [...selectedValues, option];
|
||||
setSelectedValues(newSelectedValues);
|
||||
onValueChange(newSelectedValues);
|
||||
};
|
||||
@@ -191,14 +157,9 @@ export const MultiSelect = React.forwardRef<
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
modal={modalPopover}
|
||||
>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={handleTogglePopover}
|
||||
@@ -216,15 +177,10 @@ export const MultiSelect = React.forwardRef<
|
||||
return (
|
||||
<Badge
|
||||
key={value}
|
||||
className={cn(
|
||||
isAnimating ? "animate-bounce" : "",
|
||||
multiSelectVariants({ variant })
|
||||
)}
|
||||
className={cn(isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }))}
|
||||
style={{ animationDuration: `${animation}s` }}
|
||||
>
|
||||
{IconComponent && (
|
||||
<IconComponent className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{IconComponent && <IconComponent className="h-4 w-4 mr-2" />}
|
||||
{option?.label}
|
||||
<XCircle
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
@@ -264,47 +220,29 @@ export const MultiSelect = React.forwardRef<
|
||||
handleClear();
|
||||
}}
|
||||
/>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="flex min-h-6 h-full"
|
||||
/>
|
||||
<Separator orientation="vertical" className="flex min-h-6 h-full" />
|
||||
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between w-full mx-auto">
|
||||
<span className="text-sm text-muted-foreground mx-3">
|
||||
{placeholder}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground mx-3">{placeholder}</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align="start"
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search..."
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
key="all"
|
||||
onSelect={toggleAll}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<CommandItem key="all" onSelect={toggleAll} className="cursor-pointer">
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||
selectedValues.length === options.length
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible"
|
||||
selectedValues.length === options.length ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible"
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
@@ -314,24 +252,16 @@ export const MultiSelect = React.forwardRef<
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedValues.includes(option.value);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => toggleOption(option.value)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className="cursor-pointer">
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible"
|
||||
isSelected ? "bg-primary text-primary-foreground" : "opacity-50 [&_svg]:invisible"
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
{option.icon && <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
|
||||
<span>{option.label}</span>
|
||||
</CommandItem>
|
||||
);
|
||||
@@ -342,22 +272,13 @@ export const MultiSelect = React.forwardRef<
|
||||
<div className="flex items-center justify-between">
|
||||
{selectedValues.length > 0 && (
|
||||
<>
|
||||
<CommandItem
|
||||
onSelect={handleClear}
|
||||
className="flex-1 justify-center cursor-pointer"
|
||||
>
|
||||
<CommandItem onSelect={handleClear} className="flex-1 justify-center cursor-pointer">
|
||||
Clear
|
||||
</CommandItem>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="flex min-h-6 h-full"
|
||||
/>
|
||||
<Separator orientation="vertical" className="flex min-h-6 h-full" />
|
||||
</>
|
||||
)}
|
||||
<CommandItem
|
||||
onSelect={() => setIsPopoverOpen(false)}
|
||||
className="flex-1 justify-center cursor-pointer max-w-full"
|
||||
>
|
||||
<CommandItem onSelect={() => setIsPopoverOpen(false)} className="flex-1 justify-center cursor-pointer max-w-full">
|
||||
Close
|
||||
</CommandItem>
|
||||
</div>
|
||||
@@ -367,10 +288,7 @@ export const MultiSelect = React.forwardRef<
|
||||
</PopoverContent>
|
||||
{animation > 0 && selectedValues.length > 0 && (
|
||||
<WandSparkles
|
||||
className={cn(
|
||||
"cursor-pointer my-2 text-foreground bg-background w-3 h-3",
|
||||
isAnimating ? "" : "text-muted-foreground"
|
||||
)}
|
||||
className={cn("cursor-pointer my-2 text-foreground bg-background w-3 h-3", isAnimating ? "" : "text-muted-foreground")}
|
||||
onClick={() => setIsAnimating(!isAnimating)}
|
||||
/>
|
||||
)}
|
||||
@@ -379,4 +297,4 @@ export const MultiSelect = React.forwardRef<
|
||||
}
|
||||
);
|
||||
|
||||
MultiSelect.displayName = "MultiSelect";
|
||||
MultiSelect.displayName = "MultiSelect";
|
||||
@@ -1,95 +1,80 @@
|
||||
import {PaginationEllipsis, PaginationItem, PaginationLink} from "@/components/ui/pagination";
|
||||
|
||||
import { PaginationEllipsis, PaginationItem, PaginationLink } from "@/components/ui/pagination";
|
||||
|
||||
export type paginationItemsProps = {
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
handlePageChange: (page: number) => void
|
||||
maxVisiblePages?: number
|
||||
}
|
||||
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
handlePageChange: (page: number) => void;
|
||||
maxVisiblePages?: number;
|
||||
};
|
||||
|
||||
export const PaginationIndexes = (props: paginationItemsProps) => {
|
||||
const { totalPages, currentPage, handlePageChange, maxVisiblePages = 3 } = props;
|
||||
|
||||
const {totalPages, currentPage, handlePageChange, maxVisiblePages = 3} = props
|
||||
|
||||
const items = []
|
||||
const items = [];
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
onClick={() => handlePageChange(i)}
|
||||
isActive={currentPage === i}
|
||||
>
|
||||
<PaginationLink onClick={() => handlePageChange(i)} isActive={currentPage === i}>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (currentPage <= 2) {
|
||||
for (let i = 1; i <= maxVisiblePages; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
onClick={() => handlePageChange(i)}
|
||||
isActive={currentPage === i}
|
||||
>
|
||||
<PaginationLink onClick={() => handlePageChange(i)} isActive={currentPage === i}>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
items.push(
|
||||
<PaginationItem key="ellipsis1">
|
||||
<PaginationEllipsis/>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)
|
||||
);
|
||||
} else if (currentPage >= totalPages - 1) {
|
||||
items.push(
|
||||
<PaginationItem key="ellipsis2">
|
||||
<PaginationEllipsis/>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)
|
||||
);
|
||||
for (let i = totalPages - 2; i <= totalPages; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
onClick={() => handlePageChange(i)}
|
||||
isActive={currentPage === i}
|
||||
>
|
||||
<PaginationLink onClick={() => handlePageChange(i)} isActive={currentPage === i}>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
items.push(
|
||||
<PaginationItem key="ellipsis3">
|
||||
<PaginationEllipsis/>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)
|
||||
);
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
items.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
onClick={() => handlePageChange(i)}
|
||||
isActive={currentPage === i}
|
||||
>
|
||||
<PaginationLink onClick={() => handlePageChange(i)} isActive={currentPage === i}>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
items.push(
|
||||
<PaginationItem key="ellipsis4">
|
||||
<PaginationEllipsis/>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -1,41 +1,31 @@
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious
|
||||
} from "@/components/ui/pagination";
|
||||
import {PaginationIndexes} from "@/components/wrappers/common/pagination/pagination-indexes";
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
|
||||
import { PaginationIndexes } from "@/components/wrappers/common/pagination/pagination-indexes";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type paginationNavigationProps = {
|
||||
className?: string
|
||||
totalPages: number
|
||||
currentPage: number
|
||||
goToPage: (page: number) => void
|
||||
goToPrevPage: () => void
|
||||
goToNextPage: () => void
|
||||
maxVisiblePages?: number
|
||||
}
|
||||
|
||||
className?: string;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
goToPage: (page: number) => void;
|
||||
goToPrevPage: () => void;
|
||||
goToNextPage: () => void;
|
||||
maxVisiblePages?: number;
|
||||
};
|
||||
|
||||
export const PaginationNavigation = (props: paginationNavigationProps) => {
|
||||
|
||||
const {className, totalPages, currentPage, goToPage, goToPrevPage, goToNextPage, maxVisiblePages = 3} = props
|
||||
const { className, totalPages, currentPage, goToPage, goToPrevPage, goToNextPage, maxVisiblePages = 3 } = props;
|
||||
|
||||
return (
|
||||
<Pagination className={cn("", className)}>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious onClick={goToPrevPage}/>
|
||||
<PaginationPrevious onClick={goToPrevPage} />
|
||||
</PaginationItem>
|
||||
<PaginationIndexes totalPages={totalPages} currentPage={currentPage} handlePageChange={goToPage}
|
||||
maxVisiblePages={maxVisiblePages}/>
|
||||
<PaginationIndexes totalPages={totalPages} currentPage={currentPage} handlePageChange={goToPage} maxVisiblePages={maxVisiblePages} />
|
||||
<PaginationItem>
|
||||
<PaginationNext onClick={goToNextPage}/>
|
||||
<PaginationNext onClick={goToNextPage} />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import {useState} from "react";
|
||||
|
||||
import {
|
||||
ColumnDef, getCoreRowModel, getPaginationRowModel, getSortedRowModel, SortingState, useReactTable
|
||||
} from "@tanstack/react-table"
|
||||
import {DataTable as BaseDataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {TablePagination} from "@/components/wrappers/common/table/table-pagination";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
extendedProps?: any
|
||||
DataTable?: any
|
||||
dataTableProps?: any
|
||||
}
|
||||
|
||||
export function DataTableWithPagination<TData, TValue>(props: DataTableProps<TData, TValue>) {
|
||||
|
||||
const {columns, data, extendedProps, DataTable = BaseDataTable, dataTableProps} = props;
|
||||
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
|
||||
const table = useReactTable({
|
||||
data: data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
meta: {
|
||||
extendedProps: extendedProps,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<DataTable table={table} {...dataTableProps}/>
|
||||
<TablePagination table={table} pageSizeOptions={[5, 10, 20, 50, 100]}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,59 +1,154 @@
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
|
||||
import {flexRender, Row, RowData} from "@tanstack/react-table";
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
getFilteredRowModel,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
export type dataTableProps = {
|
||||
table: any,
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
import { useState } from "react";
|
||||
import { TablePagination } from "./table-pagination";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
enableFilter?: boolean;
|
||||
enableSelect?: boolean;
|
||||
enablePagination?: boolean;
|
||||
paginationOptions?: {
|
||||
pageSize: number[];
|
||||
pageVisible: number;
|
||||
className?: string;
|
||||
};
|
||||
filterOptions?: {
|
||||
title?: string;
|
||||
key: string;
|
||||
};
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
export const DataTable = ({table}: dataTableProps) => {
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
enableFilter = false,
|
||||
enablePagination = true,
|
||||
enableSelect = true,
|
||||
paginationOptions = { pageSize: [10, 20, 30, 40, 50, 100], pageVisible: 3 },
|
||||
filterOptions = { key: "id", title: "Filter by ID" },
|
||||
emptyText = "No data.",
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
|
||||
if (enableSelect && data.length > 0) {
|
||||
const selectColumnExists = columns.some((column) => column.id === "select");
|
||||
|
||||
if (!selectColumnExists) {
|
||||
columns.unshift({
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => <Checkbox checked={row.getIsSelected()} onCheckedChange={(value) => row.toggleSelected(!!value)} aria-label="Select row" />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
rowSelection,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-md border w-full ">
|
||||
<Table className="w-full">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{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: Row<RowData>) => (
|
||||
<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>
|
||||
))}
|
||||
<div>
|
||||
{enableFilter && (
|
||||
<div className="flex items-center py-4">
|
||||
<Input
|
||||
placeholder={`${filterOptions.title ?? `Filter by ${filterOptions.key}`}`}
|
||||
value={(table.getColumn(filterOptions.key)?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) => table.getColumn(filterOptions.key)?.setFilterValue(event.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-md border w-full">
|
||||
<Table className="w-full">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={table.getAllColumns().length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
))}
|
||||
</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">
|
||||
{emptyText}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4 mt-6">
|
||||
{enableSelect && (
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
)}
|
||||
{enablePagination && (
|
||||
<TablePagination
|
||||
table={table}
|
||||
maxVisiblePages={paginationOptions?.pageVisible}
|
||||
pageSizeOptions={paginationOptions.pageSize}
|
||||
className={paginationOptions.className}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import {PaginationNavigation} from "@/components/wrappers/common/pagination/pagination-navigation";
|
||||
|
||||
import { PaginationNavigation } from "@/components/wrappers/common/pagination/pagination-navigation";
|
||||
|
||||
export type paginationNavigationProps = {
|
||||
className?: string
|
||||
table: any
|
||||
maxVisiblePages?: number
|
||||
}
|
||||
|
||||
className?: string;
|
||||
table: any;
|
||||
maxVisiblePages?: number;
|
||||
};
|
||||
|
||||
export const TablePaginationNavigation = (props: paginationNavigationProps) => {
|
||||
const { className, table, maxVisiblePages = 3 } = props;
|
||||
|
||||
const {className, table, maxVisiblePages = 3} = props
|
||||
|
||||
|
||||
const totalPages = table.getPageCount()
|
||||
const currentPage = table.getState().pagination.pageIndex + 1
|
||||
const totalPages = table.getPageCount();
|
||||
const currentPage = table.getState().pagination.pageIndex + 1;
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
table.setPageIndex(page - 1)
|
||||
}
|
||||
table.setPageIndex(page - 1);
|
||||
};
|
||||
|
||||
const goToPrevPage = () => {
|
||||
if (table.getCanPreviousPage()) table.previousPage()
|
||||
}
|
||||
if (table.getCanPreviousPage()) table.previousPage();
|
||||
};
|
||||
|
||||
const goToNextPage = () => {
|
||||
if (table.getCanNextPage()) table.nextPage()
|
||||
}
|
||||
if (table.getCanNextPage()) table.nextPage();
|
||||
};
|
||||
|
||||
return (
|
||||
<PaginationNavigation
|
||||
@@ -36,6 +32,7 @@ export const TablePaginationNavigation = (props: paginationNavigationProps) => {
|
||||
goToPage={goToPage}
|
||||
goToPrevPage={goToPrevPage}
|
||||
goToNextPage={goToNextPage}
|
||||
maxVisiblePages={maxVisiblePages}/>
|
||||
)
|
||||
}
|
||||
maxVisiblePages={maxVisiblePages}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {useEffect} from "react";
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||
import {cn} from "@/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type tablePaginationSizeProps = {
|
||||
className?: string
|
||||
table: any
|
||||
pageSizeOptions?: number[]
|
||||
}
|
||||
className?: string;
|
||||
table: any;
|
||||
pageSizeOptions?: number[];
|
||||
};
|
||||
|
||||
export const TablePaginationSize = (props: tablePaginationSizeProps) => {
|
||||
const { className, table, pageSizeOptions = [10, 20, 30, 40, 50] } = props;
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import {TablePaginationNavigation} from "@/components/wrappers/common/table/table-pagination-navigation";
|
||||
import {TablePaginationSize} from "@/components/wrappers/common/table/table-pagination-size";
|
||||
import {cn} from "@/lib/utils";
|
||||
import { TablePaginationNavigation } from "@/components/wrappers/common/table/table-pagination-navigation";
|
||||
import { TablePaginationSize } from "@/components/wrappers/common/table/table-pagination-size";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface tablePaginationProps {
|
||||
className?: string
|
||||
table: any
|
||||
maxVisiblePages?: number
|
||||
pageSizeOptions?: number[]
|
||||
className?: string;
|
||||
table: any;
|
||||
maxVisiblePages?: number;
|
||||
pageSizeOptions?: number[];
|
||||
}
|
||||
|
||||
export function TablePagination(props: tablePaginationProps) {
|
||||
|
||||
const {className, table, maxVisiblePages = 3, pageSizeOptions = [10, 20, 30, 40, 50]} = props
|
||||
const { className, table, maxVisiblePages = 3, pageSizeOptions = [10, 20, 30, 40, 50] } = props;
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col sm:flex-row mt-6", className)}>
|
||||
<div className={cn("flex flex-col gap-x-4 sm:flex-row", className)}>
|
||||
<TablePaginationSize table={table} pageSizeOptions={pageSizeOptions} />
|
||||
<TablePaginationNavigation table={table} maxVisiblePages={maxVisiblePages} className="justify-end mt-5 sm:mt-0" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Column } from "@tanstack/react-table";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
|
||||
interface tableSortButtonProps<TData, TValue> {
|
||||
title?: string;
|
||||
defaultOrder: "asc" | "desc";
|
||||
column: Column<TData, TValue>;
|
||||
}
|
||||
|
||||
export default function TableSortButton<TData, TValue>(props: tableSortButtonProps<TData, TValue>) {
|
||||
return (
|
||||
<Button variant="ghost" onClick={() => props.column.toggleSorting(props.column.getIsSorted() === "asc")}>
|
||||
{props.title ?? "Please define a title"}
|
||||
{props.column.getIsSorted() === props.defaultOrder ? <ArrowUp className="ml-2 h-4 w-4" /> : <ArrowDown className="ml-2 h-4 w-4" />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,26 @@
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import {PropsWithChildren} from "react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export type TooltipCustomProps = PropsWithChildren<{
|
||||
text: string
|
||||
disabled?: any
|
||||
text: string;
|
||||
disabled?: any;
|
||||
}>;
|
||||
|
||||
export function TooltipCustom(props: TooltipCustomProps) {
|
||||
return (
|
||||
<>
|
||||
{props.disabled ?
|
||||
{props.disabled ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="w-full">
|
||||
{props.children}
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger className="w-full">{props.children}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{props.text}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
:
|
||||
) : (
|
||||
props.children
|
||||
}
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user