- {data.series.reduce((acc, serie) => serie.metrics.total + acc, 0)}
+
+
+
Total
+
{data.metrics.sum}
-
Average
-
- {data.series.reduce((acc, serie) => serie.metrics.average + acc, 0)}
+
+
Average
+
{data.metrics.averge}
+
+
+
Min
+
{data.metrics.min}
+
+
+
Max
+
{data.metrics.max}
>
diff --git a/apps/web/src/components/report/chart/chart-utils.ts b/apps/web/src/components/report/chart/chart-utils.ts
new file mode 100644
index 00000000..cce56acf
--- /dev/null
+++ b/apps/web/src/components/report/chart/chart-utils.ts
@@ -0,0 +1,5 @@
+import { round } from '@/utils/math';
+
+export function getYAxisWidth(value: number) {
+ return round(value, 0).toString().length * 7.5 + 7.5;
+}
diff --git a/apps/web/src/components/report/chart/index.tsx b/apps/web/src/components/report/chart/index.tsx
index 31c5cef4..4fef6605 100644
--- a/apps/web/src/components/report/chart/index.tsx
+++ b/apps/web/src/components/report/chart/index.tsx
@@ -1,13 +1,18 @@
+'use client';
+
import { memo } from 'react';
-import { useOrganizationParams } from '@/hooks/useOrganizationParams';
+import { api } from '@/app/_trpc/client';
+import { useAppParams } from '@/hooks/useAppParams';
import type { IChartInput } from '@/types';
-import { api } from '@/utils/api';
import { ChartAnimation, ChartAnimationContainer } from './ChartAnimation';
import { withChartProivder } from './ChartProvider';
+import { ReportAreaChart } from './ReportAreaChart';
import { ReportBarChart } from './ReportBarChart';
import { ReportHistogramChart } from './ReportHistogramChart';
import { ReportLineChart } from './ReportLineChart';
+import { ReportMetricChart } from './ReportMetricChart';
+import { ReportPieChart } from './ReportPieChart';
export type ReportChartProps = IChartInput;
@@ -19,8 +24,9 @@ export const Chart = memo(
chartType,
name,
range,
+ lineType,
}: ReportChartProps) {
- const params = useOrganizationParams();
+ const params = useAppParams();
const hasEmptyFilters = events.some((event) =>
event.filters.some((filter) => filter.value.length === 0)
);
@@ -29,13 +35,15 @@ export const Chart = memo(
{
interval,
chartType,
+ // dont send lineType since it does not need to be sent
+ lineType: 'monotone',
events,
breakdowns,
name,
range,
startDate: null,
endDate: null,
- projectSlug: params.project,
+ projectId: params.projectId,
},
{
keepPreviousData: false,
@@ -97,8 +105,32 @@ export const Chart = memo(
return
;
}
+ if (chartType === 'metric') {
+ return
;
+ }
+
+ if (chartType === 'pie') {
+ return
;
+ }
+
if (chartType === 'linear') {
- return
;
+ return (
+
+ );
+ }
+
+ if (chartType === 'area') {
+ return (
+
+ );
}
return (
diff --git a/apps/web/src/components/report/hooks/useReportId.ts b/apps/web/src/components/report/hooks/useReportId.ts
deleted file mode 100644
index 434e101c..00000000
--- a/apps/web/src/components/report/hooks/useReportId.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { useQueryParams } from '@/hooks/useQueryParams';
-import { z } from 'zod';
-
-export const useReportId = () =>
- useQueryParams(
- z.object({
- reportId: z.string().optional(),
- })
- );
diff --git a/apps/web/src/components/report/reportSlice.ts b/apps/web/src/components/report/reportSlice.ts
index cd883e73..63cc802f 100644
--- a/apps/web/src/components/report/reportSlice.ts
+++ b/apps/web/src/components/report/reportSlice.ts
@@ -2,25 +2,34 @@ import type {
IChartBreakdown,
IChartEvent,
IChartInput,
+ IChartLineType,
IChartRange,
IChartType,
IInterval,
} from '@/types';
-import { alphabetIds, isMinuteIntervalEnabledByRange } from '@/utils/constants';
+import {
+ alphabetIds,
+ getDefaultIntervalByRange,
+ isHourIntervalEnabledByRange,
+ isMinuteIntervalEnabledByRange,
+} from '@/utils/constants';
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
type InitialState = IChartInput & {
dirty: boolean;
+ ready: boolean;
startDate: string | null;
endDate: string | null;
};
// First approach: define the initial state using that type
const initialState: InitialState = {
+ ready: false,
dirty: false,
name: 'Untitled',
chartType: 'linear',
+ lineType: 'monotone',
interval: 'day',
breakdowns: [],
events: [],
@@ -42,12 +51,19 @@ export const reportSlice = createSlice({
reset() {
return initialState;
},
+ ready() {
+ return {
+ ...initialState,
+ ready: true,
+ };
+ },
setReport(state, action: PayloadAction
) {
return {
...action.payload,
startDate: null,
endDate: null,
dirty: false,
+ ready: true,
};
},
setName(state, action: PayloadAction) {
@@ -132,6 +148,19 @@ export const reportSlice = createSlice({
) {
state.interval = 'hour';
}
+
+ if (
+ !isHourIntervalEnabledByRange(state.range) &&
+ state.interval === 'hour'
+ ) {
+ state.interval = 'day';
+ }
+ },
+
+ // Line type
+ changeLineType: (state, action: PayloadAction) => {
+ state.dirty = true;
+ state.lineType = action.payload;
},
// Date range
@@ -149,15 +178,7 @@ export const reportSlice = createSlice({
changeDateRanges: (state, action: PayloadAction) => {
state.dirty = true;
state.range = action.payload;
- if (action.payload === '30min' || action.payload === '1h') {
- state.interval = 'minute';
- } else if (action.payload === 'today' || action.payload === '24h') {
- state.interval = 'hour';
- } else if (action.payload === '7d' || action.payload === '14d') {
- state.interval = 'day';
- } else {
- state.interval = 'month';
- }
+ state.interval = getDefaultIntervalByRange(action.payload);
},
},
});
@@ -165,6 +186,7 @@ export const reportSlice = createSlice({
// Action creators are generated for each case reducer function
export const {
reset,
+ ready,
setReport,
setName,
addEvent,
@@ -176,6 +198,7 @@ export const {
changeInterval,
changeDateRanges,
changeChartType,
+ changeLineType,
resetDirty,
} = reportSlice.actions;
diff --git a/apps/web/src/components/report/sidebar/ReportBreakdowns.tsx b/apps/web/src/components/report/sidebar/ReportBreakdowns.tsx
index 9c3275f0..9d0e77ea 100644
--- a/apps/web/src/components/report/sidebar/ReportBreakdowns.tsx
+++ b/apps/web/src/components/report/sidebar/ReportBreakdowns.tsx
@@ -1,20 +1,22 @@
+'use client';
+
+import { api } from '@/app/_trpc/client';
import { ColorSquare } from '@/components/ColorSquare';
import { Combobox } from '@/components/ui/combobox';
-import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useDispatch, useSelector } from '@/redux';
import type { IChartBreakdown } from '@/types';
-import { api } from '@/utils/api';
+import { useParams } from 'next/navigation';
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
import { ReportBreakdownMore } from './ReportBreakdownMore';
import type { ReportEventMoreProps } from './ReportEventMore';
export function ReportBreakdowns() {
- const params = useOrganizationParams();
+ const params = useParams();
const selectedBreakdowns = useSelector((state) => state.report.breakdowns);
const dispatch = useDispatch();
const propertiesQuery = api.chart.properties.useQuery({
- projectSlug: params.project,
+ projectId: params.projectId as string,
});
const propertiesCombobox = (propertiesQuery.data ?? []).map((item) => ({
value: item,
@@ -43,6 +45,8 @@ export function ReportBreakdowns() {
{index}
{
dispatch(
@@ -63,6 +67,7 @@ export function ReportBreakdowns() {
{selectedBreakdowns.length === 0 && (
{
dispatch(
diff --git a/apps/web/src/components/report/sidebar/ReportEventFilters.tsx b/apps/web/src/components/report/sidebar/ReportEventFilters.tsx
index 15c1bf73..962eb6da 100644
--- a/apps/web/src/components/report/sidebar/ReportEventFilters.tsx
+++ b/apps/web/src/components/report/sidebar/ReportEventFilters.tsx
@@ -1,9 +1,9 @@
import type { Dispatch } from 'react';
+import { api } from '@/app/_trpc/client';
import { ColorSquare } from '@/components/ColorSquare';
import { Dropdown } from '@/components/Dropdown';
import { Button } from '@/components/ui/button';
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
-import { ComboboxMulti } from '@/components/ui/combobox-multi';
import {
CommandDialog,
CommandEmpty,
@@ -15,16 +15,15 @@ import {
} from '@/components/ui/command';
import { RenderDots } from '@/components/ui/RenderDots';
import { useMappings } from '@/hooks/useMappings';
-import { useOrganizationParams } from '@/hooks/useOrganizationParams';
-import { useDispatch, useSelector } from '@/redux';
+import { useDispatch } from '@/redux';
import type {
IChartEvent,
IChartEventFilter,
IChartEventFilterValue,
} from '@/types';
-import { api } from '@/utils/api';
import { operators } from '@/utils/constants';
import { CreditCard, SlidersHorizontal, Trash } from 'lucide-react';
+import { useParams } from 'next/navigation';
import { changeEvent } from '../reportSlice';
@@ -39,12 +38,12 @@ export function ReportEventFilters({
isCreating,
setIsCreating,
}: ReportEventFiltersProps) {
- const params = useOrganizationParams();
+ const params = useParams();
const dispatch = useDispatch();
const propertiesQuery = api.chart.properties.useQuery(
{
event: event.name,
- projectSlug: params.project,
+ projectId: params.projectId as string,
},
{
enabled: !!event.name,
@@ -103,13 +102,13 @@ interface FilterProps {
}
function Filter({ filter, event }: FilterProps) {
- const params = useOrganizationParams();
+ const params = useParams<{ organizationId: string; projectId: string }>();
const getLabel = useMappings();
const dispatch = useDispatch();
const potentialValues = api.chart.values.useQuery({
event: event.name,
property: filter.name,
- projectSlug: params.project,
+ projectId: params?.projectId!,
});
const valuesCombobox =
@@ -196,8 +195,8 @@ function Filter({ filter, event }: FilterProps) {
{
+ value={filter.value}
+ onChange={(setFn) => {
changeFilterValue(
typeof setFn === 'function' ? setFn(filter.value) : setFn
);
diff --git a/apps/web/src/components/report/sidebar/ReportEvents.tsx b/apps/web/src/components/report/sidebar/ReportEvents.tsx
index 9630ba78..ebf46f06 100644
--- a/apps/web/src/components/report/sidebar/ReportEvents.tsx
+++ b/apps/web/src/components/report/sidebar/ReportEvents.tsx
@@ -1,14 +1,16 @@
+'use client';
+
import { useState } from 'react';
+import { api } from '@/app/_trpc/client';
import { ColorSquare } from '@/components/ColorSquare';
import { Dropdown } from '@/components/Dropdown';
import { Combobox } from '@/components/ui/combobox';
import { Input } from '@/components/ui/input';
import { useDebounceFn } from '@/hooks/useDebounceFn';
-import { useOrganizationParams } from '@/hooks/useOrganizationParams';
import { useDispatch, useSelector } from '@/redux';
import type { IChartEvent } from '@/types';
-import { api } from '@/utils/api';
import { Filter, GanttChart, Users } from 'lucide-react';
+import { useParams } from 'next/navigation';
import { addEvent, changeEvent, removeEvent } from '../reportSlice';
import { ReportEventFilters } from './ReportEventFilters';
@@ -19,9 +21,9 @@ export function ReportEvents() {
const [isCreating, setIsCreating] = useState(false);
const selectedEvents = useSelector((state) => state.report.events);
const dispatch = useDispatch();
- const params = useOrganizationParams();
+ const params = useParams();
const eventsQuery = api.chart.events.useQuery({
- projectSlug: params.project,
+ projectId: String(params.projectId),
});
const eventsCombobox = (eventsQuery.data ?? []).map((item) => ({
value: item.name,
@@ -56,6 +58,8 @@ export function ReportEvents() {
{event.id}
{
dispatch(
diff --git a/apps/web/src/components/ui/RenderDots.tsx b/apps/web/src/components/ui/RenderDots.tsx
index 8542aec9..267c42e7 100644
--- a/apps/web/src/components/ui/RenderDots.tsx
+++ b/apps/web/src/components/ui/RenderDots.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import { cn } from '@/utils/cn';
import { Asterisk, ChevronRight } from 'lucide-react';
diff --git a/apps/web/src/components/ui/alert-dialog.tsx b/apps/web/src/components/ui/alert-dialog.tsx
index bc4751da..463dc1e2 100644
--- a/apps/web/src/components/ui/alert-dialog.tsx
+++ b/apps/web/src/components/ui/alert-dialog.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import * as React from 'react';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/utils/cn';
diff --git a/apps/web/src/components/ui/alert.tsx b/apps/web/src/components/ui/alert.tsx
new file mode 100644
index 00000000..27093d2a
--- /dev/null
+++ b/apps/web/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import { cn } from '@/utils/cn';
+import { cva } from 'class-variance-authority';
+import type { VariantProps } from 'class-variance-authority';
+
+const alertVariants = cva(
+ 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
+ {
+ variants: {
+ variant: {
+ default: 'bg-background text-foreground',
+ destructive:
+ 'border-destructive text-destructive dark:border-destructive [&>svg]:text-destructive',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = 'Alert';
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = 'AlertTitle';
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = 'AlertDescription';
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/apps/web/src/components/ui/aspect-ratio.tsx b/apps/web/src/components/ui/aspect-ratio.tsx
index 5dfdf1e6..aaabffbc 100644
--- a/apps/web/src/components/ui/aspect-ratio.tsx
+++ b/apps/web/src/components/ui/aspect-ratio.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
const AspectRatio = AspectRatioPrimitive.Root;
diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx
index 24042045..2817f1c6 100644
--- a/apps/web/src/components/ui/avatar.tsx
+++ b/apps/web/src/components/ui/avatar.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx
index 79b75eef..c8df0653 100644
--- a/apps/web/src/components/ui/badge.tsx
+++ b/apps/web/src/components/ui/badge.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import * as React from 'react';
import { cn } from '@/utils/cn';
import { cva } from 'class-variance-authority';
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
index dad1731a..5e4f5e59 100644
--- a/apps/web/src/components/ui/button.tsx
+++ b/apps/web/src/components/ui/button.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import * as React from 'react';
import { cn } from '@/utils/cn';
import { Slot } from '@radix-ui/react-slot';
@@ -7,11 +9,12 @@ import type { LucideIcon } from 'lucide-react';
import { Loader2 } from 'lucide-react';
const buttonVariants = cva(
- 'inline-flex items-center justify-center rounded-md text-sm 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',
+ 'flex-shrink-0 inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background whitespace-nowrap 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',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ cta: 'bg-blue-600 text-primary-foreground hover:bg-blue-500',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
diff --git a/apps/web/src/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx
index 0e908eb9..f5253de3 100644
--- a/apps/web/src/components/ui/checkbox.tsx
+++ b/apps/web/src/components/ui/checkbox.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
diff --git a/apps/web/src/components/ui/combobox-advanced.tsx b/apps/web/src/components/ui/combobox-advanced.tsx
index c9684f78..8a608580 100644
--- a/apps/web/src/components/ui/combobox-advanced.tsx
+++ b/apps/web/src/components/ui/combobox-advanced.tsx
@@ -1,6 +1,9 @@
+'use client';
+
import * as React from 'react';
import { Badge } from '@/components/ui/badge';
import { Command, CommandGroup, CommandItem } from '@/components/ui/command';
+import { useOnClickOutside } from 'usehooks-ts';
import { Checkbox } from './checkbox';
import { Input } from './input';
@@ -9,23 +12,25 @@ type IValue = any;
type IItem = Record<'value' | 'label', IValue>;
interface ComboboxAdvancedProps {
- selected: IValue[];
- setSelected: React.Dispatch>;
+ value: IValue[];
+ onChange: React.Dispatch>;
items: IItem[];
placeholder: string;
}
export function ComboboxAdvanced({
items,
- selected,
- setSelected,
+ value,
+ onChange,
placeholder,
}: ComboboxAdvancedProps) {
const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState('');
+ const ref = React.useRef(null);
+ useOnClickOutside(ref, () => setOpen(false));
const selectables = items
- .filter((item) => !selected.find((s) => s === item.value))
+ .filter((item) => !value.find((s) => s === item.value))
.filter(
(item) =>
(typeof item.label === 'string' &&
@@ -35,7 +40,7 @@ export function ComboboxAdvanced({
);
const renderItem = (item: IItem) => {
- const checked = !!selected.find((s) => s === item.value);
+ const checked = !!value.find((s) => s === item.value);
return (
{
setInputValue('');
- setSelected((prev) => {
+ onChange((prev) => {
if (prev.includes(item.value)) {
return prev.filter((s) => s !== item.value);
}
@@ -66,15 +71,15 @@ export function ComboboxAdvanced({
};
return (
-
+
setOpen((prev) => !prev)}
>
- {selected.length === 0 && placeholder}
- {selected.slice(0, 2).map((value) => {
+ {value.length === 0 && placeholder}
+ {value.slice(0, 2).map((value) => {
const item = items.find((item) => item.value === value) ?? {
value,
label: value,
@@ -85,13 +90,13 @@ export function ComboboxAdvanced({
);
})}
- {selected.length > 2 && (
- +{selected.length - 2} more
+ {value.length > 2 && (
+ +{value.length - 2} more
)}
-
- {open && (
+ {open && (
+
@@ -102,13 +107,16 @@ export function ComboboxAdvanced({
/>
{inputValue === ''
- ? selected.map(renderUnknownItem)
- : renderUnknownItem(inputValue)}
+ ? value.map(renderUnknownItem)
+ : renderItem({
+ value: inputValue,
+ label: `Pick "${inputValue}"`,
+ })}
{selectables.map(renderItem)}
- )}
-
+
+ )}
);
}
diff --git a/apps/web/src/components/ui/combobox-multi.tsx b/apps/web/src/components/ui/combobox-multi.tsx
index c6ddaadc..34a132a9 100644
--- a/apps/web/src/components/ui/combobox-multi.tsx
+++ b/apps/web/src/components/ui/combobox-multi.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import * as React from 'react';
import { Badge } from '@/components/ui/badge';
import { Command, CommandGroup, CommandItem } from '@/components/ui/command';
diff --git a/apps/web/src/components/ui/combobox.tsx b/apps/web/src/components/ui/combobox.tsx
index 1c6fcf18..706d51a6 100644
--- a/apps/web/src/components/ui/combobox.tsx
+++ b/apps/web/src/components/ui/combobox.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
@@ -15,29 +17,31 @@ import {
import { cn } from '@/utils/cn';
import { Check, ChevronsUpDown } from 'lucide-react';
-import { ScrollArea } from './scroll-area';
-
-interface ComboboxProps {
+interface ComboboxProps {
placeholder: string;
items: {
- value: string;
+ value: T;
label: string;
disabled?: boolean;
}[];
- value: string;
- onChange: (value: string) => void;
+ value: T | null | undefined;
+ onChange: (value: T) => void;
children?: React.ReactNode;
- onCreate?: (value: string) => void;
+ onCreate?: (value: T) => void;
+ className?: string;
+ searchable?: boolean;
}
-export function Combobox({
+export function Combobox({
placeholder,
items,
value,
onChange,
children,
onCreate,
-}: ComboboxProps) {
+ className,
+ searchable,
+}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState('');
function find(value: string) {
@@ -54,9 +58,9 @@ export function Combobox({
variant="outline"
role="combobox"
aria-expanded={open}
- className="w-full justify-between min-w-[150px]"
+ className={cn('justify-between min-w-[150px]', className)}
>
-
+
{value ? find(value)?.label ?? 'No match' : placeholder}
@@ -65,11 +69,13 @@ export function Combobox({
-
+ {searchable === true && (
+
+ )}
{typeof onCreate === 'function' && search ? (
Nothing selected
)}
-
+
{items.map((item) => (
;
+export type InputProps = React.InputHTMLAttributes & {
+ error?: string | undefined;
+};
const Input = React.forwardRef(
- ({ className, type, ...props }, ref) => {
+ ({ className, error, type, ...props }, ref) => {
return (
,
@@ -9,7 +10,7 @@ const ScrollArea = React.forwardRef<
>(({ className, children, ...props }, ref) => (
@@ -18,34 +19,34 @@ const ScrollArea = React.forwardRef<
-))
-ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
->(({ className, orientation = "vertical", ...props }, ref) => (
+>(({ className, orientation = 'vertical', ...props }, ref) => (
-))
-ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
-export { ScrollArea, ScrollBar }
+export { ScrollArea, ScrollBar };
diff --git a/apps/web/src/components/ui/sheet.tsx b/apps/web/src/components/ui/sheet.tsx
index b2c7032e..0e9cc931 100644
--- a/apps/web/src/components/ui/sheet.tsx
+++ b/apps/web/src/components/ui/sheet.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import * as React from 'react';
import { cn } from '@/utils/cn';
import * as SheetPrimitive from '@radix-ui/react-dialog';
diff --git a/apps/web/src/components/ui/table.tsx b/apps/web/src/components/ui/table.tsx
index 4e67313f..df94c8a6 100644
--- a/apps/web/src/components/ui/table.tsx
+++ b/apps/web/src/components/ui/table.tsx
@@ -1,11 +1,13 @@
+'use client';
+
import * as React from 'react';
import { cn } from '@/utils/cn';
const Table = React.forwardRef<
HTMLTableElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
+ React.HTMLAttributes
& { wrapper?: boolean }
+>(({ className, wrapper, ...props }, ref) => (
+
;
+export type Toast = Omit;
function toast({ ...props }: Toast) {
const id = genId();
diff --git a/apps/web/src/components/user/ChangePassword.tsx b/apps/web/src/components/user/ChangePassword.tsx
index a2045cdb..0230bcea 100644
--- a/apps/web/src/components/user/ChangePassword.tsx
+++ b/apps/web/src/components/user/ChangePassword.tsx
@@ -1,8 +1,8 @@
+import { api, handleError } from '@/app/_trpc/client';
import { ContentHeader, ContentSection } from '@/components/Content';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { toast } from '@/components/ui/use-toast';
-import { api, handleError } from '@/utils/api';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
diff --git a/apps/web/src/hooks/useAppParams.ts b/apps/web/src/hooks/useAppParams.ts
new file mode 100644
index 00000000..805ac9d6
--- /dev/null
+++ b/apps/web/src/hooks/useAppParams.ts
@@ -0,0 +1,16 @@
+import { useParams } from 'next/navigation';
+
+// eslint-disable-next-line
+type AppParams = {
+ organizationId: string;
+ projectId: string;
+};
+
+export function useAppParams() {
+ const params = useParams();
+ return {
+ ...(params ?? {}),
+ organizationId: params?.organizationId,
+ projectId: params?.projectId,
+ } as T & AppParams;
+}
diff --git a/apps/web/src/hooks/useEventNames.ts b/apps/web/src/hooks/useEventNames.ts
new file mode 100644
index 00000000..66f42e49
--- /dev/null
+++ b/apps/web/src/hooks/useEventNames.ts
@@ -0,0 +1,12 @@
+import { api } from '@/app/_trpc/client';
+
+export function useEventNames(projectId: string) {
+ const filterEventsQuery = api.chart.events.useQuery({
+ projectId: projectId,
+ });
+
+ return (filterEventsQuery.data ?? []).map((item) => ({
+ value: item.name,
+ label: item.name,
+ }));
+}
diff --git a/apps/web/src/hooks/useOrganizationParams.ts b/apps/web/src/hooks/useOrganizationParams.ts
deleted file mode 100644
index eaf5784e..00000000
--- a/apps/web/src/hooks/useOrganizationParams.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { z } from 'zod';
-
-import { useQueryParams } from './useQueryParams';
-
-export function useOrganizationParams() {
- return useQueryParams(
- z.object({
- organization: z.string(),
- project: z.string(),
- dashboard: z.string(),
- profileId: z.string().optional(),
- })
- );
-}
diff --git a/apps/web/src/hooks/useRechartDataModel.ts b/apps/web/src/hooks/useRechartDataModel.ts
new file mode 100644
index 00000000..19177a58
--- /dev/null
+++ b/apps/web/src/hooks/useRechartDataModel.ts
@@ -0,0 +1,34 @@
+import { useMemo } from 'react';
+import type { IChartData } from '@/app/_trpc/client';
+import { alphabetIds } from '@/utils/constants';
+import { getChartColor } from '@/utils/theme';
+
+export function useRechartDataModel(data: IChartData) {
+ return useMemo(() => {
+ return (
+ data.series[0]?.data.map(({ date }) => {
+ return {
+ date,
+ ...data.series.reduce((acc, serie, idx) => {
+ return {
+ ...acc,
+ ...serie.data.reduce(
+ (acc2, item) => {
+ if (item.date === date) {
+ acc2[`${idx}:count`] = item.count;
+ acc2[`${idx}:payload`] = {
+ ...item,
+ color: getChartColor(idx),
+ };
+ }
+ return acc2;
+ },
+ {} as Record
+ ),
+ };
+ }, {}),
+ };
+ }) ?? []
+ );
+ }, [data]);
+}
diff --git a/apps/web/src/hooks/useSetCookie.ts b/apps/web/src/hooks/useSetCookie.ts
new file mode 100644
index 00000000..58097ecd
--- /dev/null
+++ b/apps/web/src/hooks/useSetCookie.ts
@@ -0,0 +1,16 @@
+import { usePathname, useRouter } from 'next/navigation';
+
+export function useSetCookie() {
+ const router = useRouter();
+ const pathname = usePathname();
+ return (key: string, value: string, path?: string) => {
+ fetch(`/api/cookie?${key}=${value}`).then(() => {
+ if (path && path !== pathname) {
+ router.refresh();
+ router.replace(path);
+ } else {
+ router.refresh();
+ }
+ });
+ };
+}
diff --git a/apps/web/src/hooks/useVisibleSeries.ts b/apps/web/src/hooks/useVisibleSeries.ts
new file mode 100644
index 00000000..5d61bda6
--- /dev/null
+++ b/apps/web/src/hooks/useVisibleSeries.ts
@@ -0,0 +1,28 @@
+import { useEffect, useMemo, useRef, useState } from 'react';
+import type { IChartData } from '@/app/_trpc/client';
+
+export function useVisibleSeries(data: IChartData, limit?: number | undefined) {
+ const max = limit ?? 20;
+ const [visibleSeries, setVisibleSeries] = useState([]);
+ const ref = useRef(false);
+ useEffect(() => {
+ if (!ref.current && data) {
+ setVisibleSeries(
+ data?.series?.slice(0, max).map((serie) => serie.name) ?? []
+ );
+ // ref.current = true;
+ }
+ }, [data, max]);
+
+ return useMemo(() => {
+ return {
+ series: data.series
+ .map((serie, index) => ({
+ ...serie,
+ index,
+ }))
+ .filter((serie) => visibleSeries.includes(serie.name)),
+ setVisibleSeries,
+ } as const;
+ }, [visibleSeries, data.series]);
+}
diff --git a/apps/web/src/modals/AddClient.tsx b/apps/web/src/modals/AddClient.tsx
index c4c62c03..2de4a1db 100644
--- a/apps/web/src/modals/AddClient.tsx
+++ b/apps/web/src/modals/AddClient.tsx
@@ -1,25 +1,24 @@
+'use client';
+
+import { api, handleError } from '@/app/_trpc/client';
import { ButtonContainer } from '@/components/ButtonContainer';
import { InputWithLabel } from '@/components/forms/InputWithLabel';
+import Syntax from '@/components/Syntax';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Combobox } from '@/components/ui/combobox';
import { Label } from '@/components/ui/label';
import { toast } from '@/components/ui/use-toast';
-import { useOrganizationParams } from '@/hooks/useOrganizationParams';
-import { useRefetchActive } from '@/hooks/useRefetchActive';
-import { api, handleError } from '@/utils/api';
import { clipboard } from '@/utils/clipboard';
import { zodResolver } from '@hookform/resolvers/zod';
import { Copy } from 'lucide-react';
-import dynamic from 'next/dynamic';
+import { useRouter } from 'next/navigation';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
-const Syntax = dynamic(import('@/components/Syntax'));
-
const validator = z.object({
name: z.string().min(1, 'Required'),
cors: z.string().min(1, 'Required'),
@@ -28,13 +27,16 @@ const validator = z.object({
});
type IForm = z.infer;
+interface AddClientProps {
+ organizationId: string;
+}
-export default function CreateProject() {
- const params = useOrganizationParams();
- const refetch = useRefetchActive();
+export default function AddClient({ organizationId }: AddClientProps) {
+ const router = useRouter();
const query = api.project.list.useQuery({
- organizationSlug: params.organization,
+ organizationId,
});
+
const mutation = api.client.create.useMutation({
onError: handleError,
onSuccess() {
@@ -42,9 +44,10 @@ export default function CreateProject() {
title: 'Success',
description: 'Client created!',
});
- refetch();
+ router.refresh();
},
});
+
const { register, handleSubmit, formState, control } = useForm({
resolver: zodResolver(validator),
defaultValues: {
@@ -128,7 +131,7 @@ export default function CreateProject() {
onSubmit={handleSubmit((values) => {
mutation.mutate({
...values,
- organizationSlug: params.organization,
+ organizationId,
});
})}
>
diff --git a/apps/web/src/modals/AddDashboard.tsx b/apps/web/src/modals/AddDashboard.tsx
index f2cd7b2e..03eac3d2 100644
--- a/apps/web/src/modals/AddDashboard.tsx
+++ b/apps/web/src/modals/AddDashboard.tsx
@@ -1,10 +1,12 @@
+'use client';
+
+import { api, handleError } from '@/app/_trpc/client';
import { ButtonContainer } from '@/components/ButtonContainer';
import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button';
import { toast } from '@/components/ui/use-toast';
-import { useRefetchActive } from '@/hooks/useRefetchActive';
-import { api, handleError } from '@/utils/api';
import { zodResolver } from '@hookform/resolvers/zod';
+import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@@ -12,8 +14,7 @@ import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
interface AddDashboardProps {
- organizationSlug: string;
- projectSlug: string;
+ projectId: string;
}
const validator = z.object({
@@ -22,11 +23,8 @@ const validator = z.object({
type IForm = z.infer;
-export default function AddDashboard({
- // organizationSlug,
- projectSlug,
-}: AddDashboardProps) {
- const refetch = useRefetchActive();
+export default function AddDashboard({ projectId }: AddDashboardProps) {
+ const router = useRouter();
const { register, handleSubmit, formState } = useForm({
resolver: zodResolver(validator),
@@ -38,7 +36,7 @@ export default function AddDashboard({
const mutation = api.dashboard.create.useMutation({
onError: handleError,
onSuccess() {
- refetch();
+ router.refresh();
toast({
title: 'Success',
description: 'Dashboard created.',
@@ -49,13 +47,13 @@ export default function AddDashboard({
return (
-
+