Moving files and rename some of them.

This commit is contained in:
charles-gauthereau
2024-12-16 16:30:00 +01:00
parent c97d68d697
commit 31ee81c10f
75 changed files with 64 additions and 175 deletions
@@ -0,0 +1,61 @@
'use client'
import React, {useState} from 'react'
import {cn} from "@/lib/utils";
import {PaginationNavigation} from "@/components/wrappers/common/pagination/pagination-navigation";
export type cardsWithPaginationProps = {
className?: string
data: Array<{}>;
cardItem: React.ComponentType;
cardsPerPage?: number
numberOfColumns?: number
maxVisiblePages?: number
extendedProps?: any
}
export const CardsWithPagination = (props: cardsWithPaginationProps) => {
const {className, data, cardItem, cardsPerPage = 5, numberOfColumns = 1, maxVisiblePages = 3} = props
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 goToPage = (pageNumber: number) => {
setCurrentPage(pageNumber)
}
const goToPrevPage = () => {
goToPage(Math.max(1, currentPage - 1))
}
const goToNextPage = () => {
goToPage(Math.min(totalPages, currentPage + 1))
}
return (
<div className={cn("flex flex-col h-full justify-between", className)}>
<div className={cn(`grid h-max auto-rows-min gap-4 md:grid-cols-${numberOfColumns}`)}>
{currentCards.map((card, key) => (
<CardItem key={key} data={card} extendedProps={props.extendedProps} />
))}
</div>
<PaginationNavigation
className="justify-end"
totalPages={totalPages}
currentPage={currentPage}
goToPage={goToPage}
goToPrevPage={goToPrevPage}
goToNextPage={goToNextPage}
maxVisiblePages={maxVisiblePages}/>
</div>
)
}
+2 -3
View File
@@ -2,7 +2,7 @@
import {useEffect, useState} from "react";
import {Check, ChevronDown, KeyRound} from "lucide-react"
import {Check, ChevronDown} from "lucide-react"
import {cn} from "@/lib/utils"
import {Button} from "@/components/ui/button"
@@ -22,8 +22,7 @@ import {
import {FormControl} from "@/components/ui/form";
import {SidebarMenuButton} from "@/components/ui/sidebar";
import {Separator} from "@/components/ui/separator";
import {AgentModalKey} from "@/components/wrappers/dashboard/agent/AgentModalKey/AgentModalKey";
import {CreateOrganizationModal} from "@/components/wrappers/dashboard/Organization/create-organisation-modal";
import {CreateOrganizationModal} from "@/components/wrappers/dashboard/organization/create-organisation-modal";
export type comboBoxProps = {
values: Array<{ value: string, label: string }>
@@ -0,0 +1,23 @@
import {cn} from "@/lib/utils";
export type connectionCircleProps = {
date: Date
}
export const ConnectionCircle = ({date}: connectionCircleProps) => {
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"
}
return (
<div color="red" className={cn("w-5 h-5 rounded-3xl border-4", style)}/>
)
}
@@ -0,0 +1,770 @@
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};
+44
View File
@@ -0,0 +1,44 @@
"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,
}
export const AgentRegistrationDialog = (props: agentRegistrationDialogProps) => {
const {open, setOpen} = props;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>New agent registered!</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
EDGE KEY
</Label>
<Input id="name" value="Pedro Duarte" readOnly className="col-span-3"/>
</div>
</div>
<DialogFooter>
<Button type="submit">Close</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,31 @@
import React from "react";
import {cn} from "@/lib/utils";
export interface ISVGProps extends React.SVGProps<SVGSVGElement> {
size?: number;
className?: string;
}
export const LoadingSpinner = ({
size = 24,
className,
...props
}: ISVGProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
{...props}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={cn("animate-spin", className)}
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
};
@@ -0,0 +1,382 @@
//https://github.com/sersavan/shadcn-multi-select-component
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
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";
/**
* 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",
},
},
defaultVariants: {
variant: "default",
},
}
);
/**
* Props for MultiSelect component
*/
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.
*/
options: {
/** The text to display for the option. */
label: string;
/** The unique value associated with the option. */
value: string;
/** Optional icon component to display alongside the option. */
icon?: React.ComponentType<{ className?: string }>;
}[];
/**
* Callback function triggered when the selected values change.
* Receives an array of the new selected values.
*/
onValueChange: (value: string[]) => void;
/** The default selected values when the component mounts. */
defaultValue?: string[];
/**
* Placeholder text to be displayed when no values are selected.
* Optional, defaults to "Select options".
*/
placeholder?: string;
/**
* Animation duration in seconds for the visual effects (e.g., bouncing badges).
* Optional, defaults to 0 (no animation).
*/
animation?: number;
/**
* Maximum number of items to display. Extra selected items will be summarized.
* Optional, defaults to 3.
*/
maxCount?: number;
/**
* The modality of the popover. When set to true, interaction with outside elements
* will be disabled and only popover content will be visible to screen readers.
* Optional, defaults to false.
*/
modalPopover?: boolean;
/**
* If true, renders the multi-select component as a child of another component.
* Optional, defaults to false.
*/
asChild?: boolean;
/**
* Additional class names to apply custom styles to the multi-select component.
* Optional, can be used to add custom styles.
*/
className?: string;
}
export const MultiSelect = React.forwardRef<
HTMLButtonElement,
MultiSelectProps
>(
(
{
options,
onValueChange,
variant,
defaultValue = [],
placeholder = "Select options",
animation = 0,
maxCount = 3,
modalPopover = false,
asChild = false,
className,
...props
},
ref
) => {
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>
) => {
if (event.key === "Enter") {
setIsPopoverOpen(true);
} else if (event.key === "Backspace" && !event.currentTarget.value) {
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
}
};
const toggleOption = (option: string) => {
const newSelectedValues = selectedValues.includes(option)
? selectedValues.filter((value) => value !== option)
: [...selectedValues, option];
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
};
const handleClear = () => {
setSelectedValues([]);
onValueChange([]);
};
const handleTogglePopover = () => {
setIsPopoverOpen((prev) => !prev);
};
const clearExtraOptions = () => {
const newSelectedValues = selectedValues.slice(0, maxCount);
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
};
const toggleAll = () => {
if (selectedValues.length === options.length) {
handleClear();
} else {
const allValues = options.map((option) => option.value);
setSelectedValues(allValues);
onValueChange(allValues);
}
};
return (
<Popover
open={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
modal={modalPopover}
>
<PopoverTrigger asChild>
<Button
ref={ref}
{...props}
onClick={handleTogglePopover}
className={cn(
"flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto",
className
)}
>
{selectedValues.length > 0 ? (
<div className="flex justify-between items-center w-full">
<div className="flex flex-wrap items-center">
{selectedValues.slice(0, maxCount).map((value) => {
const option = options.find((o) => o.value === value);
const IconComponent = option?.icon;
return (
<Badge
key={value}
className={cn(
isAnimating ? "animate-bounce" : "",
multiSelectVariants({ variant })
)}
style={{ animationDuration: `${animation}s` }}
>
{IconComponent && (
<IconComponent className="h-4 w-4 mr-2" />
)}
{option?.label}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
toggleOption(value);
}}
/>
</Badge>
);
})}
{selectedValues.length > maxCount && (
<Badge
className={cn(
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
isAnimating ? "animate-bounce" : "",
multiSelectVariants({ variant })
)}
style={{ animationDuration: `${animation}s` }}
>
{`+ ${selectedValues.length - maxCount} more`}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
clearExtraOptions();
}}
/>
</Badge>
)}
</div>
<div className="flex items-center justify-between">
<XIcon
className="h-4 mx-2 cursor-pointer text-muted-foreground"
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
/>
<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>
<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)}
>
<Command>
<CommandInput
placeholder="Search..."
onKeyDown={handleInputKeyDown}
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<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"
)}
>
<CheckIcon className="h-4 w-4" />
</div>
<span>(Select All)</span>
</CommandItem>
{options.map((option) => {
const isSelected = selectedValues.includes(option.value);
return (
<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"
)}
>
<CheckIcon className="h-4 w-4" />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{option.label}</span>
</CommandItem>
);
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
{selectedValues.length > 0 && (
<>
<CommandItem
onSelect={handleClear}
className="flex-1 justify-center cursor-pointer"
>
Clear
</CommandItem>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
</>
)}
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="flex-1 justify-center cursor-pointer max-w-full"
>
Close
</CommandItem>
</div>
</CommandGroup>
</CommandList>
</Command>
</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"
)}
onClick={() => setIsAnimating(!isAnimating)}
/>
)}
</Popover>
);
}
);
MultiSelect.displayName = "MultiSelect";
@@ -0,0 +1,95 @@
import {PaginationEllipsis, PaginationItem, PaginationLink} from "@/components/ui/pagination";
export type paginationItemsProps = {
totalPages: number
currentPage: number
handlePageChange: (page: number) => void
maxVisiblePages?: number
}
export const PaginationIndexes = (props: paginationItemsProps) => {
const {totalPages, currentPage, handlePageChange, maxVisiblePages = 3} = props
const items = []
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
items.push(
<PaginationItem key={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}
>
{i}
</PaginationLink>
</PaginationItem>
)
}
items.push(
<PaginationItem key="ellipsis1">
<PaginationEllipsis/>
</PaginationItem>
)
} else if (currentPage >= totalPages - 1) {
items.push(
<PaginationItem key="ellipsis2">
<PaginationEllipsis/>
</PaginationItem>
)
for (let i = totalPages - 2; i <= totalPages; i++) {
items.push(
<PaginationItem key={i}>
<PaginationLink
onClick={() => handlePageChange(i)}
isActive={currentPage === i}
>
{i}
</PaginationLink>
</PaginationItem>
)
}
} else {
items.push(
<PaginationItem key="ellipsis3">
<PaginationEllipsis/>
</PaginationItem>
)
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
items.push(
<PaginationItem key={i}>
<PaginationLink
onClick={() => handlePageChange(i)}
isActive={currentPage === i}
>
{i}
</PaginationLink>
</PaginationItem>
)
}
items.push(
<PaginationItem key="ellipsis4">
<PaginationEllipsis/>
</PaginationItem>
)
}
}
return items
}
@@ -0,0 +1,41 @@
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
}
export const PaginationNavigation = (props: paginationNavigationProps) => {
const {className, totalPages, currentPage, goToPage, goToPrevPage, goToNextPage, maxVisiblePages = 3} = props
return (
<Pagination className={cn("", className)}>
<PaginationContent>
<PaginationItem>
<PaginationPrevious onClick={goToPrevPage}/>
</PaginationItem>
<PaginationIndexes totalPages={totalPages} currentPage={currentPage} handlePageChange={goToPage}
maxVisiblePages={maxVisiblePages}/>
<PaginationItem>
<PaginationNext onClick={goToNextPage}/>
</PaginationItem>
</PaginationContent>
</Pagination>
)
}
@@ -0,0 +1,33 @@
import {Badge} from "@/components/ui/badge";
import {cn} from "@/lib/utils";
export type statusBadgeProps = {
status: "pending" | "ongoing" | "success" | "failed"
}
export const StatusBadge = ({status}: statusBadgeProps) => {
let style = "";
switch (status) {
case 'pending':
style = "text-yellow-500 border-yellow-500"
break
case 'ongoing':
style = "text-orange-500 border-orange-500"
break
case 'failed':
style = "text-red-500 border-red-500"
break
case 'success':
style = "text-green-500 border-green-500"
break
default:
style = "text-yellow-500 border-yellow-500"
}
return (
<Badge variant="outline" className={cn("w-20 border-2 justify-center")}>{status}</Badge>
)
}
@@ -0,0 +1,47 @@
"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>
)
}
@@ -0,0 +1,59 @@
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
import {flexRender, Row, RowData} from "@tanstack/react-table";
export type dataTableProps = {
table: any,
}
export const DataTable = ({table}: dataTableProps) => {
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>
))}
</TableRow>
))) : (
<TableRow>
<TableCell colSpan={table.getAllColumns().length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}
@@ -0,0 +1,41 @@
import {PaginationNavigation} from "@/components/wrappers/common/pagination/pagination-navigation";
export type paginationNavigationProps = {
className?: string
table: any
maxVisiblePages?: number
}
export const TablePaginationNavigation = (props: paginationNavigationProps) => {
const {className, table, maxVisiblePages = 3} = props
const totalPages = table.getPageCount()
const currentPage = table.getState().pagination.pageIndex + 1
const goToPage = (page: number) => {
table.setPageIndex(page - 1)
}
const goToPrevPage = () => {
if (table.getCanPreviousPage()) table.previousPage()
}
const goToNextPage = () => {
if (table.getCanNextPage()) table.nextPage()
}
return (
<PaginationNavigation
className={className}
totalPages={totalPages}
currentPage={currentPage}
goToPage={goToPage}
goToPrevPage={goToPrevPage}
goToNextPage={goToNextPage}
maxVisiblePages={maxVisiblePages}/>
)
}
@@ -0,0 +1,40 @@
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[]
}
export const TablePaginationSize = (props: tablePaginationSizeProps) => {
const { className, table, pageSizeOptions = [10, 20, 30, 40, 50] } = props;
useEffect(() => {
table.setPageSize(Number(pageSizeOptions[0]));
}, []);
return (
<div className={cn("flex items-center justify-end sm:justify-center space-x-2", className)}>
<p className="whitespace-nowrap text-sm font-medium">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[4.5rem]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{pageSizeOptions.map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
@@ -0,0 +1,24 @@
"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";
interface tablePaginationProps {
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
return (
<div className={cn("flex flex-col sm:flex-row mt-6", className)}>
<TablePaginationSize table={table} pageSizeOptions={pageSizeOptions} />
<TablePaginationNavigation table={table} maxVisiblePages={maxVisiblePages} className="justify-end mt-5 sm:mt-0" />
</div>
);
}
@@ -0,0 +1,34 @@
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import {PropsWithChildren} from "react";
export type TooltipCustomProps = PropsWithChildren<{
text: string
disabled?: any
}>;
export function TooltipCustom(props: TooltipCustomProps) {
return (
<>
{props.disabled ?
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="w-full">
{props.children}
</TooltipTrigger>
<TooltipContent>
<p>{props.text}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
:
props.children
}
</>
)
}