* esm * wip * wip * wip * wip * wip * wip * subscription notice * wip * wip * wip * fix envs * fix: update docker build * fix * esm/types * delete dashboard :D * add patches to dockerfiles * update packages + catalogs + ts * wip * remove native libs * ts * improvements * fix redirects and fetching session * try fix favicon * fixes * fix * order and resize reportds within a dashboard * improvements * wip * added userjot to dashboard * fix * add op * wip * different cache key * improve date picker * fix table * event details loading * redo onboarding completely * fix login * fix * fix * extend session, billing and improve bars * fix * reduce price on 10M
189 lines
4.9 KiB
TypeScript
189 lines
4.9 KiB
TypeScript
import { useThrottle } from '@/hooks/use-throttle';
|
|
import { cn } from '@/utils/cn';
|
|
import { ChevronsUpDownIcon, Icon, type LucideIcon } from 'lucide-react';
|
|
import { last } from 'ramda';
|
|
import { Children, useEffect, useRef, useState } from 'react';
|
|
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '../ui/dropdown-menu';
|
|
import type { WidgetHeadProps, WidgetTitleProps } from '../widget';
|
|
import { WidgetHead as WidgetHeadBase } from '../widget';
|
|
|
|
export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
|
return (
|
|
<WidgetHeadBase
|
|
className={cn(
|
|
'relative flex flex-col rounded-t-xl p-0 [&_.title]:flex [&_.title]:items-center [&_.title]:p-4 [&_.title]:font-semibold',
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function WidgetTitle({
|
|
children,
|
|
className,
|
|
icon: Icon,
|
|
...props
|
|
}: WidgetTitleProps & {
|
|
icon?: LucideIcon;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn('title text-left row justify-start', className)}
|
|
{...props}
|
|
>
|
|
{Icon && (
|
|
<div className="rounded-lg bg-def-200 p-1 mr-2">
|
|
<Icon size={16} />
|
|
</div>
|
|
)}
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function WidgetAbsoluteButtons({
|
|
className,
|
|
children,
|
|
...props
|
|
}: WidgetHeadProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'row gap-1 absolute right-4 top-1/2 -translate-y-1/2',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function WidgetButtons({
|
|
className,
|
|
children,
|
|
...props
|
|
}: WidgetHeadProps) {
|
|
const container = useRef<HTMLDivElement>(null);
|
|
const sizes = useRef<number[]>([]);
|
|
const [slice, setSlice] = useState(3); // Show 3 buttons by default
|
|
const gap = 16;
|
|
|
|
const handleResize = useThrottle(() => {
|
|
if (container.current) {
|
|
if (sizes.current.length === 0) {
|
|
// Get buttons
|
|
const buttons: HTMLButtonElement[] = Array.from(
|
|
container.current.querySelectorAll('button'),
|
|
);
|
|
// Get sizes and cache them
|
|
sizes.current = buttons.map(
|
|
(button) => Math.ceil(button.offsetWidth) + gap,
|
|
);
|
|
}
|
|
const containerWidth = container.current.offsetWidth;
|
|
const buttonsWidth = sizes.current.reduce((acc, size) => acc + size, 0);
|
|
const moreWidth = (last(sizes.current) ?? 0) + gap;
|
|
|
|
if (buttonsWidth > containerWidth) {
|
|
const res = sizes.current.reduce(
|
|
(acc, size, index) => {
|
|
if (acc.size + size + moreWidth > containerWidth) {
|
|
return { index: acc.index, size: acc.size + size };
|
|
}
|
|
return { index, size: acc.size + size };
|
|
},
|
|
{ index: 0, size: 0 },
|
|
);
|
|
|
|
setSlice(res.index);
|
|
} else {
|
|
setSlice(sizes.current.length - 1);
|
|
}
|
|
}
|
|
}, 30);
|
|
|
|
useEffect(() => {
|
|
handleResize();
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
}, [handleResize, children]);
|
|
|
|
const hidden = '!opacity-0 absolute pointer-events-none';
|
|
|
|
return (
|
|
<div
|
|
ref={container}
|
|
className={cn(
|
|
'-mb-px -mt-2 flex flex-wrap justify-start self-stretch px-4 transition-opacity [&_button.active]:border-b-2 [&_button.active]:border-black [&_button.active]:opacity-100 dark:[&_button.active]:border-white [&_button]:whitespace-nowrap [&_button]:py-1 [&_button]:text-sm [&_button]:opacity-50',
|
|
className,
|
|
)}
|
|
style={{ gap }}
|
|
{...props}
|
|
>
|
|
{Children.map(children, (child, index) => {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex [&_button]:leading-normal',
|
|
slice < index ? hidden : 'opacity-100',
|
|
)}
|
|
>
|
|
{child}
|
|
</div>
|
|
);
|
|
})}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
'flex select-none items-center gap-1',
|
|
sizes.current.length - 1 === slice ? hidden : 'opacity-50',
|
|
)}
|
|
>
|
|
More <ChevronsUpDownIcon size={12} />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="[&_button]:w-full">
|
|
<DropdownMenuGroup>
|
|
{Children.map(children, (child, index) => {
|
|
if (index <= slice) {
|
|
return null;
|
|
}
|
|
return <DropdownMenuItem asChild>{child}</DropdownMenuItem>;
|
|
})}
|
|
</DropdownMenuGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function WidgetFooter({
|
|
className,
|
|
children,
|
|
...props
|
|
}: WidgetHeadProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex rounded-b-md border-t bg-def-100 p-2 py-1',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|