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
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-10-16 12:27:44 +02:00
committed by GitHub
parent 436e81ecc9
commit 81a7e5d62e
741 changed files with 32695 additions and 16996 deletions

View File

@@ -0,0 +1,54 @@
import { cn } from '@/utils/cn';
import { slug } from '@/utils/slug';
import type { LucideIcon } from 'lucide-react';
import { forwardRef } from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { Switch } from '../ui/switch';
type Props = {
label: string;
description: string;
Icon: LucideIcon;
children?: React.ReactNode;
error?: string;
} & ControllerRenderProps;
export const CheckboxItem = forwardRef<HTMLButtonElement, Props>(
(
{ label, description, Icon, children, onChange, value, disabled, error },
ref,
) => {
const id = slug(label);
return (
<div>
<label
className={cn(
'flex items-center gap-4 px-4 py-6 transition-colors hover:bg-def-200',
disabled && 'cursor-not-allowed opacity-50',
)}
htmlFor={id}
>
{Icon && <div className="w-6 shrink-0">{<Icon />}</div>}
<div className="flex-1">
<div className="font-medium">{label}</div>
<div className=" text-muted-foreground">{description}</div>
{error && <div className="text-sm text-red-600">{error}</div>}
</div>
<div>
<Switch
ref={ref}
disabled={disabled}
checked={!!value}
onCheckedChange={onChange}
id={id}
/>
</div>
</label>
{children}
</div>
);
},
);
CheckboxItem.displayName = 'CheckboxItem';

View File

@@ -0,0 +1,29 @@
import { clipboard } from '@/utils/clipboard';
import { cn } from '@/utils/cn';
import { CopyIcon } from 'lucide-react';
import { Label } from '../ui/label';
type Props = {
label: React.ReactNode;
value: string;
className?: string;
};
const CopyInput = ({ label, value, className }: Props) => {
return (
<button
type="button"
className={cn('w-full text-left', className)}
onClick={() => clipboard(value)}
>
{!!label && <Label>{label}</Label>}
<div className="font-mono flex items-center justify-between rounded bg-muted p-2 px-3 ">
{value}
<CopyIcon size={16} />
</div>
</button>
);
};
export default CopyInput;

View File

@@ -0,0 +1,68 @@
import { BanIcon, InfoIcon } from 'lucide-react';
import { forwardRef } from 'react';
import { Input } from '../ui/input';
import type { InputProps } from '../ui/input';
import { Label } from '../ui/label';
import { Tooltiper } from '../ui/tooltip';
type WithLabel = {
children: React.ReactNode;
label: string;
error?: string | undefined;
info?: React.ReactNode;
className?: string;
};
type InputWithLabelProps = InputProps & Omit<WithLabel, 'children'>;
export const WithLabel = ({
children,
className,
label,
info,
error,
}: WithLabel) => {
return (
<div className={className}>
<div className="mb-2 flex items-end justify-between">
<Label
className="mb-0 flex flex-1 shrink-0 items-center gap-1 whitespace-nowrap"
htmlFor={label}
>
{label}
{info && (
<Tooltiper content={info}>
<InfoIcon size={14} />
</Tooltiper>
)}
</Label>
{error && (
<Tooltiper
asChild
content={error}
tooltipClassName="max-w-80 leading-normal"
align="end"
>
<div className="flex items-center gap-1 leading-none text-destructive">
Issues
<BanIcon size={14} />
</div>
</Tooltiper>
)}
</div>
{children}
</div>
);
};
export const InputWithLabel = forwardRef<HTMLInputElement, InputWithLabelProps>(
(props, ref) => {
return (
<WithLabel {...props}>
<Input ref={ref} id={props.label} {...props} />
</WithLabel>
);
},
);
InputWithLabel.displayName = 'InputWithLabel';

View File

@@ -0,0 +1,152 @@
// Based on Christin Alares tag input component (https://github.com/christianalares/seventy-seven)
import { Button } from '@/components/ui/button';
import { cn } from '@/utils/cn';
import { useAnimate } from 'framer-motion';
import { XIcon } from 'lucide-react';
import type { ElementRef } from 'react';
import { useEffect, useRef, useState } from 'react';
type Props = {
placeholder: string;
value: string[];
error?: string;
className?: string;
onChange: (value: string[]) => void;
renderTag?: (tag: string) => string;
id?: string;
};
const TagInput = ({
value: propValue,
onChange,
renderTag,
placeholder,
error,
id,
}: Props) => {
const value = (
Array.isArray(propValue) ? propValue : propValue ? [propValue] : []
).filter(Boolean);
const [isMarkedForDeletion, setIsMarkedForDeletion] = useState(false);
const inputRef = useRef<ElementRef<'input'>>(null);
const [inputValue, setInputValue] = useState('');
const [scope, animate] = useAnimate();
const appendTag = (tag: string) => {
onChange([...value, tag.trim()]);
};
const removeTag = (tag: string) => {
onChange(value.filter((t) => t !== tag));
inputRef.current?.focus();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
const tagAlreadyExists = value.some(
(tag) => tag.toLowerCase() === inputValue.toLowerCase(),
);
if (inputValue) {
if (tagAlreadyExists) {
animate(
`span[data-tag="${inputValue.toLowerCase()}"]`,
{
scale: [1, 1.3, 1],
},
{
duration: 0.3,
},
);
return;
}
appendTag(inputValue);
setInputValue('');
}
}
if (e.key === 'Backspace' && inputValue === '') {
if (!isMarkedForDeletion) {
setIsMarkedForDeletion(true);
return;
}
const last = value[value.length - 1];
if (last) {
removeTag(last);
}
setIsMarkedForDeletion(false);
setInputValue('');
}
};
const handleBlur = () => {
if (inputValue) {
appendTag(inputValue);
setInputValue('');
}
};
useEffect(() => {
if (inputValue.length > 0) {
setIsMarkedForDeletion(false);
}
}, [inputValue]);
return (
<div
ref={scope}
className={cn(
'inline-flex w-full flex-wrap items-center gap-2 rounded-md border border-input p-1 px-3 ring-offset-background has-[input:focus]:ring-2 has-[input:focus]:ring-ring has-[input:focus]:ring-offset-1 bg-card',
!!error && 'border-destructive',
)}
>
{value.map((tag, i) => {
const isCreating = false;
return (
<span
data-tag={tag}
key={tag}
className={cn(
'inline-flex items-center gap-2 rounded bg-def-200 px-2 py-1 ',
isMarkedForDeletion &&
i === value.length - 1 &&
'bg-destructive-foreground ring-2 ring-destructive/50 ring-offset-1',
isCreating && 'opacity-60',
)}
>
{renderTag ? renderTag(tag) : tag}
<Button
size="icon"
variant="outline"
className="h-4 w-4 rounded-full"
onClick={() => removeTag(tag)}
>
<span className="sr-only">Remove tag</span>
<XIcon name="close" className="size-3" />
</Button>
</span>
);
})}
<input
ref={inputRef}
placeholder={`${placeholder}`}
className="min-w-20 flex-1 py-1 focus-visible:outline-none bg-card"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
id={id}
/>
</div>
);
};
export default TagInput;