Files
stats/apps/start/src/components/overview/overview-widget.tsx
Carl-Gerhard Lindesvärd 81a7e5d62e feat: dashboard v2, esm, upgrades (#211)
* 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
2025-10-16 12:27:44 +02:00

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>
);
}