chore(root): migrate to biome
This commit is contained in:
@@ -21,7 +21,7 @@ export function Card({ children, hover, className }: CardProps) {
|
||||
className={cn(
|
||||
'card relative',
|
||||
hover && 'transition-all hover:-translate-y-0.5',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -51,23 +51,23 @@ export function ChartSSR({
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`gradient`}
|
||||
id={'gradient'}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0%" stopColor={'blue'} stopOpacity={0.2}></stop>
|
||||
<stop offset="50%" stopColor={'blue'} stopOpacity={0.05}></stop>
|
||||
<stop offset="100%" stopColor={'blue'} stopOpacity={0}></stop>
|
||||
<stop offset="0%" stopColor={'blue'} stopOpacity={0.2} />
|
||||
<stop offset="50%" stopColor={'blue'} stopOpacity={0.05} />
|
||||
<stop offset="100%" stopColor={'blue'} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* Gradient area */}
|
||||
{pathArea && (
|
||||
<path
|
||||
d={pathArea}
|
||||
fill={`url(#gradient)`}
|
||||
fill={'url(#gradient)'}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -39,6 +39,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
|
||||
target="_blank"
|
||||
href="https://docs.openpanel.dev"
|
||||
className="underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
|
||||
@@ -8,7 +8,7 @@ export function ColorSquare({ children, className }: ColorSquareProps) {
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-blue-600 text-sm font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { MoonIcon, SunIcon } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import * as React from 'react';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
||||
@@ -47,7 +47,7 @@ export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
header.getContext(),
|
||||
)}
|
||||
</GridCell>
|
||||
))}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function Dot({ className, size = 8, animated }: DotProps) {
|
||||
<div
|
||||
className={cn(
|
||||
'relative',
|
||||
filterCn(['bg-', 'animate-', 'group-hover/row'], className)
|
||||
filterCn(['bg-', 'animate-', 'group-hover/row'], className),
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
@@ -30,14 +30,14 @@ export function Dot({ className, size = 8, animated }: DotProps) {
|
||||
className={cn(
|
||||
'absolute !m-0 rounded-full',
|
||||
animated !== false && 'animate-ping',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute !m-0 rounded-full',
|
||||
filterCn(['animate-', 'group-hover/row'], className)
|
||||
filterCn(['animate-', 'group-hover/row'], className),
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
|
||||
@@ -51,6 +51,7 @@ export function EventListItem(props: EventListItemProps) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isMinimal) {
|
||||
pushModal('EventDetails', {
|
||||
@@ -61,7 +62,7 @@ export function EventListItem(props: EventListItemProps) {
|
||||
className={cn(
|
||||
'card hover:bg-light-background flex w-full items-center justify-between rounded-lg p-4 transition-colors',
|
||||
meta?.conversion &&
|
||||
`bg-${meta.color}-50 dark:bg-${meta.color}-900 hover:bg-${meta.color}-100 dark:hover:bg-${meta.color}-700`
|
||||
`bg-${meta.color}-50 dark:bg-${meta.color}-900 hover:bg-${meta.color}-100 dark:hover:bg-${meta.color}-700`,
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -38,6 +38,7 @@ export default function EventListener({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
counter.set(0);
|
||||
onRefresh();
|
||||
@@ -47,14 +48,14 @@ export default function EventListener({
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all'
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all'
|
||||
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
{counter.debounced === 0 ? (
|
||||
'Listening'
|
||||
|
||||
@@ -52,6 +52,7 @@ export function useColumns() {
|
||||
<div className="flex items-center gap-2">
|
||||
<TooltipComplete content="Click to edit" side="left">
|
||||
<button
|
||||
type="button"
|
||||
className="transition-transform hover:scale-105"
|
||||
onClick={() => {
|
||||
pushModal('EditEvent', {
|
||||
@@ -68,6 +69,7 @@ export function useColumns() {
|
||||
</TooltipComplete>
|
||||
<span className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
pushModal('EventDetails', {
|
||||
id: row.original.id,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { Pagination } from '@/components/pagination';
|
||||
@@ -7,6 +6,7 @@ import { TableSkeleton } from '@/components/ui/table';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import { GanttChartIcon } from 'lucide-react';
|
||||
import { column } from 'mathjs';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import type { IServiceEvent } from '@openpanel/db';
|
||||
|
||||
@@ -55,7 +55,7 @@ export const EventsTable = ({ query, ...props }: Props) => {
|
||||
className="mt-2"
|
||||
setCursor={props.setCursor}
|
||||
cursor={props.cursor}
|
||||
count={Infinity}
|
||||
count={Number.POSITIVE_INFINITY}
|
||||
take={50}
|
||||
loading={isFetching}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef } from 'react';
|
||||
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';
|
||||
@@ -17,7 +17,7 @@ type Props = {
|
||||
export const CheckboxItem = forwardRef<HTMLButtonElement, Props>(
|
||||
(
|
||||
{ label, description, Icon, children, onChange, value, disabled, error },
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const id = slug(label);
|
||||
return (
|
||||
@@ -25,7 +25,7 @@ export const CheckboxItem = forwardRef<HTMLButtonElement, Props>(
|
||||
<label
|
||||
className={cn(
|
||||
'flex items-center gap-4 px-4 py-6 transition-colors hover:bg-def-200',
|
||||
disabled && 'cursor-not-allowed opacity-50'
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
htmlFor={id}
|
||||
>
|
||||
@@ -48,7 +48,7 @@ export const CheckboxItem = forwardRef<HTMLButtonElement, Props>(
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
CheckboxItem.displayName = 'CheckboxItem';
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
const CopyInput = ({ label, value, className }: Props) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn('w-full text-left', className)}
|
||||
onClick={() => clipboard(value)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { BanIcon, InfoIcon } from 'lucide-react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Input } from '../ui/input';
|
||||
import type { InputProps } from '../ui/input';
|
||||
@@ -57,7 +57,7 @@ export const InputWithLabel = forwardRef<HTMLInputElement, InputWithLabelProps>(
|
||||
<Input ref={ref} id={props.label} {...props} />
|
||||
</WithLabel>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
InputWithLabel.displayName = 'InputWithLabel';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Based on Christin Alares tag input component (https://github.com/christianalares/seventy-seven)
|
||||
|
||||
import type { ElementRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
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;
|
||||
@@ -47,7 +47,7 @@ const TagInput = ({
|
||||
e.preventDefault();
|
||||
|
||||
const tagAlreadyExists = value.some(
|
||||
(tag) => tag.toLowerCase() === inputValue.toLowerCase()
|
||||
(tag) => tag.toLowerCase() === inputValue.toLowerCase(),
|
||||
);
|
||||
|
||||
if (inputValue) {
|
||||
@@ -59,7 +59,7 @@ const TagInput = ({
|
||||
},
|
||||
{
|
||||
duration: 0.3,
|
||||
}
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ const TagInput = ({
|
||||
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',
|
||||
!!error && 'border-destructive'
|
||||
!!error && 'border-destructive',
|
||||
)}
|
||||
>
|
||||
{value.map((tag, i) => {
|
||||
@@ -116,7 +116,7 @@ const TagInput = ({
|
||||
isMarkedForDeletion &&
|
||||
i === value.length - 1 &&
|
||||
'bg-destructive-foreground ring-2 ring-destructive/50 ring-offset-1',
|
||||
isCreating && 'opacity-60'
|
||||
isCreating && 'opacity-60',
|
||||
)}
|
||||
>
|
||||
{renderTag ? renderTag(tag) : tag}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function FullPageEmptyState({
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center p-4 text-center',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full max-w-xl flex-col items-center justify-center p-8">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { bind } from 'bind-event-listener';
|
||||
import { ChevronLeftIcon, FullscreenIcon } from 'lucide-react';
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
@@ -21,7 +21,7 @@ export const useFullscreen = () =>
|
||||
parseAsBoolean.withDefault(false).withOptions({
|
||||
history: 'push',
|
||||
clearOnDefault: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
export const Fullscreen = (props: Props) => {
|
||||
@@ -29,7 +29,7 @@ export const Fullscreen = (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isFullscreen && 'fixed inset-0 z-50 overflow-auto bg-def-200'
|
||||
isFullscreen && 'fixed inset-0 z-50 overflow-auto bg-def-200',
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
@@ -92,10 +92,11 @@ export const FullscreenClose = () => {
|
||||
<div className="fixed bottom-0 top-0 z-50 flex items-center">
|
||||
<Tooltiper content="Exit full screen" asChild>
|
||||
<button
|
||||
type="button"
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-20 w-20 -translate-x-20 items-center justify-center rounded-full bg-foreground transition-transform',
|
||||
visible && isFullscreenDebounced && '-translate-x-10'
|
||||
visible && isFullscreenDebounced && '-translate-x-10',
|
||||
)}
|
||||
onClick={() => {
|
||||
setIsFullscreen(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type React from 'react';
|
||||
|
||||
export const Grid: React.FC<
|
||||
React.HTMLAttributes<HTMLDivElement> & { columns: number }
|
||||
@@ -63,7 +63,7 @@ export const GridCell: React.FC<
|
||||
'flex min-h-12 items-center whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border',
|
||||
isHeader && 'h-10 bg-def-100 font-semibold text-muted-foreground',
|
||||
colSpan && `col-span-${colSpan}`,
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -79,7 +79,7 @@ export const GridRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
<div
|
||||
className={cn(
|
||||
'contents transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function OriginFilter() {
|
||||
},
|
||||
{
|
||||
staleTime: 1000 * 60 * 60,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
@@ -32,7 +32,7 @@ export function OriginFilter() {
|
||||
variant="outline"
|
||||
icon={GlobeIcon}
|
||||
className={cn(
|
||||
originFilter?.value.includes(item.origin) && 'border-foreground'
|
||||
originFilter?.value.includes(item.origin) && 'border-foreground',
|
||||
)}
|
||||
onClick={() => setFilter('origin', [item.origin], 'is')}
|
||||
>
|
||||
|
||||
@@ -125,7 +125,7 @@ export function FilterOptionEvent({
|
||||
setFilter: (
|
||||
name: string,
|
||||
value: IChartEventFilterValue,
|
||||
operator: IChartEventFilterOperator
|
||||
operator: IChartEventFilterOperator,
|
||||
) => void;
|
||||
}) {
|
||||
const { interval, range } = useOverviewOptions();
|
||||
@@ -172,7 +172,7 @@ export function FilterOptionProfile({
|
||||
setFilter: (
|
||||
name: string,
|
||||
value: IChartEventFilterValue,
|
||||
operator: IChartEventFilterOperator
|
||||
operator: IChartEventFilterOperator,
|
||||
) => void;
|
||||
}) {
|
||||
const values = useProfileValues(projectId, filter.name);
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { OverviewFiltersDrawerContentProps } from './overview-filters-drawe
|
||||
import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
|
||||
|
||||
export function OverviewFiltersDrawer(
|
||||
props: OverviewFiltersDrawerContentProps
|
||||
props: OverviewFiltersDrawerContentProps,
|
||||
) {
|
||||
return (
|
||||
<Sheet>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import { TooltipComplete } from '@/components/tooltip-complete';
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -12,6 +11,7 @@ import useWS from '@/hooks/useWS';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export interface LiveCounterProps {
|
||||
@@ -34,7 +34,7 @@ export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
|
||||
const lastRefresh = useRef(Date.now());
|
||||
|
||||
useWS<number>(`/live/visitors/${projectId}`, (value) => {
|
||||
if (!isNaN(value)) {
|
||||
if (!Number.isNaN(value)) {
|
||||
counter.set(value);
|
||||
if (Date.now() - lastRefresh.current > FIFTEEN_SECONDS) {
|
||||
lastRefresh.current = Date.now();
|
||||
@@ -57,13 +57,13 @@ export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
|
||||
<div
|
||||
className={cn(
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
|
||||
counter.debounced === 0 && 'bg-destructive opacity-0'
|
||||
counter.debounced === 0 && 'bg-destructive opacity-0',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
|
||||
counter.debounced === 0 && 'bg-destructive'
|
||||
counter.debounced === 0 && 'bg-destructive',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { BarChartIcon, LineChartIcon } from 'lucide-react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
|
||||
@@ -69,18 +69,16 @@ export function OverviewLiveHistogram({
|
||||
const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0;
|
||||
|
||||
if (res.isInitialLoading || countRes.isInitialLoading) {
|
||||
// prettier-ignore
|
||||
const staticArray = [
|
||||
10, 25, 30, 45, 20, 5, 55, 18, 40, 12,
|
||||
50, 35, 8, 22, 38, 42, 15, 28, 52, 5,
|
||||
48, 14, 32, 58, 7, 19, 33, 56, 24, 5
|
||||
10, 25, 30, 45, 20, 5, 55, 18, 40, 12, 50, 35, 8, 22, 38, 42, 15, 28, 52,
|
||||
5, 48, 14, 32, 58, 7, 19, 33, 56, 24, 5,
|
||||
];
|
||||
|
||||
return (
|
||||
<Wrapper count={0}>
|
||||
{staticArray.map((percent, i) => (
|
||||
<div
|
||||
key={i}
|
||||
key={i as number}
|
||||
className="flex-1 animate-pulse rounded-t bg-def-200"
|
||||
style={{ height: `${percent}%` }}
|
||||
/>
|
||||
@@ -102,7 +100,7 @@ export function OverviewLiveHistogram({
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 rounded-t transition-all ease-in-out hover:scale-110',
|
||||
minute.count === 0 ? 'bg-def-200' : 'bg-highlight'
|
||||
minute.count === 0 ? 'bg-def-200' : 'bg-highlight',
|
||||
)}
|
||||
style={{
|
||||
height:
|
||||
|
||||
@@ -197,10 +197,11 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
<div className="card mb-2 grid grid-cols-4 overflow-hidden rounded-md">
|
||||
{reports.map((report, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
key={report.id}
|
||||
className={cn(
|
||||
'col-span-2 flex-1 shadow-[0_0_0_0.5px] shadow-border md:col-span-1',
|
||||
index === metric && 'bg-def-100'
|
||||
index === metric && 'bg-def-100',
|
||||
)}
|
||||
onClick={() => {
|
||||
setMetric(index);
|
||||
@@ -211,7 +212,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
'col-span-4 min-h-16 flex-1 p-4 pb-0 shadow-[0_0_0_0.5px] shadow-border max-md:row-start-1 md:col-span-2'
|
||||
'col-span-4 min-h-16 flex-1 p-4 pb-0 shadow-[0_0_0_0.5px] shadow-border max-md:row-start-1 md:col-span-2',
|
||||
)}
|
||||
>
|
||||
<OverviewLiveHistogram projectId={projectId} />
|
||||
|
||||
@@ -31,8 +31,8 @@ export function OverviewShare({ data }: OverviewShareProps) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button icon={data && data.public ? Globe2Icon : LockIcon} responsive>
|
||||
{data && data.public ? 'Public' : 'Private'}
|
||||
<Button icon={data?.public ? Globe2Icon : LockIcon} responsive>
|
||||
{data?.public ? 'Public' : 'Private'}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { api } from '@/trpc/client';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Pagination } from '../pagination';
|
||||
import { Tooltiper } from '../ui/tooltip';
|
||||
@@ -23,7 +23,7 @@ const OverviewTopBots = ({ projectId }: Props) => {
|
||||
{ projectId, cursor },
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
const data = res.data?.data ?? [];
|
||||
const count = res.data?.count ?? 0;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
@@ -295,6 +295,7 @@ export default function OverviewTopDevices({
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ReportChart } from '@/components/report-chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
@@ -155,6 +155,7 @@ export default function OverviewTopEvents({
|
||||
.filter((item) => item.hide !== true)
|
||||
.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { getCountry } from '@/translations/countries';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
@@ -158,6 +158,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ExternalLinkIcon, FilterIcon, Globe2Icon } from 'lucide-react';
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
@@ -180,6 +180,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
@@ -225,7 +226,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
<WidgetFooter>
|
||||
<OverviewDetailsButton chart={widget.chart.report} />
|
||||
<OverviewChartToggle {...{ chartType, setChartType }} />
|
||||
<div className="flex-1"></div>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
@@ -301,6 +301,7 @@ export default function OverviewTopSources({
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { Children, useEffect, useRef, useState } from 'react';
|
||||
import { useThrottle } from '@/hooks/useThrottle';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||
import { last } from 'ramda';
|
||||
import { Children, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -21,7 +21,7 @@ export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
||||
<WidgetHeadBase
|
||||
className={cn(
|
||||
'flex flex-col rounded-t-xl p-0 [&_.title]:flex [&_.title]:items-center [&_.title]:justify-between [&_.title]:p-4 [&_.title]:font-semibold',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -43,11 +43,11 @@ export function WidgetButtons({
|
||||
if (sizes.current.length === 0) {
|
||||
// Get buttons
|
||||
const buttons: HTMLButtonElement[] = Array.from(
|
||||
container.current.querySelectorAll(`button`)
|
||||
container.current.querySelectorAll('button'),
|
||||
);
|
||||
// Get sizes and cache them
|
||||
sizes.current = buttons.map(
|
||||
(button) => Math.ceil(button.offsetWidth) + gap
|
||||
(button) => Math.ceil(button.offsetWidth) + gap,
|
||||
);
|
||||
}
|
||||
const containerWidth = container.current.offsetWidth;
|
||||
@@ -62,7 +62,7 @@ export function WidgetButtons({
|
||||
}
|
||||
return { index, size: acc.size + size };
|
||||
},
|
||||
{ index: 0, size: 0 }
|
||||
{ index: 0, size: 0 },
|
||||
);
|
||||
|
||||
setSlice(res.index);
|
||||
@@ -86,7 +86,7 @@ export function WidgetButtons({
|
||||
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
|
||||
className,
|
||||
)}
|
||||
style={{ gap }}
|
||||
{...props}
|
||||
@@ -96,7 +96,7 @@ export function WidgetButtons({
|
||||
<div
|
||||
className={cn(
|
||||
'flex [&_button]:leading-normal',
|
||||
slice < index ? hidden : 'opacity-100'
|
||||
slice < index ? hidden : 'opacity-100',
|
||||
)}
|
||||
>
|
||||
{child}
|
||||
@@ -106,9 +106,10 @@ export function WidgetButtons({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex select-none items-center gap-1',
|
||||
sizes.current.length - 1 === slice ? hidden : 'opacity-50'
|
||||
sizes.current.length - 1 === slice ? hidden : 'opacity-50',
|
||||
)}
|
||||
>
|
||||
More <ChevronsUpDownIcon size={12} />
|
||||
@@ -138,7 +139,7 @@ export function WidgetFooter({
|
||||
<div
|
||||
className={cn(
|
||||
'flex rounded-b-md border-t bg-def-100 p-2 py-1',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -19,17 +19,17 @@ const nuqsOptions = { history: 'push' } as const;
|
||||
export function useOverviewOptions() {
|
||||
const [startDate, setStartDate] = useQueryState(
|
||||
'start',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
parseAsString.withOptions(nuqsOptions),
|
||||
);
|
||||
const [endDate, setEndDate] = useQueryState(
|
||||
'end',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
parseAsString.withOptions(nuqsOptions),
|
||||
);
|
||||
const [range, setRange] = useQueryState(
|
||||
'range',
|
||||
parseAsStringEnum(mapKeys(timeWindows))
|
||||
.withDefault('7d')
|
||||
.withOptions(nuqsOptions)
|
||||
.withOptions(nuqsOptions),
|
||||
);
|
||||
|
||||
const interval =
|
||||
@@ -38,7 +38,7 @@ export function useOverviewOptions() {
|
||||
|
||||
const [metric, setMetric] = useQueryState(
|
||||
'metric',
|
||||
parseAsInteger.withDefault(0).withOptions(nuqsOptions)
|
||||
parseAsInteger.withDefault(0).withOptions(nuqsOptions),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,14 +9,14 @@ export function useOverviewWidget<T extends string>(
|
||||
widgets: Record<
|
||||
T,
|
||||
{ title: string; btn: string; chart: ReportChartProps; hide?: boolean }
|
||||
>
|
||||
>,
|
||||
) {
|
||||
const keys = Object.keys(widgets) as T[];
|
||||
const [widget, setWidget] = useQueryState<T>(
|
||||
key,
|
||||
parseAsStringEnum(keys)
|
||||
.withDefault(keys[0]!)
|
||||
.withOptions({ history: 'push' })
|
||||
.withOptions({ history: 'push' }),
|
||||
);
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ export function PageTabsLink({
|
||||
<Link
|
||||
className={cn(
|
||||
'inline-block opacity-100 transition-transform hover:translate-y-[-1px]',
|
||||
isActive ? 'opacity-100' : 'opacity-50'
|
||||
isActive ? 'opacity-100' : 'opacity-50',
|
||||
)}
|
||||
href={href}
|
||||
>
|
||||
@@ -50,9 +50,10 @@ export function PageTabsItem({
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-block opacity-100 transition-transform hover:translate-y-[-1px]',
|
||||
isActive ? 'opacity-100' : 'opacity-50'
|
||||
isActive ? 'opacity-100' : 'opacity-50',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
@@ -7,6 +5,8 @@ import {
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon,
|
||||
} from 'lucide-react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
|
||||
@@ -45,7 +45,7 @@ export function Pagination({
|
||||
<div
|
||||
className={cn(
|
||||
'flex select-none items-center justify-end gap-1',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{size === 'base' && (
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ProfileAvatar({
|
||||
: size === 'xs'
|
||||
? 'text-[8px]'
|
||||
: 'text-base',
|
||||
'bg-def-200 text-muted-foreground'
|
||||
'bg-def-200 text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{firstName?.at(0) ?? '🫣'}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { memo } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import { Pagination } from '@/components/pagination';
|
||||
@@ -8,6 +6,8 @@ import { TableSkeleton } from '@/components/ui/table';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { GanttChartIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import type { IServiceProfile } from '@openpanel/db';
|
||||
|
||||
@@ -58,7 +58,7 @@ export const ProfilesTable = memo(
|
||||
className="mt-2"
|
||||
setCursor={props.setCursor}
|
||||
cursor={props.cursor}
|
||||
count={Infinity}
|
||||
count={Number.POSITIVE_INFINITY}
|
||||
take={50}
|
||||
loading={isFetching}
|
||||
/>
|
||||
@@ -68,7 +68,7 @@ export const ProfilesTable = memo(
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return isEqual(prevProps.query.data, nextProps.query.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ProfilesTable.displayName = 'ProfilesTable';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Suspense } from 'react';
|
||||
import { shortNumber } from '@/hooks/useNumerFormatter';
|
||||
import { Suspense } from 'react';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import type { IServiceProject } from '@openpanel/db';
|
||||
import { chQuery, TABLE_NAMES } from '@openpanel/db';
|
||||
import { TABLE_NAMES, chQuery } from '@openpanel/db';
|
||||
|
||||
import { ChartSSR } from '../chart-ssr';
|
||||
import { FadeIn } from '../fade-in';
|
||||
@@ -34,7 +34,7 @@ function ProjectCard({ id, name, organizationSlug }: IServiceProject) {
|
||||
|
||||
async function ProjectChart({ id }: { id: string }) {
|
||||
const chart = await chQuery<{ value: number; date: string }>(
|
||||
`SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(id)} AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC`
|
||||
`SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(id)} AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC`,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -70,7 +70,7 @@ async function ProjectMetrics({ id }: { id: string }) {
|
||||
(
|
||||
SELECT count(DISTINCT profile_id) as count FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 day'
|
||||
) as day
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import { api } from '@/trpc/client';
|
||||
@@ -9,6 +8,7 @@ import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import { isSameDay, isSameHour, isSameMonth } from 'date-fns';
|
||||
import { last } from 'ramda';
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
Area,
|
||||
CartesianGrid,
|
||||
@@ -57,7 +57,7 @@ export function Chart({ data }: Props) {
|
||||
},
|
||||
{
|
||||
staleTime: 1000 * 60 * 10,
|
||||
}
|
||||
},
|
||||
);
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
@@ -73,7 +73,7 @@ export function Chart({ data }: Props) {
|
||||
id: string,
|
||||
col1: string,
|
||||
col2: string,
|
||||
percentChange: number
|
||||
percentChange: number,
|
||||
) => (
|
||||
<linearGradient id={id} x1="0" y1="0" x2="100%" y2="0">
|
||||
<stop offset="0%" stopColor={col1} />
|
||||
@@ -177,29 +177,25 @@ export function Chart({ data }: Props) {
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={color}
|
||||
stopOpacity={0.8}
|
||||
></stop>
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.8} />
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={color}
|
||||
stopOpacity={0.1}
|
||||
></stop>
|
||||
/>
|
||||
</linearGradient>
|
||||
)}
|
||||
{gradientTwoColors(
|
||||
`hideAllButLastInterval_${serie.id}`,
|
||||
'rgba(0,0,0,0)',
|
||||
color,
|
||||
lastIntervalPercent
|
||||
lastIntervalPercent,
|
||||
)}
|
||||
{gradientTwoColors(
|
||||
`hideJustLastInterval_${serie.id}`,
|
||||
color,
|
||||
'rgba(0,0,0,0)',
|
||||
lastIntervalPercent
|
||||
lastIntervalPercent,
|
||||
)}
|
||||
</defs>
|
||||
<Line
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,6 +10,7 @@ import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { DropdownMenuPortal } from '@radix-ui/react-dropdown-menu';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { round } from '@openpanel/common';
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
@@ -33,7 +33,7 @@ export function Chart({ data }: Props) {
|
||||
const number = useNumber();
|
||||
const series = useMemo(
|
||||
() => (isEditMode ? data.series : data.series.slice(0, limit || 10)),
|
||||
[data, isEditMode, limit]
|
||||
[data, isEditMode, limit],
|
||||
);
|
||||
const maxCount = Math.max(...series.map((serie) => serie.metrics[metric]));
|
||||
|
||||
@@ -41,7 +41,7 @@ export function Chart({ data }: Props) {
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col text-sm',
|
||||
isEditMode ? 'card gap-2 p-4 text-base' : '-m-3 gap-1'
|
||||
isEditMode ? 'card gap-2 p-4 text-base' : '-m-3 gap-1',
|
||||
)}
|
||||
>
|
||||
{series.map((serie) => {
|
||||
@@ -56,7 +56,7 @@ export function Chart({ data }: Props) {
|
||||
<div
|
||||
className={cn(
|
||||
'relative',
|
||||
(isClickable || isDropDownEnabled) && 'cursor-pointer'
|
||||
(isClickable || isDropDownEnabled) && 'cursor-pointer',
|
||||
)}
|
||||
{...(isClickable && !isDropDownEnabled
|
||||
? { onClick: () => onClick?.(serie) }
|
||||
@@ -80,7 +80,7 @@ export function Chart({ data }: Props) {
|
||||
{serie.metrics.previous?.[metric]?.value}
|
||||
<div className="text-muted-foreground">
|
||||
{number.format(
|
||||
round((serie.metrics.sum / data.metrics.sum) * 100, 2)
|
||||
round((serie.metrics.sum / data.metrics.sum) * 100, 2),
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
|
||||
@@ -33,12 +33,15 @@ function Loading() {
|
||||
return (
|
||||
<AspectContainer className="col gap-4 overflow-hidden">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<div key={index} className="row animate-pulse justify-between">
|
||||
<div className="h-4 w-2/5 rounded bg-def-200"></div>
|
||||
<div
|
||||
key={index as number}
|
||||
className="row animate-pulse justify-between"
|
||||
>
|
||||
<div className="h-4 w-2/5 rounded bg-def-200" />
|
||||
<div className="row w-1/5 gap-2">
|
||||
<div className="h-4 w-full rounded bg-def-200"></div>
|
||||
<div className="h-4 w-full rounded bg-def-200"></div>
|
||||
<div className="h-4 w-full rounded bg-def-200"></div>
|
||||
<div className="h-4 w-full rounded bg-def-200" />
|
||||
<div className="h-4 w-full rounded bg-def-200" />
|
||||
<div className="h-4 w-full rounded bg-def-200" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { isNil } from 'ramda';
|
||||
import { useRef, useState } from 'react';
|
||||
import type { AxisDomain } from 'recharts/types/util/types';
|
||||
|
||||
import type { IInterval } from '@openpanel/validation';
|
||||
@@ -60,10 +60,10 @@ export const useXAxisProps = (
|
||||
} = {
|
||||
hide: false,
|
||||
interval: 'auto',
|
||||
}
|
||||
},
|
||||
) => {
|
||||
const formatDate = useFormatDateInterval(
|
||||
interval === 'auto' ? 'day' : interval
|
||||
interval === 'auto' ? 'day' : interval,
|
||||
);
|
||||
return {
|
||||
height: hide ? 0 : 14,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export function ReportChartLoading() {
|
||||
return <div className="h-full w-full animate-pulse rounded bg-def-100"></div>;
|
||||
return <div className="h-full w-full animate-pulse rounded bg-def-100" />;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export function getDiffIndicator<A, B, C>(
|
||||
state: string | undefined | null,
|
||||
positive: A,
|
||||
negative: B,
|
||||
neutral: C
|
||||
neutral: C,
|
||||
): A | B | C {
|
||||
if (state === 'neutral' || !state) {
|
||||
return neutral;
|
||||
@@ -48,7 +48,7 @@ export function PreviousDiffIndicator({
|
||||
state,
|
||||
'bg-emerald-300',
|
||||
'bg-rose-300',
|
||||
undefined
|
||||
undefined,
|
||||
);
|
||||
const number = useNumber();
|
||||
|
||||
@@ -72,14 +72,14 @@ export function PreviousDiffIndicator({
|
||||
className={cn(
|
||||
'flex items-center gap-1 font-mono font-medium',
|
||||
size === 'lg' && 'gap-2',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
`flex size-4 items-center justify-center rounded-full`,
|
||||
'flex size-4 items-center justify-center rounded-full',
|
||||
variant,
|
||||
size === 'lg' && 'size-8'
|
||||
size === 'lg' && 'size-8',
|
||||
)}
|
||||
>
|
||||
{renderIcon()}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
|
||||
@@ -6,6 +5,7 @@ import type { IToolTipProps } from '@/types';
|
||||
import * as Portal from '@radix-ui/react-portal';
|
||||
import { bind } from 'bind-event-listener';
|
||||
import throttle from 'lodash.throttle';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useReportChartContext } from '../context';
|
||||
import { PreviousDiffIndicator } from './previous-diff-indicator';
|
||||
@@ -29,7 +29,7 @@ export function ReportChartTooltip({
|
||||
const formatDate = useFormatDateInterval(interval);
|
||||
const number = useNumber();
|
||||
const [position, setPosition] = useState<{ x: number; y: number } | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
|
||||
const inactive = !active || !payload?.length;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from 'react';
|
||||
import { Pagination, usePagination } from '@/components/pagination';
|
||||
import { Stats, StatsCard } from '@/components/stats';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -18,6 +17,7 @@ import { useSelector } from '@/redux';
|
||||
import { getPropertyLabel } from '@/translations/properties';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import type * as React from 'react';
|
||||
|
||||
import { PreviousDiffIndicator } from './previous-diff-indicator';
|
||||
import { SerieName } from './serie-name';
|
||||
@@ -45,9 +45,8 @@ export function ReportTable({
|
||||
setVisibleSeries((prev) => {
|
||||
if (checked) {
|
||||
return [...prev, name];
|
||||
} else {
|
||||
return prev.filter((item) => item !== name);
|
||||
}
|
||||
return prev.filter((item) => item !== name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,7 +76,7 @@ export function ReportTable({
|
||||
<TableBody className="bg-def-100">
|
||||
{paginate(data.series).map((serie, index) => {
|
||||
const checked = !!visibleSeries.find(
|
||||
(item) => item.id === serie.id
|
||||
(item) => item.id === serie.id,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||
|
||||
const createFlagIcon = (url: string) => {
|
||||
return function (_props: LucideProps) {
|
||||
return (
|
||||
<span
|
||||
className={`fi !block aspect-[1.33] overflow-hidden rounded-[2px] fi-${url}`}
|
||||
></span>
|
||||
);
|
||||
} as LucideIcon;
|
||||
return ((_props: LucideProps) => (
|
||||
<span
|
||||
className={`fi !block aspect-[1.33] overflow-hidden rounded-[2px] fi-${url}`}
|
||||
/>
|
||||
)) as LucideIcon;
|
||||
};
|
||||
|
||||
const data = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||
import {
|
||||
ActivityIcon,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
TabletIcon,
|
||||
TvIcon,
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
|
||||
@@ -30,9 +30,13 @@ function getProxyImage(url: string) {
|
||||
}
|
||||
|
||||
const createImageIcon = (url: string) => {
|
||||
return function (_props: LucideProps) {
|
||||
return <img className="max-h-4 rounded-[2px] object-contain" src={url} />;
|
||||
} as LucideIcon;
|
||||
return ((_props: LucideProps) => (
|
||||
<img
|
||||
alt="serie icon"
|
||||
className="max-h-4 rounded-[2px] object-contain"
|
||||
src={url}
|
||||
/>
|
||||
)) as LucideIcon;
|
||||
};
|
||||
|
||||
const mapper: Record<string, LucideIcon> = {
|
||||
@@ -60,7 +64,7 @@ const mapper: Record<string, LucideIcon> = {
|
||||
...acc,
|
||||
[key]: createImageIcon(getProxyImage(value)),
|
||||
}),
|
||||
{}
|
||||
{},
|
||||
),
|
||||
|
||||
...flags,
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
// prettier-ignore
|
||||
// biome-ignore format: annoying
|
||||
|
||||
const data = {
|
||||
'chromium os': 'https://upload.wikimedia.org/wikipedia/commons/2/28/Chromium_Logo.svg',
|
||||
'mac os': 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/30/MacOS_logo.svg/1200px-MacOS_logo.svg.png',
|
||||
'apple': 'https://sladesportfolio.wordpress.com/wp-content/uploads/2015/08/apple_logo_black-svg.png',
|
||||
'huawei': 'https://upload.wikimedia.org/wikipedia/en/0/04/Huawei_Standard_logo.svg',
|
||||
'xiaomi': 'https://upload.wikimedia.org/wikipedia/commons/2/29/Xiaomi_logo.svg',
|
||||
'sony': 'https://serialtrainer7.com/wp-content/uploads/2021/07/sony-logo-300px-square.png',
|
||||
'lg': 'https://upload.wikimedia.org/wikipedia/commons/2/20/LG_symbol.svg',
|
||||
'samsung': 'https://seekvectors.com/storage/images/Samsung-Logo-22.svg',
|
||||
'oppo': 'https://indoleads.nyc3.cdn.digitaloceanspaces.com/uploads/offers/logos/8695_95411e367b832.png',
|
||||
'motorola': 'https://upload.wikimedia.org/wikipedia/commons/8/8f/Motorola_M_symbol_blue.svg',
|
||||
'oneplus': 'https://pbs.twimg.com/profile_images/1709165009148809216/ebHb4xhF_400x400.png',
|
||||
'asus': 'https://cdn-icons-png.freepik.com/512/5969/5969050.png',
|
||||
'fairphone': 'https://cdn.dribbble.com/users/433772/screenshots/2109827/fairphone_dribbble.jpg',
|
||||
'nokia': 'https://www.gizchina.com/wp-content/uploads/images/2023/02/Nokia-logo.webp',
|
||||
apple: 'https://sladesportfolio.wordpress.com/wp-content/uploads/2015/08/apple_logo_black-svg.png',
|
||||
huawei: 'https://upload.wikimedia.org/wikipedia/en/0/04/Huawei_Standard_logo.svg',
|
||||
xiaomi: 'https://upload.wikimedia.org/wikipedia/commons/2/29/Xiaomi_logo.svg',
|
||||
sony: 'https://serialtrainer7.com/wp-content/uploads/2021/07/sony-logo-300px-square.png',
|
||||
lg: 'https://upload.wikimedia.org/wikipedia/commons/2/20/LG_symbol.svg',
|
||||
samsung: 'https://seekvectors.com/storage/images/Samsung-Logo-22.svg',
|
||||
oppo: 'https://indoleads.nyc3.cdn.digitaloceanspaces.com/uploads/offers/logos/8695_95411e367b832.png',
|
||||
motorola: 'https://upload.wikimedia.org/wikipedia/commons/8/8f/Motorola_M_symbol_blue.svg',
|
||||
oneplus: 'https://pbs.twimg.com/profile_images/1709165009148809216/ebHb4xhF_400x400.png',
|
||||
asus: 'https://cdn-icons-png.freepik.com/512/5969/5969050.png',
|
||||
fairphone: 'https://cdn.dribbble.com/users/433772/screenshots/2109827/fairphone_dribbble.jpg',
|
||||
nokia: 'https://www.gizchina.com/wp-content/uploads/images/2023/02/Nokia-logo.webp',
|
||||
'mobile safari': 'https://upload.wikimedia.org/wikipedia/commons/5/52/Safari_browser_logo.svg',
|
||||
'openpanel.dev': 'https://openpanel.dev',
|
||||
'samsung internet': 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Samsung_Internet_logo.svg/1024px-Samsung_Internet_logo.svg.png',
|
||||
@@ -21,12 +22,12 @@ const data = {
|
||||
'yahoo!': 'https://yahoo.com',
|
||||
android: 'https://image.similarpng.com/very-thumbnail/2020/08/Android-icon-on-transparent--background-PNG.png',
|
||||
'android browser': 'https://image.similarpng.com/very-thumbnail/2020/08/Android-icon-on-transparent--background-PNG.png',
|
||||
'silk': 'https://m.media-amazon.com/images/I/51VCjQCvF0L.png',
|
||||
'kakaotalk': 'https://www.kakaocorp.com/',
|
||||
silk: 'https://m.media-amazon.com/images/I/51VCjQCvF0L.png',
|
||||
kakaotalk: 'https://www.kakaocorp.com/',
|
||||
bing: 'https://bing.com',
|
||||
'electron': 'https://www.electronjs.org',
|
||||
'whale': 'https://whale.naver.com',
|
||||
'wechat': 'https://wechat.com',
|
||||
electron: 'https://www.electronjs.org',
|
||||
whale: 'https://whale.naver.com',
|
||||
wechat: 'https://wechat.com',
|
||||
chrome: 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
'chrome webview': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
'chrome headless': 'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg',
|
||||
@@ -68,8 +69,8 @@ const data = {
|
||||
tiktok: 'https://tiktok.com',
|
||||
sharpspring: 'https://sharpspring.com',
|
||||
'hacker news': 'https://news.ycombinator.com',
|
||||
'betalist': 'https://betalist.com',
|
||||
'qwant': 'https://www.qwant.com',
|
||||
betalist: 'https://betalist.com',
|
||||
qwant: 'https://www.qwant.com',
|
||||
flipboard: 'https://flipboard.com/',
|
||||
trustpilot: 'https://trustpilot.com',
|
||||
'outlook.com': 'https://login.live.com/',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ChevronRightIcon } from 'lucide-react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { useReportChartContext } from '../context';
|
||||
|
||||
interface SerieNameProps {
|
||||
@@ -23,12 +24,12 @@ export function SerieName({ name, className }: SerieNameProps) {
|
||||
<div className={cn('flex items-center gap-1', className)}>
|
||||
{name.map((n, index) => {
|
||||
return (
|
||||
<>
|
||||
<Fragment key={n}>
|
||||
<span>{n || NOT_SET_VALUE}</span>
|
||||
{name.length - 1 > index && (
|
||||
<ChevronRightIcon className="text-muted-foreground" size={12} />
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import type {
|
||||
IChartInput,
|
||||
@@ -46,14 +46,14 @@ export const useReportChartContext = () => {
|
||||
const ctx = useContext(context);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
'useReportChartContext must be used within a ReportChartProvider'
|
||||
'useReportChartContext must be used within a ReportChartProvider',
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const useSelectReportChartContext = <T,>(
|
||||
selector: (ctx: ReportChartContextType) => T
|
||||
selector: (ctx: ReportChartContextType) => T,
|
||||
) => {
|
||||
const ctx = useReportChartContext();
|
||||
const [state, setState] = useState(selector(ctx));
|
||||
|
||||
@@ -17,7 +17,7 @@ import { PreviousDiffIndicator } from '../common/previous-diff-indicator';
|
||||
import { useReportChartContext } from '../context';
|
||||
|
||||
const findMostDropoffs = (
|
||||
steps: RouterOutputs['chart']['funnel']['current']['steps']
|
||||
steps: RouterOutputs['chart']['funnel']['current']['steps'],
|
||||
) => {
|
||||
return steps.reduce((acc, step) => {
|
||||
if (step.dropoffCount > acc.dropoffCount) {
|
||||
@@ -58,7 +58,7 @@ export function Chart({
|
||||
<div
|
||||
className={cn(
|
||||
'border border-border',
|
||||
!isEditMode && 'border-0 border-b'
|
||||
!isEditMode && 'border-0 border-b',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-8 p-4">
|
||||
@@ -72,7 +72,7 @@ export function Chart({
|
||||
<div
|
||||
className="w-full bg-def-400"
|
||||
style={{ height: `${step.percent}%` }}
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -129,7 +129,7 @@ export function Chart({
|
||||
<PreviousDiffIndicator
|
||||
{...getPreviousMetric(
|
||||
step.previousCount,
|
||||
previous.steps[index]?.previousCount
|
||||
previous.steps[index]?.previousCount,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@ export function Chart({
|
||||
inverted
|
||||
{...getPreviousMetric(
|
||||
step.dropoffCount,
|
||||
previous.steps[index]?.dropoffCount
|
||||
previous.steps[index]?.dropoffCount,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@ export function Chart({
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center gap-1 text-lg font-bold',
|
||||
isMostDropoffs && 'text-rose-500'
|
||||
isMostDropoffs && 'text-rose-500',
|
||||
)}
|
||||
>
|
||||
{isMostDropoffs && <AlertCircleIcon size={14} />}
|
||||
@@ -196,7 +196,7 @@ export function Chart({
|
||||
<PreviousDiffIndicator
|
||||
{...getPreviousMetric(
|
||||
step.count,
|
||||
previous.steps[index]?.count
|
||||
previous.steps[index]?.count,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -208,7 +208,7 @@ export function Chart({
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-lg font-bold">{step.count}</span>
|
||||
{/* <button
|
||||
{/* <button type="button"
|
||||
className="ml-2 underline"
|
||||
onClick={() =>
|
||||
pushModal('FunnelStepDetails', {
|
||||
@@ -234,6 +234,6 @@ export function Chart({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor, theme } from '@/utils/theme';
|
||||
import { useTheme } from 'next-themes';
|
||||
import React from 'react';
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { mergeDeepRight } from 'ramda';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useInViewport } from 'react-in-viewport';
|
||||
|
||||
import { ReportAreaChart } from './area';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import { api } from '@/trpc/client';
|
||||
@@ -9,6 +8,7 @@ import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import { isSameDay, isSameHour, isSameMonth } from 'date-fns';
|
||||
import { last } from 'ramda';
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
Area,
|
||||
CartesianGrid,
|
||||
@@ -57,7 +57,7 @@ export function Chart({ data }: Props) {
|
||||
},
|
||||
{
|
||||
staleTime: 1000 * 60 * 10,
|
||||
}
|
||||
},
|
||||
);
|
||||
const { series, setVisibleSeries } = useVisibleSeries(data);
|
||||
const rechartData = useRechartDataModel(series);
|
||||
@@ -73,7 +73,7 @@ export function Chart({ data }: Props) {
|
||||
id: string,
|
||||
col1: string,
|
||||
col2: string,
|
||||
percentChange: number
|
||||
percentChange: number,
|
||||
) => (
|
||||
<linearGradient id={id} x1="0" y1="0" x2="100%" y2="0">
|
||||
<stop offset="0%" stopColor={col1} />
|
||||
@@ -177,29 +177,25 @@ export function Chart({ data }: Props) {
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={color}
|
||||
stopOpacity={0.8}
|
||||
></stop>
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.8} />
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={color}
|
||||
stopOpacity={0.1}
|
||||
></stop>
|
||||
/>
|
||||
</linearGradient>
|
||||
)}
|
||||
{gradientTwoColors(
|
||||
`hideAllButLastInterval_${serie.id}`,
|
||||
'rgba(0,0,0,0)',
|
||||
color,
|
||||
lastIntervalPercent
|
||||
lastIntervalPercent,
|
||||
)}
|
||||
{gradientTwoColors(
|
||||
`hideJustLastInterval_${serie.id}`,
|
||||
color,
|
||||
'rgba(0,0,0,0)',
|
||||
lastIntervalPercent
|
||||
lastIntervalPercent,
|
||||
)}
|
||||
</defs>
|
||||
<Line
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import type { IChartData } from '@/trpc/client';
|
||||
import { theme } from '@/utils/theme';
|
||||
import { useMemo } from 'react';
|
||||
import WorldMap from 'react-svg-worldmap';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
@@ -23,7 +23,7 @@ export function Chart({ data }: Props) {
|
||||
country: s.names[0]?.toLowerCase() ?? '',
|
||||
value: s.metrics[metric],
|
||||
})),
|
||||
[series, metric]
|
||||
[series, metric],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,7 +21,7 @@ export function Chart({ data }: Props) {
|
||||
<div
|
||||
className={cn(
|
||||
'grid grid-cols-1 gap-4',
|
||||
isEditMode && 'md:grid-cols-2 lg:grid-cols-3'
|
||||
isEditMode && 'md:grid-cols-2 lg:grid-cols-3',
|
||||
)}
|
||||
>
|
||||
{series.map((serie) => {
|
||||
|
||||
@@ -29,10 +29,10 @@ export function ReportMetricChart() {
|
||||
export function Loading() {
|
||||
return (
|
||||
<div className="flex h-[78px] flex-col justify-between p-4">
|
||||
<div className="h-3 w-1/2 animate-pulse rounded bg-def-200"></div>
|
||||
<div className="h-3 w-1/2 animate-pulse rounded bg-def-200" />
|
||||
<div className="row items-end justify-between">
|
||||
<div className="h-6 w-1/3 animate-pulse rounded bg-def-200"></div>
|
||||
<div className="h-3 w-1/5 animate-pulse rounded bg-def-200"></div>
|
||||
<div className="h-6 w-1/3 animate-pulse rounded bg-def-200" />
|
||||
<div className="h-3 w-1/5 animate-pulse rounded bg-def-200" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,8 @@ import { Area, AreaChart } from 'recharts';
|
||||
import type { IChartMetric } from '@openpanel/validation';
|
||||
|
||||
import {
|
||||
getDiffIndicator,
|
||||
PreviousDiffIndicator,
|
||||
getDiffIndicator,
|
||||
} from '../common/previous-diff-indicator';
|
||||
import { SerieName } from '../common/serie-name';
|
||||
import { useReportChartContext } from '../context';
|
||||
@@ -54,7 +54,7 @@ export function MetricCard({
|
||||
previous?.state,
|
||||
'#6ee7b7', // green
|
||||
'#fda4af', // red
|
||||
'#93c5fd' // blue
|
||||
'#93c5fd', // blue
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -64,7 +64,7 @@ export function MetricCard({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute -left-1 -right-1 bottom-0 top-0 z-0 opacity-50 transition-opacity duration-300 group-hover:opacity-100'
|
||||
'pointer-events-none absolute -left-1 -right-1 bottom-0 top-0 z-0 opacity-50 transition-opacity duration-300 group-hover:opacity-100',
|
||||
)}
|
||||
>
|
||||
<AutoSizer>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function ReportLineType({ className }: ReportLineTypeProps) {
|
||||
const chartType = useSelector((state) => state.report.chartType);
|
||||
const type = useSelector((state) => state.report.lineType);
|
||||
|
||||
if (chartType != 'linear' && chartType != 'area') {
|
||||
if (chartType !== 'linear' && chartType !== 'area') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,20 +44,19 @@ export function ReportSaveButton({ className }: ReportSaveButtonProps) {
|
||||
Update
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
className={className}
|
||||
disabled={!report.dirty}
|
||||
onClick={() => {
|
||||
pushModal('SaveReport', {
|
||||
report,
|
||||
});
|
||||
}}
|
||||
icon={SaveIcon}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
className={className}
|
||||
disabled={!report.dirty}
|
||||
onClick={() => {
|
||||
pushModal('SaveReport', {
|
||||
report,
|
||||
});
|
||||
}}
|
||||
icon={SaveIcon}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { PencilIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
name?: string;
|
||||
@@ -23,7 +23,7 @@ const EditReportName = ({ name }: Props) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('report-name-change', {
|
||||
detail: newName === '' ? name : newName,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
setIsEditing(false);
|
||||
@@ -33,8 +33,6 @@ const EditReportName = ({ name }: Props) => {
|
||||
return (
|
||||
<div className="flex">
|
||||
<input
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
type="text"
|
||||
value={newName}
|
||||
onKeyDown={(e) => {
|
||||
@@ -53,6 +51,7 @@ const EditReportName = ({ name }: Props) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer select-none items-center gap-2"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
|
||||
@@ -100,11 +100,11 @@ export const reportSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id?: string;
|
||||
}>
|
||||
}>,
|
||||
) => {
|
||||
state.dirty = true;
|
||||
state.events = state.events.filter(
|
||||
(event) => event.id !== action.payload.id
|
||||
(event) => event.id !== action.payload.id,
|
||||
);
|
||||
},
|
||||
changeEvent: (state, action: PayloadAction<IChartEvent>) => {
|
||||
@@ -126,7 +126,7 @@ export const reportSlice = createSlice({
|
||||
// Breakdowns
|
||||
addBreakdown: (
|
||||
state,
|
||||
action: PayloadAction<Omit<IChartBreakdown, 'id'>>
|
||||
action: PayloadAction<Omit<IChartBreakdown, 'id'>>,
|
||||
) => {
|
||||
state.dirty = true;
|
||||
state.breakdowns.push({
|
||||
@@ -138,11 +138,11 @@ export const reportSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id?: string;
|
||||
}>
|
||||
}>,
|
||||
) => {
|
||||
state.dirty = true;
|
||||
state.breakdowns = state.breakdowns.filter(
|
||||
(event) => event.id !== action.payload.id
|
||||
(event) => event.id !== action.payload.id,
|
||||
);
|
||||
},
|
||||
changeBreakdown: (state, action: PayloadAction<IChartBreakdown>) => {
|
||||
@@ -193,7 +193,7 @@ export const reportSlice = createSlice({
|
||||
action: PayloadAction<{
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}>
|
||||
}>,
|
||||
) => {
|
||||
state.dirty = true;
|
||||
state.startDate = formatISO(startOfDay(action.payload.startDate));
|
||||
@@ -215,7 +215,7 @@ export const reportSlice = createSlice({
|
||||
|
||||
const interval = getDefaultIntervalByDates(
|
||||
state.startDate,
|
||||
state.endDate
|
||||
state.endDate,
|
||||
);
|
||||
if (interval) {
|
||||
state.interval = interval;
|
||||
@@ -229,7 +229,7 @@ export const reportSlice = createSlice({
|
||||
|
||||
const interval = getDefaultIntervalByDates(
|
||||
state.startDate,
|
||||
state.endDate
|
||||
state.endDate,
|
||||
);
|
||||
if (interval) {
|
||||
state.interval = interval;
|
||||
|
||||
@@ -30,7 +30,7 @@ export function EventPropertiesCombobox({
|
||||
},
|
||||
{
|
||||
enabled: !!event.name,
|
||||
}
|
||||
},
|
||||
).map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
@@ -47,14 +47,15 @@ export function EventPropertiesCombobox({
|
||||
changeEvent({
|
||||
...event,
|
||||
property: value,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex items-center gap-1 rounded-md border border-border p-1 px-2 text-sm font-medium leading-none',
|
||||
!event.property && 'border-destructive text-destructive'
|
||||
!event.property && 'border-destructive text-destructive',
|
||||
)}
|
||||
>
|
||||
<DatabaseIcon size={12} />{' '}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { MoreHorizontal, Trash } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ReportBreakdownMoreProps {
|
||||
onClick: (action: 'remove') => void;
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ReportBreakdowns() {
|
||||
changeBreakdown({
|
||||
...item,
|
||||
name: value,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}
|
||||
items={properties}
|
||||
@@ -80,7 +80,7 @@ export function ReportBreakdowns() {
|
||||
dispatch(
|
||||
addBreakdown({
|
||||
name: value,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}
|
||||
items={properties}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Command,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Filter, MoreHorizontal, Tags, Trash } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
const labels = [
|
||||
'feature',
|
||||
|
||||
@@ -21,10 +21,10 @@ import {
|
||||
removeEvent,
|
||||
} from '../reportSlice';
|
||||
import { EventPropertiesCombobox } from './EventPropertiesCombobox';
|
||||
import { FiltersCombobox } from './filters/FiltersCombobox';
|
||||
import { FiltersList } from './filters/FiltersList';
|
||||
import { ReportEventMore } from './ReportEventMore';
|
||||
import type { ReportEventMoreProps } from './ReportEventMore';
|
||||
import { FiltersCombobox } from './filters/FiltersCombobox';
|
||||
import { FiltersList } from './filters/FiltersList';
|
||||
|
||||
export function ReportEvents() {
|
||||
const previous = useSelector((state) => state.report.previous);
|
||||
@@ -79,7 +79,7 @@ export function ReportEvents() {
|
||||
...event,
|
||||
name: value,
|
||||
filters: [],
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}
|
||||
items={eventNames.map((item) => ({
|
||||
@@ -113,7 +113,7 @@ export function ReportEvents() {
|
||||
changeEvent({
|
||||
...event,
|
||||
segment,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}
|
||||
items={[
|
||||
@@ -148,7 +148,10 @@ export function ReportEvents() {
|
||||
]}
|
||||
label="Segment"
|
||||
>
|
||||
<button className="flex items-center gap-1 rounded-md border border-border bg-card p-1 px-2 text-sm font-medium leading-none">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded-md border border-border bg-card p-1 px-2 text-sm font-medium leading-none"
|
||||
>
|
||||
{event.segment === 'user' ? (
|
||||
<>
|
||||
<Users size={12} /> Unique users
|
||||
@@ -205,7 +208,7 @@ export function ReportEvents() {
|
||||
name: value,
|
||||
segment: 'event',
|
||||
filters: [],
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}
|
||||
items={eventNames.map((item) => ({
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ColorSquare } from '@/components/color-square';
|
||||
import { RenderDots } from '@/components/ui/RenderDots';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||
import { DropdownMenuComposed } from '@/components/ui/dropdown-menu';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RenderDots } from '@/components/ui/RenderDots';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { usePropertyValues } from '@/hooks/usePropertyValues';
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { RefreshCcwIcon, SlidersHorizontal, Trash } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { operators } from '@openpanel/constants';
|
||||
import type {
|
||||
@@ -41,31 +41,31 @@ interface PureFilterProps {
|
||||
onRemove: (filter: IChartEventFilter) => void;
|
||||
onChangeValue: (
|
||||
value: IChartEventFilterValue[],
|
||||
filter: IChartEventFilter
|
||||
filter: IChartEventFilter,
|
||||
) => void;
|
||||
onChangeOperator: (
|
||||
operator: IChartEventFilterOperator,
|
||||
filter: IChartEventFilter
|
||||
filter: IChartEventFilter,
|
||||
) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FilterItem({ filter, event }: FilterProps) {
|
||||
const { range, startDate, endDate, interval } = useSelector(
|
||||
(state) => state.report
|
||||
(state) => state.report,
|
||||
);
|
||||
const onRemove = ({ id }: IChartEventFilter) => {
|
||||
dispatch(
|
||||
changeEvent({
|
||||
...event,
|
||||
filters: event.filters.filter((item) => item.id !== id),
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onChangeValue = (
|
||||
value: IChartEventFilterValue[],
|
||||
{ id }: IChartEventFilter
|
||||
{ id }: IChartEventFilter,
|
||||
) => {
|
||||
dispatch(
|
||||
changeEvent({
|
||||
@@ -80,13 +80,13 @@ export function FilterItem({ filter, event }: FilterProps) {
|
||||
|
||||
return item;
|
||||
}),
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onChangeOperator = (
|
||||
operator: IChartEventFilterOperator,
|
||||
{ id }: IChartEventFilter
|
||||
{ id }: IChartEventFilter,
|
||||
) => {
|
||||
dispatch(
|
||||
changeEvent({
|
||||
@@ -102,7 +102,7 @@ export function FilterItem({ filter, event }: FilterProps) {
|
||||
|
||||
return item;
|
||||
}),
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export function FiltersCombobox({ event }: FiltersComboboxProps) {
|
||||
},
|
||||
{
|
||||
enabled: !!event.name,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -57,11 +57,14 @@ export function FiltersCombobox({ event }: FiltersComboboxProps) {
|
||||
value: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<button className="flex items-center gap-1 rounded-md border border-border bg-card p-1 px-2 text-sm font-medium leading-none">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded-md border border-border bg-card p-1 px-2 text-sm font-medium leading-none"
|
||||
>
|
||||
<FilterIcon size={12} /> Add filter
|
||||
</button>
|
||||
</Combobox>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { CheckIcon, MoreHorizontalIcon, PlusIcon } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ProjectLink } from './links';
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { toast } from 'sonner';
|
||||
import type { IServiceInvite, IServiceProject } from '@openpanel/db';
|
||||
|
||||
export function useColumns(
|
||||
projects: IServiceProject[]
|
||||
projects: IServiceProject[],
|
||||
): ColumnDef<IServiceInvite>[] {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -21,11 +21,11 @@ export const InvitesTable = ({ projects, data }: Props) => {
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
import { TooltipComplete } from '@/components/tooltip-complete';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||
@@ -12,6 +11,7 @@ import { api } from '@/trpc/client';
|
||||
import type { ColumnDef, Row } from '@tanstack/react-table';
|
||||
import { MoreHorizontalIcon } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { IServiceMember, IServiceProject } from '@openpanel/db';
|
||||
@@ -77,7 +77,7 @@ function AccessCell({
|
||||
projects: IServiceProject[];
|
||||
}) {
|
||||
const [access, setAccess] = useState<string[]>(
|
||||
row.original.access.map((item) => item.projectId)
|
||||
row.original.access.map((item) => item.projectId),
|
||||
);
|
||||
const mutation = api.organization.updateMemberAccess.useMutation();
|
||||
|
||||
@@ -106,13 +106,13 @@ function ActionsCell({ row }: { row: Row<IServiceMember> }) {
|
||||
const revoke = api.organization.removeMember.useMutation({
|
||||
onSuccess() {
|
||||
toast.success(
|
||||
`${row.original.user?.firstName} has been removed from the organization`
|
||||
`${row.original.user?.firstName} has been removed from the organization`,
|
||||
);
|
||||
router.refresh();
|
||||
},
|
||||
onError() {
|
||||
toast.error(
|
||||
`Failed to remove ${row.original.user?.firstName} from the organization`
|
||||
`Failed to remove ${row.original.user?.firstName} from the organization`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,11 +21,11 @@ export const MembersTable = ({ projects, data }: Props) => {
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4"></div>
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
<div className="card h-[74px] w-full animate-pulse items-center justify-between rounded-lg p-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function Stats({
|
||||
<div
|
||||
className={cn(
|
||||
'grid overflow-hidden rounded border bg-background @xl:grid-cols-3 @4xl:grid-cols-6',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function Syntax({ code }: SyntaxProps) {
|
||||
return (
|
||||
<div className="group relative">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-1 top-1 rounded bg-card p-2 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
clipboard(code);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -14,6 +13,7 @@ import { pushModal, useOnPushModal } from '@/modals';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { bind } from 'bind-event-listener';
|
||||
import { CalendarIcon } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { timeWindows } from '@openpanel/constants';
|
||||
import type { IChartRange } from '@openpanel/validation';
|
||||
@@ -21,9 +21,9 @@ import type { IChartRange } from '@openpanel/validation';
|
||||
function shouldIgnoreKeypress(event: KeyboardEvent) {
|
||||
const tagName = (event?.target as HTMLElement)?.tagName;
|
||||
const modifierPressed =
|
||||
event.ctrlKey || event.metaKey || event.altKey || event.keyCode == 229;
|
||||
event.ctrlKey || event.metaKey || event.altKey || event.keyCode === 229;
|
||||
const isTyping =
|
||||
event.isComposing || tagName == 'INPUT' || tagName == 'TEXTAREA';
|
||||
event.isComposing || tagName === 'INPUT' || tagName === 'TEXTAREA';
|
||||
|
||||
return modifierPressed || isTyping;
|
||||
}
|
||||
@@ -47,10 +47,9 @@ export function TimeWindowPicker({
|
||||
className,
|
||||
}: Props) {
|
||||
const isDateRangerPickerOpen = useRef(false);
|
||||
useOnPushModal(
|
||||
'DateRangerPicker',
|
||||
(open) => (isDateRangerPickerOpen.current = open)
|
||||
);
|
||||
useOnPushModal('DateRangerPicker', (open) => {
|
||||
isDateRangerPickerOpen.current = open;
|
||||
});
|
||||
const timeWindow = timeWindows[value ?? '30d'];
|
||||
|
||||
const handleCustom = useCallback(() => {
|
||||
@@ -78,7 +77,7 @@ export function TimeWindowPicker({
|
||||
}
|
||||
|
||||
const match = Object.values(timeWindows).find(
|
||||
(tw) => event.key === tw.shortcut.toLowerCase()
|
||||
(tw) => event.key === tw.shortcut.toLowerCase(),
|
||||
);
|
||||
if (match?.key === 'custom') {
|
||||
handleCustom();
|
||||
|
||||
@@ -27,7 +27,10 @@ export function RenderDots({
|
||||
<div {...props} className={cn('flex items-center gap-1', className)}>
|
||||
{parts.slice(-sliceAt).map((str, index) => {
|
||||
return (
|
||||
<div className="flex items-center gap-1" key={str + index}>
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
key={str + (index as number)}
|
||||
>
|
||||
{index !== 0 && (
|
||||
<ChevronRight className="relative top-[0.9px] !h-3 !w-3 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
@@ -26,7 +26,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all [&[data-state=closed]]:hover:bg-muted/30 [&[data-state=open]>svg]:rotate-180',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
import * as React from 'react';
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root;
|
||||
|
||||
@@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-card/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
@@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -51,7 +51,7 @@ const AlertDialogHeader = ({
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -65,7 +65,7 @@ const AlertDialogFooter = ({
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -118,7 +118,7 @@ const AlertDialogCancel = React.forwardRef<
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'mt-2 sm:mt-0',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
|
||||
@@ -16,7 +16,7 @@ const alertVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
import * as React from 'react';
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
@@ -12,7 +12,7 @@ const Avatar = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full ',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -39,7 +39,7 @@ const AvatarFallback = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full items-center justify-center rounded-full bg-primary text-white',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import type * as React from 'react';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex h-[20px] items-center rounded-full border px-2 text-sm font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
@@ -25,7 +25,7 @@ const badgeVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva } from 'class-variance-authority';
|
||||
@@ -8,6 +7,7 @@ import type { VariantProps } from 'class-variance-authority';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import * as React from 'react';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
@@ -35,7 +35,7 @@ const buttonVariants = cva(
|
||||
variant: 'default',
|
||||
size: 'sm',
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
@@ -61,10 +61,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
responsive,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
const Icon = loading ? Loader2 : icon ?? null;
|
||||
const Icon = loading ? Loader2 : (icon ?? null);
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
@@ -78,7 +78,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
'h-4 w-4 flex-shrink-0',
|
||||
loading && 'animate-spin',
|
||||
size !== 'icon' && responsive && 'mr-0 sm:mr-2',
|
||||
size !== 'icon' && !responsive && 'mr-2'
|
||||
size !== 'icon' && !responsive && 'mr-2',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -89,7 +89,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
Button.defaultProps = {
|
||||
@@ -122,9 +122,9 @@ const LinkButton = React.forwardRef<
|
||||
href,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const Icon = loading ? Loader2 : icon ?? null;
|
||||
const Icon = loading ? Loader2 : (icon ?? null);
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
@@ -138,7 +138,7 @@ const LinkButton = React.forwardRef<
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4 flex-shrink-0',
|
||||
responsive && 'mr-0 sm:mr-2',
|
||||
loading && 'animate-spin'
|
||||
loading && 'animate-spin',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -149,7 +149,7 @@ const LinkButton = React.forwardRef<
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
LinkButton.displayName = 'LinkButton';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import type * as React from 'react';
|
||||
import { DayPicker } from 'react-day-picker';
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
@@ -24,7 +24,7 @@ function Calendar({
|
||||
nav: 'space-x-1 flex items-center',
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
),
|
||||
nav_button_previous: 'absolute left-1',
|
||||
nav_button_next: 'absolute right-1',
|
||||
@@ -36,7 +36,7 @@ function Calendar({
|
||||
cell: 'h-9 w-9 text-center 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',
|
||||
day: cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-9 w-9 p-0 font-normal aria-selected:opacity-100'
|
||||
'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
|
||||
),
|
||||
day_range_end: 'day-range-end',
|
||||
day_selected:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import useEmblaCarousel from 'embla-carousel-react';
|
||||
import type { UseEmblaCarouselType } from 'embla-carousel-react';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
@@ -52,14 +52,14 @@ const Carousel = React.forwardRef<
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y',
|
||||
},
|
||||
plugins
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
@@ -91,7 +91,7 @@ const Carousel = React.forwardRef<
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext]
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -142,7 +142,7 @@ const Carousel = React.forwardRef<
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Carousel.displayName = 'Carousel';
|
||||
|
||||
@@ -159,7 +159,7 @@ const CarouselContent = React.forwardRef<
|
||||
className={cn(
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '' : 'flex-col',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -182,7 +182,7 @@ const CarouselItem = React.forwardRef<
|
||||
className={cn(
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -206,7 +206,7 @@ const CarouselPrevious = React.forwardRef<
|
||||
orientation === 'horizontal'
|
||||
? 'left-6 top-1/2 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
@@ -235,7 +235,7 @@ const CarouselNext = React.forwardRef<
|
||||
orientation === 'horizontal'
|
||||
? 'right-6 top-1/2 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import { Check } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
export const DumpCheckbox = ({ checked }: { checked: boolean }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'block h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
checked && 'bg-primary text-primary-foreground'
|
||||
checked && 'bg-primary text-primary-foreground',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-center text-current">
|
||||
@@ -28,7 +28,7 @@ const Checkbox = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'block h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -48,7 +48,7 @@ const CheckboxInput = React.forwardRef<
|
||||
<label
|
||||
className={cn(
|
||||
'flex min-h-10 cursor-pointer select-none gap-4 rounded-md border border-border px-3 py-[0.5rem]',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Checkbox ref={ref} {...props} className="relative top-0.5" />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Command, CommandInput, CommandItem } from '@/components/ui/command';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||
import VirtualList from 'rc-virtual-list';
|
||||
import * as React from 'react';
|
||||
import { useOnClickOutside } from 'usehooks-ts';
|
||||
|
||||
import { Button } from './button';
|
||||
@@ -40,7 +40,7 @@ export function ComboboxAdvanced({
|
||||
(typeof item.label === 'string' &&
|
||||
item.label.toLowerCase().includes(inputValue.toLowerCase())) ||
|
||||
(typeof item.value === 'string' &&
|
||||
item.value.toLowerCase().includes(inputValue.toLowerCase()))
|
||||
item.value.toLowerCase().includes(inputValue.toLowerCase())),
|
||||
);
|
||||
|
||||
const renderItem = (item: IItem) => {
|
||||
@@ -56,7 +56,7 @@ export function ComboboxAdvanced({
|
||||
onChange(
|
||||
value.includes(item.value)
|
||||
? value.filter((s) => s !== item.value)
|
||||
: [...value, item.value]
|
||||
: [...value, item.value],
|
||||
);
|
||||
}}
|
||||
className={'flex cursor-pointer items-center gap-2'}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import type { ButtonProps } from '@/components/ui/button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -18,6 +17,7 @@ import { cn } from '@/utils/cn';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
||||
import VirtualList from 'rc-virtual-list';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ComboboxProps<T> {
|
||||
placeholder: string;
|
||||
@@ -66,7 +66,7 @@ export function Combobox<T extends string>({
|
||||
const [search, setSearch] = React.useState('');
|
||||
function find(value: string) {
|
||||
return items.find(
|
||||
(item) => item.value.toLowerCase() === value.toLowerCase()
|
||||
(item) => item.value.toLowerCase() === value.toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,13 +82,13 @@ export function Combobox<T extends string>({
|
||||
className={cn(
|
||||
'justify-between',
|
||||
!!error && 'border-destructive',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center">
|
||||
{Icon ? <Icon size={16} className="mr-2 shrink-0" /> : null}
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{value ? find(value)?.label ?? 'No match' : placeholder}
|
||||
{value ? (find(value)?.label ?? 'No match') : placeholder}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
@@ -147,7 +147,7 @@ export function Combobox<T extends string>({
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4 flex-shrink-0',
|
||||
value === item.value ? 'opacity-100' : 'opacity-0'
|
||||
value === item.value ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{item.label}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { DialogProps } from '@radix-ui/react-dialog';
|
||||
import { Command as CommandPrimitive } from 'cmdk';
|
||||
import { Search } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
@@ -15,7 +15,7 @@ const Command = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -46,7 +46,7 @@ const CommandInput = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-11 w-full rounded-md bg-transparent py-3 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -85,7 +85,7 @@ const CommandGroup = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -113,7 +113,7 @@ const CommandItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
data-disabled={props.disabled}
|
||||
{...props}
|
||||
@@ -130,7 +130,7 @@ const CommandShortcut = ({
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto text-sm tracking-widest text-muted-foreground',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { X } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/20 backdrop-blur-[1px] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -42,7 +42,7 @@ const DialogContent = React.forwardRef<
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full',
|
||||
className,
|
||||
'max-h-screen overflow-y-auto', // Ensure the dialog is scrollable if it exceeds the screen height
|
||||
'mt-auto' // Add margin-top: auto for all screen sizes
|
||||
'mt-auto', // Add margin-top: auto for all screen sizes
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -59,7 +59,7 @@ const DialogHeader = ({
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -73,7 +73,7 @@ const DialogFooter = ({
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -88,7 +88,7 @@ const DialogTitle = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { Check, ChevronRight, Circle } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
@@ -28,7 +28,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
className={cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 outline-none focus:bg-accent data-[state=open]:bg-accent',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -84,7 +84,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
className={cn(
|
||||
'relative flex min-h-8 cursor-pointer select-none items-center rounded-sm px-2 py-1.5 outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:mr-2',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -99,7 +99,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
@@ -123,7 +123,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -171,7 +171,7 @@ const DropdownMenuShortcut = ({
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto font-mono text-sm tracking-widest opacity-60',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function GradientBackground({
|
||||
<div
|
||||
className={cn(
|
||||
'from-def-200 rounded-md bg-gradient-to-tr to-white',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||
import { Dot } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
@@ -11,7 +11,7 @@ const InputOTP = React.forwardRef<
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
'flex items-center gap-2 has-[:disabled]:opacity-50',
|
||||
containerClassName
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn('disabled:cursor-not-allowed', className)}
|
||||
{...props}
|
||||
@@ -40,7 +40,7 @@ const InputOTPSlot = React.forwardRef<
|
||||
className={cn(
|
||||
'relative flex h-10 w-10 items-center justify-center border-y border-r border-input transition-all first:rounded-l-md first:border-l last:rounded-r-md',
|
||||
isActive && 'z-10 ring-2 ring-ring ring-offset-background',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
|
||||
const inputVariant = cva(
|
||||
'file: flex w-full rounded-md border border-input bg-card ring-offset-background file:border-0 file:bg-transparent file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
@@ -18,7 +18,7 @@ const inputVariant = cva(
|
||||
defaultVariants: {
|
||||
size: 'sm',
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export type InputProps = VariantProps<typeof inputVariant> &
|
||||
@@ -35,13 +35,13 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
type={type}
|
||||
className={cn(
|
||||
inputVariant({ size, className }),
|
||||
!!error && 'border-destructive'
|
||||
!!error && 'border-destructive',
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
|
||||
<Component
|
||||
className={cn(
|
||||
'group flex min-w-0 max-w-full divide-x self-start overflow-hidden rounded-md border border-border text-sm font-medium transition-transform',
|
||||
clickable && 'hover:-translate-y-0.5'
|
||||
clickable && 'hover:-translate-y-0.5',
|
||||
)}
|
||||
{...{ href, onClick }}
|
||||
>
|
||||
@@ -24,7 +24,7 @@ export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
|
||||
<div
|
||||
className={cn(
|
||||
'font-mono overflow-hidden text-ellipsis whitespace-nowrap bg-card p-1 px-2 text-highlight',
|
||||
clickable && 'group-hover:underline'
|
||||
clickable && 'group-hover:underline',
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user