refactor packages
This commit is contained in:
@@ -31,30 +31,20 @@
|
|||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"embla-carousel-autoplay": "8.0.0-rc22",
|
"embla-carousel-autoplay": "8.0.0-rc22",
|
||||||
"embla-carousel-react": "8.0.0-rc22",
|
"embla-carousel-react": "8.0.0-rc22",
|
||||||
"hamburger-react": "^2.5.0",
|
|
||||||
"lucide-react": "^0.323.0",
|
"lucide-react": "^0.323.0",
|
||||||
"next": "~14.0.4",
|
"next": "~14.0.4",
|
||||||
"nuqs": "^1.15.2",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-animate-height": "^3.2.3",
|
"react-animate-height": "^3.2.3",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-in-viewport": "1.0.0-alpha.30",
|
|
||||||
"react-responsive": "^9.0.2",
|
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"sharp": "^0.33.2",
|
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7"
|
||||||
"usehooks-ts": "^2.9.1",
|
|
||||||
"zod": "^3.22.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mixan/eslint-config": "workspace:*",
|
"@mixan/eslint-config": "workspace:*",
|
||||||
"@mixan/prettier-config": "workspace:*",
|
"@mixan/prettier-config": "workspace:*",
|
||||||
"@mixan/tsconfig": "workspace:*",
|
"@mixan/tsconfig": "workspace:*",
|
||||||
"@types/bcrypt": "^5.0.0",
|
|
||||||
"@types/lodash.debounce": "^4.0.9",
|
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^18.16.0",
|
||||||
"@types/ramda": "^0.29.6",
|
|
||||||
"@types/react": "^18.2.20",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-syntax-highlighter": "^15.5.9",
|
"@types/react-syntax-highlighter": "^15.5.9",
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export function clipboard(value: string | number) {
|
|
||||||
navigator.clipboard.writeText(value.toString());
|
|
||||||
toast({
|
|
||||||
title: 'Copied to clipboard',
|
|
||||||
description: value.toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
export const operators = {
|
|
||||||
is: 'Is',
|
|
||||||
isNot: 'Is not',
|
|
||||||
contains: 'Contains',
|
|
||||||
doesNotContain: 'Not contains',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const chartTypes = {
|
|
||||||
linear: 'Linear',
|
|
||||||
bar: 'Bar',
|
|
||||||
histogram: 'Histogram',
|
|
||||||
pie: 'Pie',
|
|
||||||
metric: 'Metric',
|
|
||||||
area: 'Area',
|
|
||||||
map: 'Map',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const lineTypes = {
|
|
||||||
monotone: 'Monotone',
|
|
||||||
monotoneX: 'Monotone X',
|
|
||||||
monotoneY: 'Monotone Y',
|
|
||||||
linear: 'Linear',
|
|
||||||
natural: 'Natural',
|
|
||||||
basis: 'Basis',
|
|
||||||
step: 'Step',
|
|
||||||
stepBefore: 'Step before',
|
|
||||||
stepAfter: 'Step after',
|
|
||||||
basisClosed: 'Basis closed',
|
|
||||||
basisOpen: 'Basis open',
|
|
||||||
bumpX: 'Bump X',
|
|
||||||
bumpY: 'Bump Y',
|
|
||||||
bump: 'Bump',
|
|
||||||
linearClosed: 'Linear closed',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const intervals = {
|
|
||||||
minute: 'minute',
|
|
||||||
day: 'day',
|
|
||||||
hour: 'hour',
|
|
||||||
month: 'month',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const alphabetIds = [
|
|
||||||
'A',
|
|
||||||
'B',
|
|
||||||
'C',
|
|
||||||
'D',
|
|
||||||
'E',
|
|
||||||
'F',
|
|
||||||
'G',
|
|
||||||
'H',
|
|
||||||
'I',
|
|
||||||
'J',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const timeRanges = {
|
|
||||||
'30min': '30min',
|
|
||||||
'1h': '1h',
|
|
||||||
today: 'today',
|
|
||||||
'24h': '24h',
|
|
||||||
'7d': '7d',
|
|
||||||
'14d': '14d',
|
|
||||||
'1m': '1m',
|
|
||||||
'3m': '3m',
|
|
||||||
'6m': '6m',
|
|
||||||
'1y': '1y',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const metrics = {
|
|
||||||
sum: 'sum',
|
|
||||||
average: 'average',
|
|
||||||
min: 'min',
|
|
||||||
max: 'max',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function isMinuteIntervalEnabledByRange(range: keyof typeof timeRanges) {
|
|
||||||
return range === '30min' || range === '1h';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isHourIntervalEnabledByRange(range: keyof typeof timeRanges) {
|
|
||||||
return (
|
|
||||||
isMinuteIntervalEnabledByRange(range) ||
|
|
||||||
range === 'today' ||
|
|
||||||
range === '24h'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultIntervalByRange(
|
|
||||||
range: keyof typeof timeRanges
|
|
||||||
): keyof typeof intervals {
|
|
||||||
if (range === '30min' || range === '1h') {
|
|
||||||
return 'minute';
|
|
||||||
} else if (range === 'today' || range === '24h') {
|
|
||||||
return 'hour';
|
|
||||||
} else if (range === '7d' || range === '14d' || range === '1m') {
|
|
||||||
return 'day';
|
|
||||||
}
|
|
||||||
return 'month';
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export function getDaysOldDate(days: number) {
|
|
||||||
const date = new Date();
|
|
||||||
date.setDate(date.getDate() - days);
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dateDifferanceInDays(date1: Date, date2: Date) {
|
|
||||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
|
||||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLocale() {
|
|
||||||
if (typeof navigator === 'undefined') {
|
|
||||||
return 'en-US';
|
|
||||||
}
|
|
||||||
|
|
||||||
return navigator.language ?? 'en-US';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDate(date: Date) {
|
|
||||||
return new Intl.DateTimeFormat(getLocale()).format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDateTime(date: Date) {
|
|
||||||
return new Intl.DateTimeFormat(getLocale(), {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
}).format(date);
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import type { Profile } from '@mixan/db';
|
|
||||||
|
|
||||||
export function getProfileName(profile: Profile | undefined | null) {
|
|
||||||
if (!profile) return 'No profile';
|
|
||||||
return [profile.first_name, profile.last_name].filter(Boolean).join(' ');
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { isNumber } from 'mathjs';
|
|
||||||
|
|
||||||
export const round = (num: number, decimals = 2) => {
|
|
||||||
const factor = Math.pow(10, decimals);
|
|
||||||
return Math.round((num + Number.EPSILON) * factor) / factor;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const average = (arr: (number | null)[]) => {
|
|
||||||
const filtered = arr.filter(isNumber);
|
|
||||||
return filtered.reduce((p, c) => p + c, 0) / filtered.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sum = (arr: (number | null)[]): number =>
|
|
||||||
round(arr.filter(isNumber).reduce((acc, item) => acc + item, 0));
|
|
||||||
|
|
||||||
export const min = (arr: (number | null)[]): number =>
|
|
||||||
Math.min(...arr.filter(isNumber));
|
|
||||||
|
|
||||||
export const max = (arr: (number | null)[]): number =>
|
|
||||||
Math.max(...arr.filter(isNumber));
|
|
||||||
|
|
||||||
export const isFloat = (n: number) => n % 1 !== 0;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import _slugify from 'slugify';
|
|
||||||
|
|
||||||
const slugify = (str: string) => {
|
|
||||||
return _slugify(
|
|
||||||
str
|
|
||||||
.replace('å', 'a')
|
|
||||||
.replace('ä', 'a')
|
|
||||||
.replace('ö', 'o')
|
|
||||||
.replace('Å', 'A')
|
|
||||||
.replace('Ä', 'A')
|
|
||||||
.replace('Ö', 'O'),
|
|
||||||
{ lower: true, strict: true, trim: true }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function slug(str: string): string {
|
|
||||||
return slugify(str);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import resolveConfig from 'tailwindcss/resolveConfig';
|
|
||||||
|
|
||||||
import tailwinConfig from '../../tailwind.config';
|
|
||||||
|
|
||||||
export const resolvedTailwindConfig = resolveConfig(tailwinConfig);
|
|
||||||
|
|
||||||
export const theme = resolvedTailwindConfig.theme as Record<string, any>;
|
|
||||||
|
|
||||||
export function getChartColor(index: number): string {
|
|
||||||
const colors = theme?.colors ?? {};
|
|
||||||
const chartColors: string[] = Object.keys(colors)
|
|
||||||
.filter((key) => key.startsWith('chart-'))
|
|
||||||
.map((key) => colors[key])
|
|
||||||
.filter((item): item is string => typeof item === 'string');
|
|
||||||
|
|
||||||
return chartColors[index % chartColors.length]!;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export function truncate(str: string, len: number) {
|
|
||||||
if (str.length <= len) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
return str.slice(0, len) + '...';
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import {
|
|
||||||
chartTypes,
|
|
||||||
intervals,
|
|
||||||
lineTypes,
|
|
||||||
metrics,
|
|
||||||
operators,
|
|
||||||
timeRanges,
|
|
||||||
} from './constants';
|
|
||||||
|
|
||||||
export function objectToZodEnums<K extends string>(
|
|
||||||
obj: Record<K, any>
|
|
||||||
): [K, ...K[]] {
|
|
||||||
const [firstKey, ...otherKeys] = Object.keys(obj) as K[];
|
|
||||||
return [firstKey!, ...otherKeys];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mapKeys = objectToZodEnums;
|
|
||||||
|
|
||||||
export const zChartEvent = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
displayName: z.string().optional(),
|
|
||||||
property: z.string().optional(),
|
|
||||||
segment: z.enum([
|
|
||||||
'event',
|
|
||||||
'user',
|
|
||||||
'user_average',
|
|
||||||
'one_event_per_user',
|
|
||||||
'property_sum',
|
|
||||||
'property_average',
|
|
||||||
]),
|
|
||||||
filters: z.array(
|
|
||||||
z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
operator: z.enum(objectToZodEnums(operators)),
|
|
||||||
value: z.array(z.string().or(z.number()).or(z.boolean()).or(z.null())),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
|
||||||
export const zChartBreakdown = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zChartEvents = z.array(zChartEvent);
|
|
||||||
export const zChartBreakdowns = z.array(zChartBreakdown);
|
|
||||||
|
|
||||||
export const zChartType = z.enum(objectToZodEnums(chartTypes));
|
|
||||||
|
|
||||||
export const zLineType = z.enum(objectToZodEnums(lineTypes));
|
|
||||||
|
|
||||||
export const zTimeInterval = z.enum(objectToZodEnums(intervals));
|
|
||||||
|
|
||||||
export const zMetric = z.enum(objectToZodEnums(metrics));
|
|
||||||
|
|
||||||
export const zChartInput = z.object({
|
|
||||||
name: z.string(),
|
|
||||||
chartType: zChartType,
|
|
||||||
lineType: zLineType,
|
|
||||||
interval: zTimeInterval,
|
|
||||||
events: zChartEvents,
|
|
||||||
breakdowns: zChartBreakdowns,
|
|
||||||
range: z.enum(objectToZodEnums(timeRanges)),
|
|
||||||
previous: z.boolean(),
|
|
||||||
formula: z.string().optional(),
|
|
||||||
metric: zMetric,
|
|
||||||
unit: z.string().optional(),
|
|
||||||
previousIndicatorInverted: z.boolean().optional(),
|
|
||||||
projectId: z.string(),
|
|
||||||
startDate: z.string().nullish(),
|
|
||||||
endDate: z.string().nullish(),
|
|
||||||
});
|
|
||||||
@@ -15,7 +15,8 @@ function getHostname(url: string | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseReferrer(url: string | undefined) {
|
export function parseReferrer(url: string | undefined) {
|
||||||
const match = referrers[getHostname(url)];
|
const hostname = getHostname(url);
|
||||||
|
const match = referrers[hostname] ?? referrers[hostname.replace('www.', '')];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: match?.name ?? '',
|
name: match?.name ?? '',
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ COPY packages/db/package.json packages/db/package.json
|
|||||||
COPY packages/redis/package.json packages/redis/package.json
|
COPY packages/redis/package.json packages/redis/package.json
|
||||||
COPY packages/queue/package.json packages/queue/package.json
|
COPY packages/queue/package.json packages/queue/package.json
|
||||||
COPY packages/common/package.json packages/common/package.json
|
COPY packages/common/package.json packages/common/package.json
|
||||||
|
COPY packages/constants/package.json packages/constants/package.json
|
||||||
|
COPY packages/validation/package.json packages/validation/package.json
|
||||||
COPY packages/types/package.json packages/types/package.json
|
COPY packages/types/package.json packages/types/package.json
|
||||||
|
|
||||||
# BUILD
|
# BUILD
|
||||||
@@ -92,11 +94,15 @@ COPY --from=build /app/packages/db /app/packages/db
|
|||||||
COPY --from=build /app/packages/redis /app/packages/redis
|
COPY --from=build /app/packages/redis /app/packages/redis
|
||||||
COPY --from=build /app/packages/common /app/packages/common
|
COPY --from=build /app/packages/common /app/packages/common
|
||||||
COPY --from=build /app/packages/queue /app/packages/queue
|
COPY --from=build /app/packages/queue /app/packages/queue
|
||||||
|
COPY --from=build /app/packages/constants /app/packages/constants
|
||||||
|
COPY --from=build /app/packages/validation /app/packages/validation
|
||||||
COPY --from=build /app/packages/types /app/packages/types
|
COPY --from=build /app/packages/types /app/packages/types
|
||||||
# Packages node_modules
|
# Packages node_modules
|
||||||
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
|
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
|
||||||
COPY --from=prod /app/packages/redis/node_modules /app/packages/redis/node_modules
|
COPY --from=prod /app/packages/redis/node_modules /app/packages/redis/node_modules
|
||||||
COPY --from=prod /app/packages/common/node_modules /app/packages/common/node_modules
|
COPY --from=prod /app/packages/common/node_modules /app/packages/common/node_modules
|
||||||
|
COPY --from=prod /app/packages/constants/node_modules /app/packages/constants/node_modules
|
||||||
|
COPY --from=prod /app/packages/validation/node_modules /app/packages/validation/node_modules
|
||||||
COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
|
COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
|
||||||
|
|
||||||
RUN pnpm db:codegen
|
RUN pnpm db:codegen
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"@clickhouse/client": "^0.2.9",
|
"@clickhouse/client": "^0.2.9",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@mixan/common": "workspace:^",
|
"@mixan/common": "workspace:^",
|
||||||
|
"@mixan/constants": "workspace:^",
|
||||||
|
"@mixan/validation": "workspace:^",
|
||||||
"@mixan/db": "workspace:^",
|
"@mixan/db": "workspace:^",
|
||||||
"@mixan/queue": "workspace:^",
|
"@mixan/queue": "workspace:^",
|
||||||
"@mixan/types": "workspace:*",
|
"@mixan/types": "workspace:*",
|
||||||
@@ -90,8 +92,8 @@
|
|||||||
"@types/lodash.throttle": "^4.1.9",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
"@types/node": "^18.19.15",
|
"@types/node": "^18.19.15",
|
||||||
"@types/ramda": "^0.29.10",
|
"@types/ramda": "^0.29.10",
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.19",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-syntax-highlighter": "^15.5.11",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"@types/request-ip": "^0.0.41",
|
"@types/request-ip": "^0.0.41",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layo
|
|||||||
import { LazyChart } from '@/components/report/chart/LazyChart';
|
import { LazyChart } from '@/components/report/chart/LazyChart';
|
||||||
import { ReportRange } from '@/components/report/ReportRange';
|
import { ReportRange } from '@/components/report/ReportRange';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -14,14 +13,15 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import type { getReportsByDashboardId } from '@/server/services/reports.service';
|
|
||||||
import type { IChartRange } from '@/types';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
|
||||||
import { ChevronRight, MoreHorizontal, PlusIcon, Trash } from 'lucide-react';
|
import { ChevronRight, MoreHorizontal, PlusIcon, Trash } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { getDefaultIntervalByRange } from '@mixan/constants';
|
||||||
|
import type { getReportsByDashboardId } from '@mixan/db';
|
||||||
|
import type { IChartRange } from '@mixan/validation';
|
||||||
|
|
||||||
interface ListReportsProps {
|
interface ListReportsProps {
|
||||||
reports: Awaited<ReturnType<typeof getReportsByDashboardId>>;
|
reports: Awaited<ReturnType<typeof getReportsByDashboardId>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { getDashboardById } from '@/server/services/dashboard.service';
|
|
||||||
import { getReportsByDashboardId } from '@/server/services/reports.service';
|
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { getDashboardById, getReportsByDashboardId } from '@mixan/db';
|
||||||
|
|
||||||
import { ListReports } from './list-reports';
|
import { ListReports } from './list-reports';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { ToastAction } from '@/components/ui/toast';
|
import { ToastAction } from '@/components/ui/toast';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import type { IServiceDashboards } from '@/server/services/dashboard.service';
|
|
||||||
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
|
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import type { IServiceDashboards } from '@mixan/db';
|
||||||
|
|
||||||
interface ListDashboardsProps {
|
interface ListDashboardsProps {
|
||||||
dashboards: IServiceDashboards;
|
dashboards: IServiceDashboards;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { getDashboardsByProjectId } from '@/server/services/dashboard.service';
|
|
||||||
|
import { getDashboardsByProjectId } from '@mixan/db';
|
||||||
|
|
||||||
import { HeaderDashboards } from './header-dashboards';
|
import { HeaderDashboards } from './header-dashboards';
|
||||||
import { ListDashboards } from './list-dashboards';
|
import { ListDashboards } from './list-dashboards';
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { RouterOutputs } from '@/app/_trpc/client';
|
|
||||||
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
||||||
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
|
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import {
|
||||||
|
useEventQueryFilters,
|
||||||
|
useEventQueryNamesFilter,
|
||||||
|
} from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getProfileName } from '@/utils/getters';
|
import { getProfileName } from '@/utils/getters';
|
||||||
import { round } from '@/utils/math';
|
import { round } from '@/utils/math';
|
||||||
|
import { uniq } from 'ramda';
|
||||||
|
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||||
|
|
||||||
@@ -40,7 +43,8 @@ export function EventListItem({
|
|||||||
meta,
|
meta,
|
||||||
}: EventListItemProps) {
|
}: EventListItemProps) {
|
||||||
const params = useAppParams();
|
const params = useAppParams();
|
||||||
const eventQueryFilters = useEventQueryFilters({ shallow: false });
|
const [, setEvents] = useEventQueryNamesFilter({ shallow: false });
|
||||||
|
const [, setFilter] = useEventQueryFilters({ shallow: false });
|
||||||
const keyValueList = [
|
const keyValueList = [
|
||||||
{
|
{
|
||||||
name: 'Duration',
|
name: 'Duration',
|
||||||
@@ -50,98 +54,98 @@ export function EventListItem({
|
|||||||
name: 'Referrer',
|
name: 'Referrer',
|
||||||
value: referrer,
|
value: referrer,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.referrer.set(referrer ?? null);
|
setFilter('referrer', referrer ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Referrer name',
|
name: 'Referrer name',
|
||||||
value: referrerName,
|
value: referrerName,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.referrerName.set(referrerName ?? null);
|
setFilter('referrer_name', referrerName ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Referrer type',
|
name: 'Referrer type',
|
||||||
value: referrerType,
|
value: referrerType,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.referrerType.set(referrerType ?? null);
|
setFilter('referrer_type', referrerType ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Brand',
|
name: 'Brand',
|
||||||
value: brand,
|
value: brand,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.brand.set(brand ?? null);
|
setFilter('brand', brand ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Model',
|
name: 'Model',
|
||||||
value: model,
|
value: model,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.model.set(model ?? null);
|
setFilter('model', model ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Browser',
|
name: 'Browser',
|
||||||
value: browser,
|
value: browser,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.browser.set(browser ?? null);
|
setFilter('browser', browser ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Browser version',
|
name: 'Browser version',
|
||||||
value: browserVersion,
|
value: browserVersion,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.browserVersion.set(browserVersion ?? null);
|
setFilter('browser_version', browserVersion ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'OS',
|
name: 'OS',
|
||||||
value: os,
|
value: os,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.os.set(os ?? null);
|
setFilter('os', os ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'OS cersion',
|
name: 'OS cersion',
|
||||||
value: osVersion,
|
value: osVersion,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.osVersion.set(osVersion ?? null);
|
setFilter('os_version', osVersion ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'City',
|
name: 'City',
|
||||||
value: city,
|
value: city,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.city.set(city ?? null);
|
setFilter('city', city ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Region',
|
name: 'Region',
|
||||||
value: region,
|
value: region,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.region.set(region ?? null);
|
setFilter('region', region ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Country',
|
name: 'Country',
|
||||||
value: country,
|
value: country,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.country.set(country ?? null);
|
setFilter('country', country ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Continent',
|
name: 'Continent',
|
||||||
value: continent,
|
value: continent,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.continent.set(continent ?? null);
|
setFilter('continent', continent ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Device',
|
name: 'Device',
|
||||||
value: device,
|
value: device,
|
||||||
onClick() {
|
onClick() {
|
||||||
eventQueryFilters.device.set(device ?? null);
|
setFilter('device', device ?? '');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
].filter((item) => typeof item.value === 'string' && item.value);
|
].filter((item) => typeof item.value === 'string' && item.value);
|
||||||
@@ -156,7 +160,11 @@ export function EventListItem({
|
|||||||
return (
|
return (
|
||||||
<ExpandableListItem
|
<ExpandableListItem
|
||||||
className={cn(meta?.conversion && 'ring-2 ring-primary-500')}
|
className={cn(meta?.conversion && 'ring-2 ring-primary-500')}
|
||||||
title={name.split('_').join(' ')}
|
title={
|
||||||
|
<button onClick={() => setEvents((p) => uniq([...p, name]))}>
|
||||||
|
{name.split('_').join(' ')}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
content={
|
content={
|
||||||
<>
|
<>
|
||||||
<KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
|
<KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
|
||||||
@@ -172,7 +180,7 @@ export function EventListItem({
|
|||||||
name="Path"
|
name="Path"
|
||||||
value={path}
|
value={path}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
eventQueryFilters.path.set(path);
|
setFilter('path', path);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -191,6 +199,13 @@ export function EventListItem({
|
|||||||
key={item.name}
|
key={item.name}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
value={item.value}
|
value={item.value}
|
||||||
|
onClick={() => {
|
||||||
|
setFilter(
|
||||||
|
`properties.${item.name}`,
|
||||||
|
item.value ? String(item.value) : '',
|
||||||
|
'is'
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
|||||||
import { Pagination } from '@/components/Pagination';
|
import { Pagination } from '@/components/Pagination';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useCursor } from '@/hooks/useCursor';
|
import { useCursor } from '@/hooks/useCursor';
|
||||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { GanttChartIcon } from 'lucide-react';
|
import { GanttChartIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||||
@@ -18,8 +18,7 @@ interface EventListProps {
|
|||||||
}
|
}
|
||||||
export function EventList({ data, count }: EventListProps) {
|
export function EventList({ data, count }: EventListProps) {
|
||||||
const { cursor, setCursor } = useCursor();
|
const { cursor, setCursor } = useCursor();
|
||||||
const filters = useEventFilters();
|
const [filters] = useEventQueryFilters();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||||
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
||||||
import { getEventFilters } from '@/hooks/useEventQueryFilters';
|
import {
|
||||||
|
eventQueryFiltersParser,
|
||||||
|
eventQueryNamesFilter,
|
||||||
|
} from '@/hooks/useEventQueryFilters';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
|
||||||
import { getEventList, getEventsCount } from '@mixan/db';
|
import { getEventList, getEventsCount } from '@mixan/db';
|
||||||
@@ -15,27 +18,9 @@ interface PageProps {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
searchParams: {
|
searchParams: {
|
||||||
|
events?: string;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
path?: string;
|
f?: string;
|
||||||
device?: string;
|
|
||||||
referrer?: string;
|
|
||||||
referrerName?: string;
|
|
||||||
referrerType?: string;
|
|
||||||
utmSource?: string;
|
|
||||||
utmMedium?: string;
|
|
||||||
utmCampaign?: string;
|
|
||||||
utmContent?: string;
|
|
||||||
utmTerm?: string;
|
|
||||||
continent?: string;
|
|
||||||
country?: string;
|
|
||||||
region?: string;
|
|
||||||
city?: string;
|
|
||||||
browser?: string;
|
|
||||||
browserVersion?: string;
|
|
||||||
os?: string;
|
|
||||||
osVersion?: string;
|
|
||||||
brand?: string;
|
|
||||||
model?: string;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,53 +44,13 @@ export default async function Page({
|
|||||||
cursor: parseQueryAsNumber(searchParams.cursor),
|
cursor: parseQueryAsNumber(searchParams.cursor),
|
||||||
projectId,
|
projectId,
|
||||||
take: 50,
|
take: 50,
|
||||||
filters: getEventFilters({
|
events: eventQueryNamesFilter.parse(searchParams.events ?? ''),
|
||||||
path: searchParams.path ?? null,
|
filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined,
|
||||||
device: searchParams.device ?? null,
|
|
||||||
referrer: searchParams.referrer ?? null,
|
|
||||||
referrerName: searchParams.referrerName ?? null,
|
|
||||||
referrerType: searchParams.referrerType ?? null,
|
|
||||||
utmSource: searchParams.utmSource ?? null,
|
|
||||||
utmMedium: searchParams.utmMedium ?? null,
|
|
||||||
utmCampaign: searchParams.utmCampaign ?? null,
|
|
||||||
utmContent: searchParams.utmContent ?? null,
|
|
||||||
utmTerm: searchParams.utmTerm ?? null,
|
|
||||||
continent: searchParams.continent ?? null,
|
|
||||||
country: searchParams.country ?? null,
|
|
||||||
region: searchParams.region ?? null,
|
|
||||||
city: searchParams.city ?? null,
|
|
||||||
browser: searchParams.browser ?? null,
|
|
||||||
browserVersion: searchParams.browserVersion ?? null,
|
|
||||||
os: searchParams.os ?? null,
|
|
||||||
osVersion: searchParams.osVersion ?? null,
|
|
||||||
brand: searchParams.brand ?? null,
|
|
||||||
model: searchParams.model ?? null,
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
getEventsCount({
|
getEventsCount({
|
||||||
projectId,
|
projectId,
|
||||||
filters: getEventFilters({
|
events: eventQueryNamesFilter.parse(searchParams.events ?? ''),
|
||||||
path: searchParams.path ?? null,
|
filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined,
|
||||||
device: searchParams.device ?? null,
|
|
||||||
referrer: searchParams.referrer ?? null,
|
|
||||||
referrerName: searchParams.referrerName ?? null,
|
|
||||||
referrerType: searchParams.referrerType ?? null,
|
|
||||||
utmSource: searchParams.utmSource ?? null,
|
|
||||||
utmMedium: searchParams.utmMedium ?? null,
|
|
||||||
utmCampaign: searchParams.utmCampaign ?? null,
|
|
||||||
utmContent: searchParams.utmContent ?? null,
|
|
||||||
utmTerm: searchParams.utmTerm ?? null,
|
|
||||||
continent: searchParams.continent ?? null,
|
|
||||||
country: searchParams.country ?? null,
|
|
||||||
region: searchParams.region ?? null,
|
|
||||||
city: searchParams.city ?? null,
|
|
||||||
browser: searchParams.browser ?? null,
|
|
||||||
browserVersion: searchParams.browserVersion ?? null,
|
|
||||||
os: searchParams.os ?? null,
|
|
||||||
osVersion: searchParams.osVersion ?? null,
|
|
||||||
brand: searchParams.brand ?? null,
|
|
||||||
model: searchParams.model ?? null,
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
getExists(organizationId, projectId),
|
getExists(organizationId, projectId),
|
||||||
]);
|
]);
|
||||||
@@ -116,6 +61,7 @@ export default async function Page({
|
|||||||
<OverviewFiltersDrawer
|
<OverviewFiltersDrawer
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
nuqsOptions={nuqsOptions}
|
nuqsOptions={nuqsOptions}
|
||||||
|
enableEventsFilter
|
||||||
/>
|
/>
|
||||||
<OverviewFiltersButtons
|
<OverviewFiltersButtons
|
||||||
className="p-0 justify-end"
|
className="p-0 justify-end"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import type { IServiceDashboards } from '@/server/services/dashboard.service';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { useUser } from '@clerk/nextjs';
|
import { useUser } from '@clerk/nextjs';
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +20,8 @@ import type { LucideProps } from 'lucide-react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
import type { IServiceDashboards } from '@mixan/db';
|
||||||
|
|
||||||
function LinkWithIcon({
|
function LinkWithIcon({
|
||||||
href,
|
href,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import type { IServiceOrganization } from '@/server/services/organization.service';
|
|
||||||
import { Building } from 'lucide-react';
|
import { Building } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import type { IServiceOrganization } from '@mixan/db';
|
||||||
|
|
||||||
interface LayoutOrganizationSelectorProps {
|
interface LayoutOrganizationSelectorProps {
|
||||||
organizations: IServiceOrganization[];
|
organizations: IServiceOrganization[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import type { getCurrentProjects } from '@/server/services/project.service';
|
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import type { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||||
|
|
||||||
interface LayoutProjectSelectorProps {
|
interface LayoutProjectSelectorProps {
|
||||||
projects: Awaited<ReturnType<typeof getCurrentProjects>>;
|
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
||||||
}
|
}
|
||||||
export default function LayoutProjectSelector({
|
export default function LayoutProjectSelector({
|
||||||
projects,
|
projects,
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Logo } from '@/components/Logo';
|
import { Logo } from '@/components/Logo';
|
||||||
import type { IServiceDashboards } from '@/server/services/dashboard.service';
|
|
||||||
import type { IServiceOrganization } from '@/server/services/organization.service';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { Rotate as Hamburger } from 'hamburger-react';
|
import { Rotate as Hamburger } from 'hamburger-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
import type { IServiceDashboards, IServiceOrganization } from '@mixan/db';
|
||||||
|
|
||||||
import LayoutMenu from './layout-menu';
|
import LayoutMenu from './layout-menu';
|
||||||
import LayoutOrganizationSelector from './layout-organization-selector';
|
import LayoutOrganizationSelector from './layout-organization-selector';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { getDashboardsByOrganization } from '@/server/services/dashboard.service';
|
import {
|
||||||
import { getCurrentOrganizations } from '@/server/services/organization.service';
|
getCurrentOrganizations,
|
||||||
|
getDashboardsByOrganization,
|
||||||
|
} from '@mixan/db';
|
||||||
|
|
||||||
import { LayoutSidebar } from './layout-sidebar';
|
import { LayoutSidebar } from './layout-sidebar';
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import { WidgetHead } from '@/components/overview/overview-widget';
|
|||||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { Chart } from '@/components/report/chart';
|
import { Chart } from '@/components/report/chart';
|
||||||
import { Widget, WidgetBody } from '@/components/Widget';
|
import { Widget, WidgetBody } from '@/components/Widget';
|
||||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import type { IChartInput } from '@/types';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
|
import type { IChartInput } from '@mixan/validation';
|
||||||
|
|
||||||
interface OverviewMetricsProps {
|
interface OverviewMetricsProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||||
const { previous, range, interval, metric, setMetric } = useOverviewOptions();
|
const { previous, range, interval, metric, setMetric } = useOverviewOptions();
|
||||||
const filters = useEventFilters();
|
const [filters] = useEventQueryFilters();
|
||||||
|
|
||||||
const reports = [
|
const reports = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getCurrentProjects } from '@/server/services/project.service';
|
import { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||||
|
|
||||||
import LayoutProjectSelector from './layout-project-selector';
|
import LayoutProjectSelector from './layout-project-selector';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export default async function PageLayout({
|
|||||||
title,
|
title,
|
||||||
organizationSlug,
|
organizationSlug,
|
||||||
}: PageLayoutProps) {
|
}: PageLayoutProps) {
|
||||||
const projects = await getCurrentProjects(organizationSlug);
|
const projects = await getProjectsByOrganizationSlug(organizationSlug);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function ListProfileEvents({
|
|||||||
projectId,
|
projectId,
|
||||||
profileId,
|
profileId,
|
||||||
}: ListProfileEvents) {
|
}: ListProfileEvents) {
|
||||||
const pagination = usePagination();
|
const pagination = usePagination(50);
|
||||||
const [eventFilters, setEventFilters] = useQueryState(
|
const [eventFilters, setEventFilters] = useQueryState(
|
||||||
'events',
|
'events',
|
||||||
parseAsJson<string[]>().withDefault([])
|
parseAsJson<string[]>().withDefault([])
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import { ListProperties } from '@/components/events/ListProperties';
|
|||||||
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import {
|
|
||||||
getProfileById,
|
|
||||||
getProfilesByExternalId,
|
|
||||||
} from '@/server/services/profile.service';
|
|
||||||
import { formatDateTime } from '@/utils/date';
|
import { formatDateTime } from '@/utils/date';
|
||||||
import { getProfileName } from '@/utils/getters';
|
import { getProfileName } from '@/utils/getters';
|
||||||
|
|
||||||
|
import { getProfileById, getProfilesByExternalId } from '@mixan/db';
|
||||||
|
|
||||||
import ListProfileEvents from './list-profile-events';
|
import ListProfileEvents from './list-profile-events';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import type { RouterOutputs } from '@/app/_trpc/client';
|
import type { RouterOutputs } from '@/app/_trpc/client';
|
||||||
import { ListProperties } from '@/components/events/ListProperties';
|
import { ListProperties } from '@/components/events/ListProperties';
|
||||||
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
||||||
@@ -16,24 +15,24 @@ export function ProfileListItem(props: ProfileListItemProps) {
|
|||||||
const { id, properties, createdAt } = props;
|
const { id, properties, createdAt } = props;
|
||||||
const params = useAppParams();
|
const params = useAppParams();
|
||||||
|
|
||||||
const bullets = useMemo(() => {
|
const renderContent = () => {
|
||||||
const bullets: React.ReactNode[] = [
|
return (
|
||||||
<span>{formatDateTime(createdAt)}</span>,
|
<>
|
||||||
<Link
|
<span>{formatDateTime(createdAt)}</span>
|
||||||
href={`/${params.organizationId}/${params.projectId}/profiles/${id}`}
|
<Link
|
||||||
className="text-black font-medium hover:underline"
|
href={`/${params.organizationId}/${params.projectId}/profiles/${id}`}
|
||||||
>
|
className="text-black font-medium hover:underline"
|
||||||
See profile
|
>
|
||||||
</Link>,
|
See profile
|
||||||
];
|
</Link>
|
||||||
|
</>
|
||||||
return bullets;
|
);
|
||||||
}, [createdAt, id, params]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableListItem
|
<ExpandableListItem
|
||||||
title={getProfileName(props)}
|
title={getProfileName(props)}
|
||||||
bullets={bullets}
|
content={renderContent()}
|
||||||
image={<ProfileAvatar {...props} />}
|
image={<ProfileAvatar {...props} />}
|
||||||
>
|
>
|
||||||
<ListProperties data={properties} className="rounded-none border-none" />
|
<ListProperties data={properties} className="rounded-none border-none" />
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
|
||||||
import { getReportById } from '@/server/services/reports.service';
|
|
||||||
import { Pencil } from 'lucide-react';
|
import { Pencil } from 'lucide-react';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { getOrganizationBySlug, getReportById } from '@mixan/db';
|
||||||
|
|
||||||
import ReportEditor from '../report-editor';
|
import ReportEditor from '../report-editor';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
|
||||||
import { Pencil } from 'lucide-react';
|
import { Pencil } from 'lucide-react';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { getOrganizationBySlug } from '@mixan/db';
|
||||||
|
|
||||||
import ReportEditor from './report-editor';
|
import ReportEditor from './report-editor';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import type { IServiceReport } from '@/server/services/reports.service';
|
|
||||||
import { GanttChartSquareIcon } from 'lucide-react';
|
import { GanttChartSquareIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { IServiceReport } from '@mixan/db';
|
||||||
|
|
||||||
interface ReportEditorProps {
|
interface ReportEditorProps {
|
||||||
report: IServiceReport | null;
|
report: IServiceReport | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import { DataTable } from '@/components/DataTable';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import type { getClientsByOrganizationId } from '@/server/services/clients.service';
|
|
||||||
import { PlusIcon } from 'lucide-react';
|
import { PlusIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { getClientsByOrganizationId } from '@mixan/db';
|
||||||
|
|
||||||
interface ListClientsProps {
|
interface ListClientsProps {
|
||||||
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
|
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { getClientsByOrganizationId } from '@/server/services/clients.service';
|
|
||||||
|
import { getClientsByOrganizationId } from '@mixan/db';
|
||||||
|
|
||||||
import ListClients from './list-clients';
|
import ListClients from './list-clients';
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { api, handleError } from '@/app/_trpc/client';
|
|||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||||
import type { getOrganizationBySlug } from '@/server/services/organization.service';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { getOrganizationBySlug } from '@mixan/db';
|
||||||
|
|
||||||
const validator = z.object({
|
const validator = z.object({
|
||||||
id: z.string().min(2),
|
id: z.string().min(2),
|
||||||
name: z.string().min(2),
|
name: z.string().min(2),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { api } from '@/app/_trpc/client';
|
|||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { zInviteUser } from '@/utils/validation';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { SendIcon } from 'lucide-react';
|
import { SendIcon } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -10,6 +9,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import { zInviteUser } from '@mixan/validation';
|
||||||
|
|
||||||
type IForm = z.infer<typeof zInviteUser>;
|
type IForm = z.infer<typeof zInviteUser>;
|
||||||
|
|
||||||
export function InviteUser() {
|
export function InviteUser() {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||||
import type { IServiceInvites } from '@/server/services/organization.service';
|
|
||||||
|
import type { IServiceInvites } from '@mixan/db';
|
||||||
|
|
||||||
import { InviteUser } from './invite-user';
|
import { InviteUser } from './invite-user';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import {
|
|
||||||
getInvites,
|
|
||||||
getOrganizationBySlug,
|
|
||||||
} from '@/server/services/organization.service';
|
|
||||||
import { clerkClient } from '@clerk/nextjs';
|
import { clerkClient } from '@clerk/nextjs';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { getInvites, getOrganizationBySlug } from '@mixan/db';
|
||||||
|
|
||||||
import EditOrganization from './edit-organization';
|
import EditOrganization from './edit-organization';
|
||||||
import InvitedUsers from './invited-users';
|
import InvitedUsers from './invited-users';
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { api, handleError } from '@/app/_trpc/client';
|
|||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||||
import type { getUserById } from '@/server/services/user.service';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { getUserById } from '@mixan/db';
|
||||||
|
|
||||||
const validator = z.object({
|
const validator = z.object({
|
||||||
firstName: z.string().min(2),
|
firstName: z.string().min(2),
|
||||||
lastName: z.string().min(2),
|
lastName: z.string().min(2),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { getUserById } from '@/server/services/user.service';
|
|
||||||
import { auth } from '@clerk/nextjs';
|
import { auth } from '@clerk/nextjs';
|
||||||
|
|
||||||
|
import { getUserById } from '@mixan/db';
|
||||||
|
|
||||||
import EditProfile from './edit-profile';
|
import EditProfile from './edit-profile';
|
||||||
import { Logout } from './logout';
|
import { Logout } from './logout';
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import { columns } from '@/components/projects/table';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import type { getProjectsByOrganizationId } from '@/server/services/project.service';
|
|
||||||
import { PlusIcon } from 'lucide-react';
|
import { PlusIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||||
|
|
||||||
interface ListProjectsProps {
|
interface ListProjectsProps {
|
||||||
projects: Awaited<ReturnType<typeof getProjectsByOrganizationId>>;
|
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
||||||
}
|
}
|
||||||
export default function ListProjects({ projects }: ListProjectsProps) {
|
export default function ListProjects({ projects }: ListProjectsProps) {
|
||||||
const organizationId = useAppParams().organizationId;
|
const organizationId = useAppParams().organizationId;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { getProjectsByOrganizationSlug } from '@/server/services/project.service';
|
|
||||||
|
import { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||||
|
|
||||||
import ListProjects from './list-projects';
|
import ListProjects from './list-projects';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
import { getOrganizationBySlug } from '@mixan/db';
|
||||||
import { getProjectWithMostEvents } from '@/server/services/project.service';
|
import { getProjectWithMostEvents } from '@mixan/db';
|
||||||
import { notFound, redirect } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
|
|
||||||
import PageLayout from './[projectId]/page-layout';
|
import PageLayout from './[projectId]/page-layout';
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { getCurrentOrganizations } from '@/server/services/organization.service';
|
|
||||||
import { CreateOrganization } from '@clerk/nextjs';
|
import { CreateOrganization } from '@clerk/nextjs';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import { getCurrentOrganizations } from '@mixan/db';
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const organizations = await getCurrentOrganizations();
|
const organizations = await getCurrentOrganizations();
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import OverviewTopEvents from '@/components/overview/overview-top-events';
|
|||||||
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { getShareOverviewById } from '@mixan/db';
|
import { getOrganizationBySlug, getShareOverviewById } from '@mixan/db';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { appRouter } from '@/server/api/root';
|
import { appRouter } from '@/server/api/root';
|
||||||
import { getSession } from '@/server/auth';
|
|
||||||
import { getAuth } from '@clerk/nextjs/server';
|
import { getAuth } from '@clerk/nextjs/server';
|
||||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
||||||
|
|
||||||
@@ -9,9 +8,7 @@ const handler = (req: Request) =>
|
|||||||
req,
|
req,
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
async createContext({ req }) {
|
async createContext({ req }) {
|
||||||
console.log('------- createContext --------');
|
|
||||||
const session = getAuth(req as any);
|
const session = getAuth(req as any);
|
||||||
console.log('session', JSON.stringify(session, null, 2));
|
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
};
|
};
|
||||||
|
|||||||
20
apps/web/src/app/manifest.ts
Normal file
20
apps/web/src/app/manifest.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
name: 'Openpanel.dev',
|
||||||
|
short_name: 'Openpanel.dev',
|
||||||
|
description: '',
|
||||||
|
start_url: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
background_color: '#fff',
|
||||||
|
theme_color: '#fff',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: 'https://openpanel.dev/favicon.ico',
|
||||||
|
sizes: 'any',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
import { pushModal, showConfirm } from '@/modals';
|
import { pushModal, showConfirm } from '@/modals';
|
||||||
import type { IClientWithProject } from '@/types';
|
|
||||||
import { clipboard } from '@/utils/clipboard';
|
import { clipboard } from '@/utils/clipboard';
|
||||||
import { MoreHorizontal } from 'lucide-react';
|
import { MoreHorizontal } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import type { IServiceClientWithProject } from '@mixan/db';
|
||||||
|
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '../ui/dropdown-menu';
|
} from '../ui/dropdown-menu';
|
||||||
|
|
||||||
export function ClientActions(client: IClientWithProject) {
|
export function ClientActions(client: IServiceClientWithProject) {
|
||||||
const { id } = client;
|
const { id } = client;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const deletion = api.client.remove.useMutation({
|
const deletion = api.client.remove.useMutation({
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { IClientWithProject } from '@/types';
|
|
||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import type { IServiceClientWithProject } from '@mixan/db';
|
||||||
|
|
||||||
import { ClientActions } from './ClientActions';
|
import { ClientActions } from './ClientActions';
|
||||||
|
|
||||||
export const columns: ColumnDef<IClientWithProject>[] = [
|
export const columns: ColumnDef<IServiceClientWithProject>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Button } from '../ui/button';
|
|||||||
interface ExpandableListItemProps {
|
interface ExpandableListItemProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
content: React.ReactNode;
|
content: React.ReactNode;
|
||||||
title: string;
|
title: React.ReactNode;
|
||||||
image?: React.ReactNode;
|
image?: React.ReactNode;
|
||||||
initialOpen?: boolean;
|
initialOpen?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -29,7 +29,7 @@ export function ExpandableListItem({
|
|||||||
<div className="p-2 sm:p-4 flex gap-4">
|
<div className="p-2 sm:p-4 flex gap-4">
|
||||||
<div className="flex gap-1">{image}</div>
|
<div className="flex gap-1">{image}</div>
|
||||||
<div className="flex flex-col flex-1 gap-1 min-w-0">
|
<div className="flex flex-col flex-1 gap-1 min-w-0">
|
||||||
<span className="text-md font-medium leading-none mb-1">{title}</span>
|
<div className="text-md font-medium leading-none mb-1">{title}</div>
|
||||||
{!!content && (
|
{!!content && (
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4">
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import { api } from '@/app/_trpc/client';
|
|
||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
|
||||||
import { ChevronRight, HomeIcon } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
import { Container } from '../Container';
|
|
||||||
|
|
||||||
export function Breadcrumbs() {
|
|
||||||
const params = useOrganizationParams();
|
|
||||||
|
|
||||||
const org = api.organization.get.useQuery(
|
|
||||||
{
|
|
||||||
id: params.organizationId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!params.organizationId,
|
|
||||||
staleTime: Infinity,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const pro = api.project.get.useQuery(
|
|
||||||
{
|
|
||||||
id: params.projectId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!params.projectId,
|
|
||||||
staleTime: Infinity,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const dashboard = api.dashboard.get.useQuery(
|
|
||||||
{
|
|
||||||
id: params.dashboardId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!params.dashboardId,
|
|
||||||
staleTime: Infinity,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="border-b border-border text-xs">
|
|
||||||
<Container className="flex items-center gap-2 h-8">
|
|
||||||
{org.isLoading && pro.isLoading && (
|
|
||||||
<div className="animate-pulse bg-slate-200 h-4 w-24 rounded"></div>
|
|
||||||
)}
|
|
||||||
{org.data && (
|
|
||||||
<>
|
|
||||||
<HomeIcon size={14} />
|
|
||||||
<Link shallow href={`/${org.data.id}`}>
|
|
||||||
{org.data.name}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{org.data && pro.data && (
|
|
||||||
<>
|
|
||||||
<ChevronRight size={10} />
|
|
||||||
<Link shallow href={`/${org.data.id}/${pro.data.id}`}>
|
|
||||||
{pro.data.name}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{org.data && pro.data && dashboard.data && (
|
|
||||||
<>
|
|
||||||
<ChevronRight size={10} />
|
|
||||||
<Link
|
|
||||||
shallow
|
|
||||||
href={`/${org.data.id}/${pro.data.id}/${dashboard.data.id}`}
|
|
||||||
>
|
|
||||||
{dashboard.data.name}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
|
||||||
import { LineChart } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export function NavbarCreate() {
|
|
||||||
const params = useOrganizationParams();
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button>Create</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="w-56" align="end">
|
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link
|
|
||||||
shallow
|
|
||||||
href={`/${params.organizationId}/${params.projectId}/reports`}
|
|
||||||
>
|
|
||||||
<LineChart className="mr-2 h-4 w-4" />
|
|
||||||
<span>Create a report</span>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
|
||||||
import { cn } from '@/utils/cn';
|
|
||||||
import type { LinkProps } from 'next/link';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { strip } from '@mixan/common';
|
|
||||||
|
|
||||||
import { NavbarUserDropdown } from './NavbarUserDropdown';
|
|
||||||
|
|
||||||
function Item({
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: LinkProps & { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
{...props}
|
|
||||||
className="h-9 items-center flex px-3 leading-none relative [&>div]:hover:opacity-100 [&>div]:hover:ring-1"
|
|
||||||
shallow
|
|
||||||
>
|
|
||||||
<div className="opacity-0 absolute inset-0 transition-all bg-gradient-to-r from-blue-50 to-purple-50 rounded ring-0 ring-purple-900" />
|
|
||||||
<span className="relative">{children}</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NavbarMenu() {
|
|
||||||
const params = useOrganizationParams();
|
|
||||||
return (
|
|
||||||
<div className={cn('flex gap-1 items-center text-sm', 'max-sm:flex-col')}>
|
|
||||||
{params.projectId && (
|
|
||||||
<Item href={`/${params.organizationId}/${params.projectId}`}>
|
|
||||||
Dashboards
|
|
||||||
</Item>
|
|
||||||
)}
|
|
||||||
{params.projectId && (
|
|
||||||
<Item href={`/${params.organizationId}/${params.projectId}/events`}>
|
|
||||||
Events
|
|
||||||
</Item>
|
|
||||||
)}
|
|
||||||
{params.projectId && (
|
|
||||||
<Item href={`/${params.organizationId}/${params.projectId}/profiles`}>
|
|
||||||
Profiles
|
|
||||||
</Item>
|
|
||||||
)}
|
|
||||||
{params.projectId && (
|
|
||||||
<Item
|
|
||||||
href={{
|
|
||||||
pathname: `/${params.organizationId}/${params.projectId}/reports`,
|
|
||||||
query: strip({
|
|
||||||
dashboardId: params.dashboardId,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create report
|
|
||||||
</Item>
|
|
||||||
)}
|
|
||||||
<NavbarUserDropdown />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
import { useOrganizationParams } from '@/hooks/useOrganizationParams';
|
|
||||||
import { User } from 'lucide-react';
|
|
||||||
import { signOut, useSession } from 'next-auth/react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export function NavbarUserDropdown() {
|
|
||||||
const params = useOrganizationParams();
|
|
||||||
const session = useSession();
|
|
||||||
const user = session.data?.user;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<Avatar>
|
|
||||||
<AvatarFallback>{user?.name?.charAt(0) ?? '🤠'}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-[200px]">
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem asChild className="cursor-pointer">
|
|
||||||
<Link
|
|
||||||
href={`/${params.organizationId}/settings/organization`}
|
|
||||||
shallow
|
|
||||||
>
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
Organization
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild className="cursor-pointer">
|
|
||||||
<Link href={`/${params.organizationId}/settings/projects`} shallow>
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
Projects
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild className="cursor-pointer">
|
|
||||||
<Link href={`/${params.organizationId}/settings/clients`} shallow>
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
Clients
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild className="cursor-pointer">
|
|
||||||
<Link href={`/${params.organizationId}/settings/profile`} shallow>
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
Profile
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="text-red-600 cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
signOut().catch(console.error);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
Logout
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import {
|
||||||
|
useEventQueryFilters,
|
||||||
|
useEventQueryNamesFilter,
|
||||||
|
} from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { Options as NuqsOptions } from 'nuqs';
|
import type { Options as NuqsOptions } from 'nuqs';
|
||||||
|
|
||||||
interface OverviewFiltersButtonsProps {
|
interface OverviewFiltersButtonsProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -15,25 +18,40 @@ export function OverviewFiltersButtons({
|
|||||||
className,
|
className,
|
||||||
nuqsOptions,
|
nuqsOptions,
|
||||||
}: OverviewFiltersButtonsProps) {
|
}: OverviewFiltersButtonsProps) {
|
||||||
const eventQueryFilters = useEventQueryFilters(nuqsOptions);
|
const [events, setEvents] = useEventQueryNamesFilter(nuqsOptions);
|
||||||
const filters = Object.entries(eventQueryFilters).filter(
|
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
|
||||||
([, filter]) => filter.get !== null
|
if (filters.length === 0 && events.length === 0) return null;
|
||||||
);
|
|
||||||
if (filters.length === 0) return null;
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-wrap gap-2 px-4 pb-4', className)}>
|
<div className={cn('flex flex-wrap gap-2 px-4 pb-4', className)}>
|
||||||
{filters.map(([key, filter]) => (
|
{events.map((event) => (
|
||||||
<Button
|
<Button
|
||||||
key={key}
|
key={event}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
icon={X}
|
icon={X}
|
||||||
onClick={() => filter.set(null)}
|
onClick={() => setEvents((p) => p.filter((e) => e !== event))}
|
||||||
>
|
>
|
||||||
<span className="mr-1">{key} is</span>
|
<strong>{event}</strong>
|
||||||
<strong>{filter.get}</strong>
|
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
{filters.map((filter) => {
|
||||||
|
if (!filter.value[0]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={filter.name}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
icon={X}
|
||||||
|
onClick={() => setFilter(filter.name, filter.value[0], 'is')}
|
||||||
|
>
|
||||||
|
<span className="mr-1">{filter.name} is</span>
|
||||||
|
<strong>{filter.value[0]}</strong>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +1,131 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { api } from '@/app/_trpc/client';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||||
|
import { SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||||
|
import { useEventNames } from '@/hooks/useEventNames';
|
||||||
|
import { useEventProperties } from '@/hooks/useEventProperties';
|
||||||
|
import {
|
||||||
|
useEventQueryFilters,
|
||||||
|
useEventQueryNamesFilter,
|
||||||
|
} from '@/hooks/useEventQueryFilters';
|
||||||
|
import { useEventValues } from '@/hooks/useEventValues';
|
||||||
import { XIcon } from 'lucide-react';
|
import { XIcon } from 'lucide-react';
|
||||||
import { Options as NuqsOptions } from 'nuqs';
|
import type { Options as NuqsOptions } from 'nuqs';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
IChartEventFilter,
|
||||||
|
IChartEventFilterOperator,
|
||||||
|
IChartEventFilterValue,
|
||||||
|
} from '@mixan/validation';
|
||||||
|
|
||||||
interface OverviewFiltersProps {
|
interface OverviewFiltersProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
nuqsOptions?: NuqsOptions;
|
nuqsOptions?: NuqsOptions;
|
||||||
|
enableEventsFilter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverviewFiltersDrawerContent({
|
export function OverviewFiltersDrawerContent({
|
||||||
projectId,
|
projectId,
|
||||||
nuqsOptions,
|
nuqsOptions,
|
||||||
|
enableEventsFilter,
|
||||||
}: OverviewFiltersProps) {
|
}: OverviewFiltersProps) {
|
||||||
const eventQueryFilters = useEventQueryFilters(nuqsOptions);
|
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
|
||||||
|
const [event, setEvent] = useEventQueryNamesFilter(nuqsOptions);
|
||||||
|
const eventNames = useEventNames(projectId);
|
||||||
|
const eventProperties = useEventProperties(projectId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-medium mb-8">Overview filters</h2>
|
<SheetHeader className="mb-8">
|
||||||
<Combobox
|
<SheetTitle>Overview filters</SheetTitle>
|
||||||
className="w-full"
|
</SheetHeader>
|
||||||
onChange={(value) => {
|
|
||||||
// @ts-expect-error
|
<div className="flex flex-col gap-4">
|
||||||
eventQueryFilters[value].set('');
|
{enableEventsFilter && (
|
||||||
}}
|
<ComboboxAdvanced
|
||||||
value=""
|
className="w-full"
|
||||||
placeholder="Filter by..."
|
value={event}
|
||||||
label="What do you want to filter by?"
|
onChange={setEvent}
|
||||||
items={Object.entries(eventQueryFilters)
|
// First items is * which is only used for report editing
|
||||||
.filter(([, filter]) => filter.get === null)
|
items={eventNames.slice(1).map((item) => ({
|
||||||
.map(([name]) => ({
|
label: item.name,
|
||||||
label: name,
|
value: item.name,
|
||||||
value: name,
|
}))}
|
||||||
|
placeholder="Select event"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Combobox
|
||||||
|
className="w-full"
|
||||||
|
onChange={(value) => {
|
||||||
|
setFilter(value, '');
|
||||||
|
}}
|
||||||
|
value=""
|
||||||
|
placeholder="Filter by property"
|
||||||
|
label="What do you want to filter by?"
|
||||||
|
items={eventProperties.map((item) => ({
|
||||||
|
label: item,
|
||||||
|
value: item,
|
||||||
}))}
|
}))}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-8">
|
<div className="flex flex-col gap-4 mt-8">
|
||||||
{Object.entries(eventQueryFilters)
|
{filters
|
||||||
.filter(([, filter]) => filter.get !== null)
|
.filter((filter) => filter.value[0] !== null)
|
||||||
.map(([name, filter]) => (
|
.map((filter) => {
|
||||||
<FilterOption
|
return (
|
||||||
key={name}
|
<FilterOption
|
||||||
projectId={projectId}
|
key={filter.name}
|
||||||
name={name}
|
projectId={projectId}
|
||||||
{...filter}
|
setFilter={setFilter}
|
||||||
/>
|
{...filter}
|
||||||
))}
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterOption({
|
export function FilterOption({
|
||||||
name,
|
setFilter,
|
||||||
get,
|
|
||||||
set,
|
|
||||||
projectId,
|
projectId,
|
||||||
}: {
|
...filter
|
||||||
name: string;
|
}: IChartEventFilter & {
|
||||||
get: string | null;
|
|
||||||
set: (value: string | null) => void;
|
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
setFilter: (
|
||||||
|
name: string,
|
||||||
|
value: IChartEventFilterValue,
|
||||||
|
operator: IChartEventFilterOperator
|
||||||
|
) => void;
|
||||||
}) {
|
}) {
|
||||||
const { data } = api.chart.values.useQuery({
|
const values = useEventValues(
|
||||||
projectId,
|
projectId,
|
||||||
event: name === 'path' ? 'screen_view' : 'session_start',
|
filter.name === 'path' ? 'screen_view' : 'session_start',
|
||||||
property: name,
|
filter.name
|
||||||
});
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div>{name}</div>
|
<div>{filter.name}</div>
|
||||||
<Combobox
|
<Combobox
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
onChange={(value) => set(value)}
|
onChange={(value) => setFilter(filter.name, value, filter.operator)}
|
||||||
placeholder={'Select a value'}
|
placeholder={'Select a value'}
|
||||||
items={
|
items={values.map((value) => ({
|
||||||
data?.values.filter(Boolean).map((value) => ({
|
value,
|
||||||
value,
|
label: value,
|
||||||
label: value,
|
}))}
|
||||||
})) ?? []
|
value={String(filter.value[0] ?? '')}
|
||||||
}
|
|
||||||
value={get}
|
|
||||||
/>
|
/>
|
||||||
<Button size="icon" variant="ghost" onClick={() => set(null)}>
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
setFilter(filter.name, filter.value[0] ?? '', filter.operator)
|
||||||
|
}
|
||||||
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,18 +3,20 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
import { FilterIcon } from 'lucide-react';
|
import { FilterIcon } from 'lucide-react';
|
||||||
import { Options as NuqsOptions } from 'nuqs';
|
import type { Options as NuqsOptions } from 'nuqs';
|
||||||
|
|
||||||
import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
|
import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
|
||||||
|
|
||||||
interface OverviewFiltersDrawerProps {
|
interface OverviewFiltersDrawerProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
nuqsOptions?: NuqsOptions;
|
nuqsOptions?: NuqsOptions;
|
||||||
|
enableEventsFilter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverviewFiltersDrawer({
|
export function OverviewFiltersDrawer({
|
||||||
projectId,
|
projectId,
|
||||||
nuqsOptions,
|
nuqsOptions,
|
||||||
|
enableEventsFilter,
|
||||||
}: OverviewFiltersDrawerProps) {
|
}: OverviewFiltersDrawerProps) {
|
||||||
return (
|
return (
|
||||||
<Sheet>
|
<Sheet>
|
||||||
@@ -27,6 +29,7 @@ export function OverviewFiltersDrawer({
|
|||||||
<OverviewFiltersDrawerContent
|
<OverviewFiltersDrawerContent
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
nuqsOptions={nuqsOptions}
|
nuqsOptions={nuqsOptions}
|
||||||
|
enableEventsFilter={enableEventsFilter}
|
||||||
/>
|
/>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { IChartInput } from '@/types';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||||
import AnimateHeight from 'react-animate-height';
|
import AnimateHeight from 'react-animate-height';
|
||||||
|
|
||||||
|
import type { IChartInput } from '@mixan/validation';
|
||||||
|
|
||||||
import { Chart } from '../report/chart';
|
import { Chart } from '../report/chart';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '../Widget';
|
import { Widget, WidgetBody, WidgetHead } from '../Widget';
|
||||||
import { useOverviewOptions } from './useOverviewOptions';
|
import { useOverviewOptions } from './useOverviewOptions';
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Chart } from '@/components/report/chart';
|
import { Chart } from '@/components/report/chart';
|
||||||
import {
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
useEventFilters,
|
|
||||||
useEventQueryFilters,
|
|
||||||
} from '@/hooks/useEventQueryFilters';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -19,9 +16,7 @@ export default function OverviewTopDevices({
|
|||||||
projectId,
|
projectId,
|
||||||
}: OverviewTopDevicesProps) {
|
}: OverviewTopDevicesProps) {
|
||||||
const { interval, range, previous } = useOverviewOptions();
|
const { interval, range, previous } = useOverviewOptions();
|
||||||
const filters = useEventFilters();
|
const [filters, setFilter] = useEventQueryFilters();
|
||||||
const { device, browser, browserVersion, os, osVersion } =
|
|
||||||
useEventQueryFilters();
|
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
||||||
devices: {
|
devices: {
|
||||||
title: 'Top devices',
|
title: 'Top devices',
|
||||||
@@ -190,21 +185,21 @@ export default function OverviewTopDevices({
|
|||||||
onClick={(item) => {
|
onClick={(item) => {
|
||||||
switch (widget.key) {
|
switch (widget.key) {
|
||||||
case 'devices':
|
case 'devices':
|
||||||
device.set(item.name);
|
setFilter('device', item.name);
|
||||||
break;
|
break;
|
||||||
case 'browser':
|
case 'browser':
|
||||||
setWidget('browser_version');
|
setWidget('browser_version');
|
||||||
browser.set(item.name);
|
setFilter('browser', item.name);
|
||||||
break;
|
break;
|
||||||
case 'browser_version':
|
case 'browser_version':
|
||||||
browserVersion.set(item.name);
|
setFilter('browser_version', item.name);
|
||||||
break;
|
break;
|
||||||
case 'os':
|
case 'os':
|
||||||
setWidget('os_version');
|
setWidget('os_version');
|
||||||
os.set(item.name);
|
setFilter('os', item.name);
|
||||||
break;
|
break;
|
||||||
case 'os_version':
|
case 'os_version':
|
||||||
osVersion.set(item.name);
|
setFilter('os_version', item.name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Chart } from '@/components/report/chart';
|
import { Chart } from '@/components/report/chart';
|
||||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -16,7 +16,7 @@ export default function OverviewTopEvents({
|
|||||||
projectId,
|
projectId,
|
||||||
}: OverviewTopEventsProps) {
|
}: OverviewTopEventsProps) {
|
||||||
const { interval, range, previous } = useOverviewOptions();
|
const { interval, range, previous } = useOverviewOptions();
|
||||||
const filters = useEventFilters();
|
const [filters] = useEventQueryFilters();
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
||||||
all: {
|
all: {
|
||||||
title: 'Top events',
|
title: 'Top events',
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Chart } from '@/components/report/chart';
|
import { Chart } from '@/components/report/chart';
|
||||||
import {
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
useEventFilters,
|
|
||||||
useEventQueryFilters,
|
|
||||||
} from '@/hooks/useEventQueryFilters';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -17,8 +14,7 @@ interface OverviewTopGeoProps {
|
|||||||
}
|
}
|
||||||
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||||
const { interval, range, previous } = useOverviewOptions();
|
const { interval, range, previous } = useOverviewOptions();
|
||||||
const filters = useEventFilters();
|
const [filters, setFilter] = useEventQueryFilters();
|
||||||
const { region, country, city } = useEventQueryFilters();
|
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
||||||
map: {
|
map: {
|
||||||
title: 'Map',
|
title: 'Map',
|
||||||
@@ -160,14 +156,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
|||||||
switch (widget.key) {
|
switch (widget.key) {
|
||||||
case 'countries':
|
case 'countries':
|
||||||
setWidget('regions');
|
setWidget('regions');
|
||||||
country.set(item.name);
|
setFilter('country', item.name);
|
||||||
break;
|
break;
|
||||||
case 'regions':
|
case 'regions':
|
||||||
setWidget('cities');
|
setWidget('cities');
|
||||||
region.set(item.name);
|
setFilter('region', item.name);
|
||||||
break;
|
break;
|
||||||
case 'cities':
|
case 'cities':
|
||||||
city.set(item.name);
|
setFilter('city', item.name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Chart } from '@/components/report/chart';
|
import { Chart } from '@/components/report/chart';
|
||||||
import {
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
useEventFilters,
|
|
||||||
useEventQueryFilters,
|
|
||||||
} from '@/hooks/useEventQueryFilters';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -17,8 +14,7 @@ interface OverviewTopPagesProps {
|
|||||||
}
|
}
|
||||||
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||||
const { interval, range, previous } = useOverviewOptions();
|
const { interval, range, previous } = useOverviewOptions();
|
||||||
const filters = useEventFilters();
|
const [filters, setFilter] = useEventQueryFilters();
|
||||||
const { path } = useEventQueryFilters();
|
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||||
top: {
|
top: {
|
||||||
title: 'Top pages',
|
title: 'Top pages',
|
||||||
@@ -129,7 +125,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
{...widget.chart}
|
{...widget.chart}
|
||||||
previous={false}
|
previous={false}
|
||||||
onClick={(item) => {
|
onClick={(item) => {
|
||||||
path.set(item.name);
|
setFilter('path', item.name);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</WidgetBody>
|
</WidgetBody>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Chart } from '@/components/report/chart';
|
import { Chart } from '@/components/report/chart';
|
||||||
import {
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
useEventFilters,
|
|
||||||
useEventQueryFilters,
|
|
||||||
} from '@/hooks/useEventQueryFilters';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import { Widget, WidgetBody } from '../Widget';
|
import { Widget, WidgetBody } from '../Widget';
|
||||||
@@ -19,17 +16,7 @@ export default function OverviewTopSources({
|
|||||||
projectId,
|
projectId,
|
||||||
}: OverviewTopSourcesProps) {
|
}: OverviewTopSourcesProps) {
|
||||||
const { interval, range, previous } = useOverviewOptions();
|
const { interval, range, previous } = useOverviewOptions();
|
||||||
const {
|
const [filters, setFilter] = useEventQueryFilters();
|
||||||
referrer,
|
|
||||||
referrerName,
|
|
||||||
referrerType,
|
|
||||||
utmCampaign,
|
|
||||||
utmContent,
|
|
||||||
utmMedium,
|
|
||||||
utmSource,
|
|
||||||
utmTerm,
|
|
||||||
} = useEventQueryFilters();
|
|
||||||
const filters = useEventFilters();
|
|
||||||
const [widget, setWidget, widgets] = useOverviewWidget('sources', {
|
const [widget, setWidget, widgets] = useOverviewWidget('sources', {
|
||||||
all: {
|
all: {
|
||||||
title: 'Top sources',
|
title: 'Top sources',
|
||||||
@@ -282,30 +269,30 @@ export default function OverviewTopSources({
|
|||||||
onClick={(item) => {
|
onClick={(item) => {
|
||||||
switch (widget.key) {
|
switch (widget.key) {
|
||||||
case 'all':
|
case 'all':
|
||||||
referrerName.set(item.name);
|
setFilter('referrer_name', item.name);
|
||||||
setWidget('domain');
|
setWidget('domain');
|
||||||
break;
|
break;
|
||||||
case 'domain':
|
case 'domain':
|
||||||
referrer.set(item.name);
|
setFilter('referrer', item.name);
|
||||||
break;
|
break;
|
||||||
case 'type':
|
case 'type':
|
||||||
referrerType.set(item.name);
|
setFilter('referrer_type', item.name);
|
||||||
setWidget('domain');
|
setWidget('domain');
|
||||||
break;
|
break;
|
||||||
case 'utm_source':
|
case 'utm_source':
|
||||||
utmSource.set(item.name);
|
setFilter('utm_source', item.name);
|
||||||
break;
|
break;
|
||||||
case 'utm_medium':
|
case 'utm_medium':
|
||||||
utmMedium.set(item.name);
|
setFilter('utm_medium', item.name);
|
||||||
break;
|
break;
|
||||||
case 'utm_campaign':
|
case 'utm_campaign':
|
||||||
utmCampaign.set(item.name);
|
setFilter('utm_campaign', item.name);
|
||||||
break;
|
break;
|
||||||
case 'utm_term':
|
case 'utm_term':
|
||||||
utmTerm.set(item.name);
|
setFilter('utm_term', item.name);
|
||||||
break;
|
break;
|
||||||
case 'utm_content':
|
case 'utm_content':
|
||||||
utmContent.set(item.name);
|
setFilter('utm_content', item.name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
|
||||||
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
|
||||||
import { mapKeys } from '@/utils/validation';
|
|
||||||
import {
|
import {
|
||||||
parseAsBoolean,
|
parseAsBoolean,
|
||||||
parseAsInteger,
|
parseAsInteger,
|
||||||
@@ -8,6 +5,9 @@ import {
|
|||||||
useQueryState,
|
useQueryState,
|
||||||
} from 'nuqs';
|
} from 'nuqs';
|
||||||
|
|
||||||
|
import { getDefaultIntervalByRange, timeRanges } from '@mixan/constants';
|
||||||
|
import { mapKeys } from '@mixan/validation';
|
||||||
|
|
||||||
const nuqsOptions = { history: 'push' } as const;
|
const nuqsOptions = { history: 'push' } as const;
|
||||||
|
|
||||||
export function useOverviewOptions() {
|
export function useOverviewOptions() {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { IChartInput } from '@/types';
|
|
||||||
import { mapKeys } from '@/utils/validation';
|
|
||||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||||
|
|
||||||
|
import { mapKeys } from '@mixan/validation';
|
||||||
|
import type { IChartInput } from '@mixan/validation';
|
||||||
|
|
||||||
export function useOverviewWidget<T extends string>(
|
export function useOverviewWidget<T extends string>(
|
||||||
key: string,
|
key: string,
|
||||||
widgets: Record<T, { title: string; btn: string; chart: IChartInput }>
|
widgets: Record<T, { title: string; btn: string; chart: IChartInput }>
|
||||||
@@ -15,7 +16,7 @@ export function useOverviewWidget<T extends string>(
|
|||||||
);
|
);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
...widgets[widget]!,
|
...widgets[widget],
|
||||||
key: widget,
|
key: widget,
|
||||||
},
|
},
|
||||||
setWidget,
|
setWidget,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { IServiceProfile } from '@/server/services/profile.service';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { AvatarImage } from '@radix-ui/react-avatar';
|
import { AvatarImage } from '@radix-ui/react-avatar';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
|
|
||||||
|
import type { IServiceProfile } from '@mixan/db';
|
||||||
|
|
||||||
import { Avatar, AvatarFallback } from '../ui/avatar';
|
import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||||
|
|
||||||
interface ProfileAvatarProps
|
interface ProfileAvatarProps
|
||||||
@@ -41,8 +42,8 @@ export function ProfileAvatar({
|
|||||||
size === 'sm'
|
size === 'sm'
|
||||||
? 'text-xs'
|
? 'text-xs'
|
||||||
: size === 'xs'
|
: size === 'xs'
|
||||||
? 'text-[8px]'
|
? 'text-[8px]'
|
||||||
: 'text-base',
|
: 'text-base',
|
||||||
'bg-slate-200 text-slate-800'
|
'bg-slate-200 text-slate-800'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
import { pushModal, showConfirm } from '@/modals';
|
import { pushModal, showConfirm } from '@/modals';
|
||||||
import type { IProject } from '@/types';
|
|
||||||
import { clipboard } from '@/utils/clipboard';
|
import { clipboard } from '@/utils/clipboard';
|
||||||
import { MoreHorizontal } from 'lucide-react';
|
import { MoreHorizontal } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import type { IServiceProject } from '@mixan/db';
|
||||||
|
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '../ui/dropdown-menu';
|
} from '../ui/dropdown-menu';
|
||||||
|
|
||||||
export function ProjectActions(project: IProject) {
|
export function ProjectActions(project: Exclude<IServiceProject, null>) {
|
||||||
const { id } = project;
|
const { id } = project;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const deletion = api.project.remove.useMutation({
|
const deletion = api.project.remove.useMutation({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IServiceProject } from '@/server/services/project.service';
|
|
||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { IServiceProject } from '@mixan/db';
|
||||||
import type { Project as IProject } from '@mixan/db';
|
import type { Project as IProject } from '@mixan/db';
|
||||||
|
|
||||||
import { ProjectActions } from './ProjectActions';
|
import { ProjectActions } from './ProjectActions';
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import { chartTypes } from '@/utils/constants';
|
|
||||||
import { objectToZodEnums } from '@/utils/validation';
|
|
||||||
import { LineChartIcon } from 'lucide-react';
|
import { LineChartIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { chartTypes } from '@mixan/constants';
|
||||||
|
import { objectToZodEnums } from '@mixan/validation';
|
||||||
|
|
||||||
import { Combobox } from '../ui/combobox';
|
import { Combobox } from '../ui/combobox';
|
||||||
import { changeChartType } from './reportSlice';
|
import { changeChartType } from './reportSlice';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import type { IInterval } from '@/types';
|
import { ClockIcon } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isHourIntervalEnabledByRange,
|
isHourIntervalEnabledByRange,
|
||||||
isMinuteIntervalEnabledByRange,
|
isMinuteIntervalEnabledByRange,
|
||||||
} from '@/utils/constants';
|
} from '@mixan/constants';
|
||||||
import { ClockIcon } from 'lucide-react';
|
import type { IInterval } from '@mixan/validation';
|
||||||
|
|
||||||
import { Combobox } from '../ui/combobox';
|
import { Combobox } from '../ui/combobox';
|
||||||
import { changeInterval } from './reportSlice';
|
import { changeInterval } from './reportSlice';
|
||||||
@@ -32,7 +33,7 @@ export function ReportInterval({ className }: ReportIntervalProps) {
|
|||||||
className={className}
|
className={className}
|
||||||
placeholder="Interval"
|
placeholder="Interval"
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
dispatch(changeInterval(value as IInterval));
|
dispatch(changeInterval(value));
|
||||||
}}
|
}}
|
||||||
value={interval}
|
value={interval}
|
||||||
items={[
|
items={[
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import { lineTypes } from '@/utils/constants';
|
|
||||||
import { objectToZodEnums } from '@/utils/validation';
|
|
||||||
import { Tv2Icon } from 'lucide-react';
|
import { Tv2Icon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { lineTypes } from '@mixan/constants';
|
||||||
|
import { objectToZodEnums } from '@mixan/validation';
|
||||||
|
|
||||||
import { Combobox } from '../ui/combobox';
|
import { Combobox } from '../ui/combobox';
|
||||||
import { changeLineType } from './reportSlice';
|
import { changeLineType } from './reportSlice';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { IChartRange } from '@/types';
|
|
||||||
import { timeRanges } from '@/utils/constants';
|
|
||||||
import { CalendarIcon } from 'lucide-react';
|
import { CalendarIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { timeRanges } from '@mixan/constants';
|
||||||
|
import type { IChartRange } from '@mixan/validation';
|
||||||
|
|
||||||
import type { ExtendedComboboxProps } from '../ui/combobox';
|
import type { ExtendedComboboxProps } from '../ui/combobox';
|
||||||
import { Combobox } from '../ui/combobox';
|
import { Combobox } from '../ui/combobox';
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { IChartSerie } from '@/server/api/routers/chart';
|
import type { IChartSerie } from '@/server/api/routers/chart';
|
||||||
import type { IChartInput } from '@/types';
|
|
||||||
|
import type { IChartInput } from '@mixan/validation';
|
||||||
|
|
||||||
import { ChartLoading } from './ChartLoading';
|
import { ChartLoading } from './ChartLoading';
|
||||||
import { MetricCardLoading } from './MetricCard';
|
import { MetricCardLoading } from './MetricCard';
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
import type { IChartData } from '@/app/_trpc/client';
|
import type { IChartData } from '@/app/_trpc/client';
|
||||||
import { ColorSquare } from '@/components/ColorSquare';
|
import { ColorSquare } from '@/components/ColorSquare';
|
||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import type { IChartMetric } from '@/types';
|
|
||||||
import { theme } from '@/utils/theme';
|
import { theme } from '@/utils/theme';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { Area, AreaChart } from 'recharts';
|
import { Area, AreaChart } from 'recharts';
|
||||||
|
|
||||||
|
import type { IChartMetric } from '@mixan/validation';
|
||||||
|
|
||||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||||
|
|
||||||
interface MetricCardProps {
|
interface MetricCardProps {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { AutoSizer } from '@/components/AutoSizer';
|
|||||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||||
import type { IChartLineType, IInterval } from '@/types';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +15,8 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
|
||||||
|
import type { IChartLineType, IInterval } from '@mixan/validation';
|
||||||
|
|
||||||
import { getYAxisWidth } from './chart-utils';
|
import { getYAxisWidth } from './chart-utils';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import type { IChartData } from '@/app/_trpc/client';
|
|||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { NOT_SET_VALUE } from '@/utils/constants';
|
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
|
|
||||||
|
import { NOT_SET_VALUE } from '@mixan/constants';
|
||||||
|
|
||||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { SerieIcon } from './SerieIcon';
|
import { SerieIcon } from './SerieIcon';
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
|||||||
import { useMappings } from '@/hooks/useMappings';
|
import { useMappings } from '@/hooks/useMappings';
|
||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
|
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
|
||||||
import { useSelector } from '@/redux';
|
|
||||||
import type { IToolTipProps } from '@/types';
|
import type { IToolTipProps } from '@/types';
|
||||||
|
|
||||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { AutoSizer } from '@/components/AutoSizer';
|
|||||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||||
import type { IInterval } from '@/types';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getChartColor, theme } from '@/utils/theme';
|
import { getChartColor, theme } from '@/utils/theme';
|
||||||
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
|
||||||
|
import type { IInterval } from '@mixan/validation';
|
||||||
|
|
||||||
import { getYAxisWidth } from './chart-utils';
|
import { getYAxisWidth } from './chart-utils';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { AutoSizer } from '@/components/AutoSizer';
|
|||||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||||
import type { IChartLineType, IInterval } from '@/types';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +17,8 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
|
||||||
|
import type { IChartLineType, IInterval } from '@mixan/validation';
|
||||||
|
|
||||||
import { getYAxisWidth } from './chart-utils';
|
import { getYAxisWidth } from './chart-utils';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { NOT_SET_VALUE } from '@/utils/constants';
|
|
||||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
ActivityIcon,
|
ActivityIcon,
|
||||||
@@ -15,6 +14,8 @@ import {
|
|||||||
TabletIcon,
|
TabletIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import { NOT_SET_VALUE } from '@mixan/constants';
|
||||||
|
|
||||||
interface SerieIconProps extends LucideProps {
|
interface SerieIconProps extends LucideProps {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import type { RouterOutputs } from '@/app/_trpc/client';
|
import type { RouterOutputs } from '@/app/_trpc/client';
|
||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
import type { IChartInput } from '@/types';
|
|
||||||
|
import type { IChartInput } from '@mixan/validation';
|
||||||
|
|
||||||
import { ChartEmpty } from './ChartEmpty';
|
import { ChartEmpty } from './ChartEmpty';
|
||||||
import { ChartLoading } from './ChartLoading';
|
import { ChartLoading } from './ChartLoading';
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
alphabetIds,
|
||||||
|
getDefaultIntervalByRange,
|
||||||
|
isHourIntervalEnabledByRange,
|
||||||
|
isMinuteIntervalEnabledByRange,
|
||||||
|
} from '@mixan/constants';
|
||||||
import type {
|
import type {
|
||||||
IChartBreakdown,
|
IChartBreakdown,
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
@@ -6,15 +15,7 @@ import type {
|
|||||||
IChartRange,
|
IChartRange,
|
||||||
IChartType,
|
IChartType,
|
||||||
IInterval,
|
IInterval,
|
||||||
} from '@/types';
|
} from '@mixan/validation';
|
||||||
import {
|
|
||||||
alphabetIds,
|
|
||||||
getDefaultIntervalByRange,
|
|
||||||
isHourIntervalEnabledByRange,
|
|
||||||
isMinuteIntervalEnabledByRange,
|
|
||||||
} from '@/utils/constants';
|
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
type InitialState = IChartInput & {
|
type InitialState = IChartInput & {
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { api } from '@/app/_trpc/client';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDispatch } from '@/redux';
|
import { useDispatch } from '@/redux';
|
||||||
import type { IChartEvent } from '@/types';
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { DatabaseIcon } from 'lucide-react';
|
import { DatabaseIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { IChartEvent } from '@mixan/validation';
|
||||||
|
|
||||||
import { changeEvent } from '../reportSlice';
|
import { changeEvent } from '../reportSlice';
|
||||||
|
|
||||||
interface EventPropertiesComboboxProps {
|
interface EventPropertiesComboboxProps {
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { ColorSquare } from '@/components/ColorSquare';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import type { IChartBreakdown } from '@/types';
|
|
||||||
import { SplitIcon } from 'lucide-react';
|
import { SplitIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { IChartBreakdown } from '@mixan/validation';
|
||||||
|
|
||||||
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
|
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
|
||||||
import { ReportBreakdownMore } from './ReportBreakdownMore';
|
import { ReportBreakdownMore } from './ReportBreakdownMore';
|
||||||
import type { ReportEventMoreProps } from './ReportEventMore';
|
import type { ReportEventMoreProps } from './ReportEventMore';
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api } from '@/app/_trpc/client';
|
|
||||||
import { ColorSquare } from '@/components/ColorSquare';
|
import { ColorSquare } from '@/components/ColorSquare';
|
||||||
import { Dropdown } from '@/components/Dropdown';
|
import { Dropdown } from '@/components/Dropdown';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
@@ -8,10 +7,12 @@ import { Combobox } from '@/components/ui/combobox';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
||||||
|
import { useEventNames } from '@/hooks/useEventNames';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import type { IChartEvent } from '@/types';
|
|
||||||
import { GanttChart, GanttChartIcon, Users } from 'lucide-react';
|
import { GanttChart, GanttChartIcon, Users } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { IChartEvent } from '@mixan/validation';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addEvent,
|
addEvent,
|
||||||
changeEvent,
|
changeEvent,
|
||||||
@@ -29,14 +30,8 @@ export function ReportEvents() {
|
|||||||
const selectedEvents = useSelector((state) => state.report.events);
|
const selectedEvents = useSelector((state) => state.report.events);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { projectId } = useAppParams();
|
const { projectId } = useAppParams();
|
||||||
|
const eventNames = useEventNames(projectId);
|
||||||
|
|
||||||
const eventsQuery = api.chart.events.useQuery({
|
|
||||||
projectId,
|
|
||||||
});
|
|
||||||
const eventsCombobox = (eventsQuery.data ?? []).map((item) => ({
|
|
||||||
value: item.name,
|
|
||||||
label: item.name,
|
|
||||||
}));
|
|
||||||
const dispatchChangeEvent = useDebounceFn((event: IChartEvent) => {
|
const dispatchChangeEvent = useDebounceFn((event: IChartEvent) => {
|
||||||
dispatch(changeEvent(event));
|
dispatch(changeEvent(event));
|
||||||
});
|
});
|
||||||
@@ -76,7 +71,10 @@ export function ReportEvents() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
items={eventsCombobox}
|
items={eventNames.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.name,
|
||||||
|
}))}
|
||||||
placeholder="Select event"
|
placeholder="Select event"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -189,7 +187,10 @@ export function ReportEvents() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
items={eventsCombobox}
|
items={eventNames.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.name,
|
||||||
|
}))}
|
||||||
placeholder="Select event"
|
placeholder="Select event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import { RenderDots } from '@/components/ui/RenderDots';
|
|||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useMappings } from '@/hooks/useMappings';
|
import { useMappings } from '@/hooks/useMappings';
|
||||||
import { useDispatch } from '@/redux';
|
import { useDispatch } from '@/redux';
|
||||||
|
import { SlidersHorizontal, Trash } from 'lucide-react';
|
||||||
|
|
||||||
|
import { operators } from '@mixan/constants';
|
||||||
import type {
|
import type {
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
IChartEventFilter,
|
IChartEventFilterOperator,
|
||||||
IChartEventFilterValue,
|
IChartEventFilterValue,
|
||||||
} from '@/types';
|
} from '@mixan/validation';
|
||||||
import { operators } from '@/utils/constants';
|
import { mapKeys } from '@mixan/validation';
|
||||||
import { SlidersHorizontal, Trash } from 'lucide-react';
|
|
||||||
|
|
||||||
import { changeEvent } from '../../reportSlice';
|
import { changeEvent } from '../../reportSlice';
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@ export function FilterItem({ filter, event }: FilterProps) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeFilterOperator = (operator: IChartEventFilter['operator']) => {
|
const changeFilterOperator = (operator: IChartEventFilterOperator) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
changeEvent({
|
changeEvent({
|
||||||
...event,
|
...event,
|
||||||
@@ -104,9 +106,9 @@ export function FilterItem({ filter, event }: FilterProps) {
|
|||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
onChange={changeFilterOperator}
|
onChange={changeFilterOperator}
|
||||||
items={Object.entries(operators).map(([key, value]) => ({
|
items={mapKeys(operators).map((key) => ({
|
||||||
value: key as IChartEventFilter['operator'],
|
value: key,
|
||||||
label: value,
|
label: operators[key],
|
||||||
}))}
|
}))}
|
||||||
label="Operator"
|
label="Operator"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { api } from '@/app/_trpc/client';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDispatch } from '@/redux';
|
import { useDispatch } from '@/redux';
|
||||||
import type { IChartEvent } from '@/types';
|
|
||||||
import { FilterIcon } from 'lucide-react';
|
import { FilterIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { IChartEvent } from '@mixan/validation';
|
||||||
|
|
||||||
import { changeEvent } from '../../reportSlice';
|
import { changeEvent } from '../../reportSlice';
|
||||||
|
|
||||||
interface FiltersComboboxProps {
|
interface FiltersComboboxProps {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { IChartEvent } from '@/types';
|
import type { IChartEvent } from '@mixan/validation';
|
||||||
|
|
||||||
import { FilterItem } from './FilterItem';
|
import { FilterItem } from './FilterItem';
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface ComboboxAdvancedProps {
|
|||||||
onChange: React.Dispatch<React.SetStateAction<IValue[]>>;
|
onChange: React.Dispatch<React.SetStateAction<IValue[]>>;
|
||||||
items: IItem[];
|
items: IItem[];
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComboboxAdvanced({
|
export function ComboboxAdvanced({
|
||||||
@@ -32,6 +33,7 @@ export function ComboboxAdvanced({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
className,
|
||||||
}: ComboboxAdvancedProps) {
|
}: ComboboxAdvancedProps) {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [inputValue, setInputValue] = React.useState('');
|
const [inputValue, setInputValue] = React.useState('');
|
||||||
@@ -81,8 +83,12 @@ export function ComboboxAdvanced({
|
|||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant={'outline'} onClick={() => setOpen((prev) => !prev)}>
|
<Button
|
||||||
<div className="flex gap-1 flex-wrap">
|
variant={'outline'}
|
||||||
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<div className="flex gap-1 flex-wrap w-full">
|
||||||
{value.length === 0 && placeholder}
|
{value.length === 0 && placeholder}
|
||||||
{value.slice(0, 2).map((value) => {
|
{value.slice(0, 2).map((value) => {
|
||||||
const item = items.find((item) => item.value === value) ?? {
|
const item = items.find((item) => item.value === value) ?? {
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
|
|
||||||
export function useEventNames(projectId: string) {
|
export function useEventNames(projectId: string) {
|
||||||
const filterEventsQuery = api.chart.events.useQuery({
|
const query = api.chart.events.useQuery({
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (filterEventsQuery.data ?? []).map((item) => ({
|
return query.data ?? [];
|
||||||
value: item.name,
|
|
||||||
label: item.name,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
apps/web/src/hooks/useEventProperties.ts
Normal file
10
apps/web/src/hooks/useEventProperties.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { api } from '@/app/_trpc/client';
|
||||||
|
|
||||||
|
export function useEventProperties(projectId: string, event?: string) {
|
||||||
|
const query = api.chart.properties.useQuery({
|
||||||
|
projectId: projectId,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.data ?? [];
|
||||||
|
}
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import type { IChartInput } from '@/types';
|
|
||||||
import { parseAsString, useQueryState } from 'nuqs';
|
|
||||||
|
|
||||||
const nuqsOptions = { history: 'push' } as const;
|
|
||||||
|
|
||||||
export function useEventQueryFiltersqweqweqweqweqwe() {
|
|
||||||
// Path
|
|
||||||
const [path, setPath] = useQueryState(
|
|
||||||
'path',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Referrer
|
|
||||||
const [referrer, setReferrer] = useQueryState(
|
|
||||||
'referrer',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [referrerName, setReferrerName] = useQueryState(
|
|
||||||
'referrer_name',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [referrerType, setReferrerType] = useQueryState(
|
|
||||||
'referrer_type',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sources
|
|
||||||
const [utmSource, setUtmSource] = useQueryState(
|
|
||||||
'utm_source',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [utmMedium, setUtmMedium] = useQueryState(
|
|
||||||
'utm_medium',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [utmCampaign, setUtmCampaign] = useQueryState(
|
|
||||||
'utm_campaign',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [utmContent, setUtmContent] = useQueryState(
|
|
||||||
'utm_content',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [utmTerm, setUtmTerm] = useQueryState(
|
|
||||||
'utm_term',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Geo
|
|
||||||
const [country, setCountry] = useQueryState(
|
|
||||||
'country',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [region, setRegion] = useQueryState(
|
|
||||||
'region',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [city, setCity] = useQueryState(
|
|
||||||
'city',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
// tech
|
|
||||||
const [device, setDevice] = useQueryState(
|
|
||||||
'device',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [browser, setBrowser] = useQueryState(
|
|
||||||
'browser',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [browserVersion, setBrowserVersion] = useQueryState(
|
|
||||||
'browser_version',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [os, setOS] = useQueryState(
|
|
||||||
'os',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
const [osVersion, setOSVersion] = useQueryState(
|
|
||||||
'os_version',
|
|
||||||
parseAsString.withOptions(nuqsOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
const filters = useMemo(() => {
|
|
||||||
const filters: IChartInput['events'][number]['filters'] = [];
|
|
||||||
|
|
||||||
if (path) {
|
|
||||||
filters.push({
|
|
||||||
id: 'path',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'path',
|
|
||||||
value: [path],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device) {
|
|
||||||
filters.push({
|
|
||||||
id: 'device',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'device',
|
|
||||||
value: [device],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrer) {
|
|
||||||
filters.push({
|
|
||||||
id: 'referrer',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'referrer',
|
|
||||||
value: [referrer],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrerName) {
|
|
||||||
filters.push({
|
|
||||||
id: 'referrer_name',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'referrer_name',
|
|
||||||
value: [referrerName],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrerType) {
|
|
||||||
filters.push({
|
|
||||||
id: 'referrer_type',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'referrer_type',
|
|
||||||
value: [referrerType],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmSource) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utm_source',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_source',
|
|
||||||
value: [utmSource],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmMedium) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utm_medium',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_medium',
|
|
||||||
value: [utmMedium],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmCampaign) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utm_campaign',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_campaign',
|
|
||||||
value: [utmCampaign],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmContent) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utm_content',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_content',
|
|
||||||
value: [utmContent],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmTerm) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utm_term',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_term',
|
|
||||||
value: [utmTerm],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (country) {
|
|
||||||
filters.push({
|
|
||||||
id: 'country',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'country',
|
|
||||||
value: [country],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (region) {
|
|
||||||
filters.push({
|
|
||||||
id: 'region',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'region',
|
|
||||||
value: [region],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (city) {
|
|
||||||
filters.push({
|
|
||||||
id: 'city',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'city',
|
|
||||||
value: [city],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (browser) {
|
|
||||||
filters.push({
|
|
||||||
id: 'browser',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'browser',
|
|
||||||
value: [browser],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (browserVersion) {
|
|
||||||
filters.push({
|
|
||||||
id: 'browser_version',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'browser_version',
|
|
||||||
value: [browserVersion],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (os) {
|
|
||||||
filters.push({
|
|
||||||
id: 'os',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'os',
|
|
||||||
value: [os],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (osVersion) {
|
|
||||||
filters.push({
|
|
||||||
id: 'os_version',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'os_version',
|
|
||||||
value: [osVersion],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return filters;
|
|
||||||
}, [
|
|
||||||
path,
|
|
||||||
device,
|
|
||||||
referrer,
|
|
||||||
referrerName,
|
|
||||||
referrerType,
|
|
||||||
utmSource,
|
|
||||||
utmMedium,
|
|
||||||
utmCampaign,
|
|
||||||
utmContent,
|
|
||||||
utmTerm,
|
|
||||||
country,
|
|
||||||
region,
|
|
||||||
city,
|
|
||||||
browser,
|
|
||||||
browserVersion,
|
|
||||||
os,
|
|
||||||
osVersion,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Computed
|
|
||||||
filters,
|
|
||||||
|
|
||||||
// Path
|
|
||||||
path,
|
|
||||||
setPath,
|
|
||||||
|
|
||||||
// Refs
|
|
||||||
referrer,
|
|
||||||
setReferrer,
|
|
||||||
referrerName,
|
|
||||||
setReferrerName,
|
|
||||||
referrerType,
|
|
||||||
setReferrerType,
|
|
||||||
|
|
||||||
// UTM
|
|
||||||
utmSource,
|
|
||||||
setUtmSource,
|
|
||||||
utmMedium,
|
|
||||||
setUtmMedium,
|
|
||||||
utmCampaign,
|
|
||||||
setUtmCampaign,
|
|
||||||
utmContent,
|
|
||||||
setUtmContent,
|
|
||||||
utmTerm,
|
|
||||||
setUtmTerm,
|
|
||||||
|
|
||||||
// GEO
|
|
||||||
country,
|
|
||||||
setCountry,
|
|
||||||
region,
|
|
||||||
setRegion,
|
|
||||||
city,
|
|
||||||
setCity,
|
|
||||||
|
|
||||||
// Tech
|
|
||||||
device,
|
|
||||||
setDevice,
|
|
||||||
browser,
|
|
||||||
setBrowser,
|
|
||||||
browserVersion,
|
|
||||||
setBrowserVersion,
|
|
||||||
os,
|
|
||||||
setOS,
|
|
||||||
osVersion,
|
|
||||||
setOSVersion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,326 +1,102 @@
|
|||||||
import { useMemo } from 'react';
|
import { useCallback } from 'react';
|
||||||
import type { IChartInput } from '@/types';
|
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
import type { Options as NuqsOptions, UseQueryStateReturn } from 'nuqs';
|
import type { Options as NuqsOptions } from 'nuqs';
|
||||||
|
|
||||||
import { parseAsString, useQueryState } from 'nuqs';
|
import {
|
||||||
|
createParser,
|
||||||
|
parseAsArrayOf,
|
||||||
|
parseAsString,
|
||||||
|
useQueryState,
|
||||||
|
} from 'nuqs';
|
||||||
|
|
||||||
const nuqsOptions = { history: 'push' } as const;
|
const nuqsOptions = { history: 'push' } as const;
|
||||||
|
|
||||||
function useFix<T>(hook: UseQueryStateReturn<T, undefined>) {
|
type Operator = 'is' | 'isNot' | 'contains' | 'doesNotContain';
|
||||||
return useMemo(
|
|
||||||
() => ({
|
export const eventQueryFiltersParser = createParser({
|
||||||
get: hook[0],
|
parse: (query: string) => {
|
||||||
set: hook[1],
|
if (query === '') return [];
|
||||||
}),
|
const filters = query.split(';');
|
||||||
[hook]
|
|
||||||
);
|
return (
|
||||||
}
|
filters.map((filter) => {
|
||||||
|
const [key, operator, value] = filter.split(',');
|
||||||
|
return {
|
||||||
|
id: key!,
|
||||||
|
name: key!,
|
||||||
|
operator: (operator ?? 'is') as Operator,
|
||||||
|
value: [decodeURIComponent(value!)],
|
||||||
|
};
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
serialize: (value) => {
|
||||||
|
return value
|
||||||
|
.map(
|
||||||
|
(filter) =>
|
||||||
|
`${filter.id},${filter.operator},${encodeURIComponent(filter.value[0] ?? '')}`
|
||||||
|
)
|
||||||
|
.join(';');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export function useEventQueryFilters(options: NuqsOptions = {}) {
|
export function useEventQueryFilters(options: NuqsOptions = {}) {
|
||||||
// Ignore prettier so that we have all one same line
|
const [filters, setFilters] = useQueryState(
|
||||||
// prettier-ignore
|
'f',
|
||||||
return {
|
eventQueryFiltersParser.withDefault([]).withOptions({
|
||||||
path: useFix(useQueryState('path', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
...nuqsOptions,
|
||||||
referrer: useFix(useQueryState('referrer', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
...options,
|
||||||
referrerName: useFix(useQueryState('referrerName',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
})
|
||||||
referrerType: useFix(useQueryState('referrerType',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
);
|
||||||
utmSource: useFix(useQueryState('utmSource',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
|
||||||
utmMedium: useFix(useQueryState('utmMedium',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
const setFilter = useCallback(
|
||||||
utmCampaign: useFix(useQueryState('utmCampaign',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
(
|
||||||
utmContent: useFix(useQueryState('utmContent',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
name: string,
|
||||||
utmTerm: useFix(useQueryState('utmTerm', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
value: string | number | boolean | undefined | null,
|
||||||
continent: useFix(useQueryState('continent', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
operator: Operator = 'is'
|
||||||
country: useFix(useQueryState('country', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
) => {
|
||||||
region: useFix(useQueryState('region', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
setFilters((prev) => {
|
||||||
city: useFix(useQueryState('city', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
const exists = prev.find((filter) => filter.name === name);
|
||||||
device: useFix(useQueryState('device', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
if (exists) {
|
||||||
browser: useFix(useQueryState('browser', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
// If same value is already set, remove the filter
|
||||||
browserVersion: useFix(useQueryState('browserVersion',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
if (exists.value[0] === value) {
|
||||||
os: useFix(useQueryState('os', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
return prev.filter((filter) => filter.name !== name);
|
||||||
osVersion: useFix(useQueryState('osVersion',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
}
|
||||||
brand: useFix(useQueryState('brand',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
|
||||||
model: useFix(useQueryState('model',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
return prev.map((filter) => {
|
||||||
} as const;
|
if (filter.name === name) {
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
operator,
|
||||||
|
value: [String(value)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return filter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: name,
|
||||||
|
name,
|
||||||
|
operator,
|
||||||
|
value: [String(value)],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setFilters]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [filters, setFilter, setFilters] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEventFilters() {
|
export const eventQueryNamesFilter = parseAsArrayOf(parseAsString).withDefault(
|
||||||
const eventQueryFilters = useEventQueryFilters();
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const filters = useMemo(() => {
|
export function useEventQueryNamesFilter(options: NuqsOptions = {}) {
|
||||||
return getEventFilters({
|
return useQueryState('events', eventQueryNamesFilter.withOptions(options));
|
||||||
path: eventQueryFilters.path.get,
|
|
||||||
device: eventQueryFilters.device.get,
|
|
||||||
referrer: eventQueryFilters.referrer.get,
|
|
||||||
referrerName: eventQueryFilters.referrerName.get,
|
|
||||||
referrerType: eventQueryFilters.referrerType.get,
|
|
||||||
utmSource: eventQueryFilters.utmSource.get,
|
|
||||||
utmMedium: eventQueryFilters.utmMedium.get,
|
|
||||||
utmCampaign: eventQueryFilters.utmCampaign.get,
|
|
||||||
utmContent: eventQueryFilters.utmContent.get,
|
|
||||||
utmTerm: eventQueryFilters.utmTerm.get,
|
|
||||||
continent: eventQueryFilters.continent.get,
|
|
||||||
country: eventQueryFilters.country.get,
|
|
||||||
region: eventQueryFilters.region.get,
|
|
||||||
city: eventQueryFilters.city.get,
|
|
||||||
browser: eventQueryFilters.browser.get,
|
|
||||||
browserVersion: eventQueryFilters.browserVersion.get,
|
|
||||||
os: eventQueryFilters.os.get,
|
|
||||||
osVersion: eventQueryFilters.osVersion.get,
|
|
||||||
brand: eventQueryFilters.brand.get,
|
|
||||||
model: eventQueryFilters.model.get,
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
eventQueryFilters.path.get,
|
|
||||||
eventQueryFilters.device.get,
|
|
||||||
eventQueryFilters.referrer.get,
|
|
||||||
eventQueryFilters.referrerName.get,
|
|
||||||
eventQueryFilters.referrerType.get,
|
|
||||||
eventQueryFilters.utmSource.get,
|
|
||||||
eventQueryFilters.utmMedium.get,
|
|
||||||
eventQueryFilters.utmCampaign.get,
|
|
||||||
eventQueryFilters.utmContent.get,
|
|
||||||
eventQueryFilters.utmTerm.get,
|
|
||||||
eventQueryFilters.continent.get,
|
|
||||||
eventQueryFilters.country.get,
|
|
||||||
eventQueryFilters.region.get,
|
|
||||||
eventQueryFilters.city.get,
|
|
||||||
eventQueryFilters.browser.get,
|
|
||||||
eventQueryFilters.browserVersion.get,
|
|
||||||
eventQueryFilters.os.get,
|
|
||||||
eventQueryFilters.osVersion.get,
|
|
||||||
eventQueryFilters.model.get,
|
|
||||||
eventQueryFilters.brand.get,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return filters;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEventFilters({
|
|
||||||
path,
|
|
||||||
device,
|
|
||||||
referrer,
|
|
||||||
referrerName,
|
|
||||||
referrerType,
|
|
||||||
utmSource,
|
|
||||||
utmMedium,
|
|
||||||
utmCampaign,
|
|
||||||
utmContent,
|
|
||||||
utmTerm,
|
|
||||||
continent,
|
|
||||||
country,
|
|
||||||
region,
|
|
||||||
city,
|
|
||||||
browser,
|
|
||||||
browserVersion,
|
|
||||||
os,
|
|
||||||
osVersion,
|
|
||||||
brand,
|
|
||||||
model,
|
|
||||||
}: {
|
|
||||||
path: string | null;
|
|
||||||
device: string | null;
|
|
||||||
referrer: string | null;
|
|
||||||
referrerName: string | null;
|
|
||||||
referrerType: string | null;
|
|
||||||
utmSource: string | null;
|
|
||||||
utmMedium: string | null;
|
|
||||||
utmCampaign: string | null;
|
|
||||||
utmContent: string | null;
|
|
||||||
utmTerm: string | null;
|
|
||||||
continent: string | null;
|
|
||||||
country: string | null;
|
|
||||||
region: string | null;
|
|
||||||
city: string | null;
|
|
||||||
browser: string | null;
|
|
||||||
browserVersion: string | null;
|
|
||||||
os: string | null;
|
|
||||||
osVersion: string | null;
|
|
||||||
brand: string | null;
|
|
||||||
model: string | null;
|
|
||||||
}) {
|
|
||||||
const filters: IChartInput['events'][number]['filters'] = [];
|
|
||||||
|
|
||||||
if (path) {
|
|
||||||
filters.push({
|
|
||||||
id: 'path',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'path' as const,
|
|
||||||
value: [path],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device) {
|
|
||||||
filters.push({
|
|
||||||
id: 'device',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'device' as const,
|
|
||||||
value: [device],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrer) {
|
|
||||||
filters.push({
|
|
||||||
id: 'referrer',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'referrer' as const,
|
|
||||||
value: [referrer],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrerName) {
|
|
||||||
filters.push({
|
|
||||||
id: 'referrerName',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'referrer_name' as const,
|
|
||||||
value: [referrerName],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrerType) {
|
|
||||||
filters.push({
|
|
||||||
id: 'referrerType',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'referrer_type' as const,
|
|
||||||
value: [referrerType],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmSource) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utmSource',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_source' as const,
|
|
||||||
value: [utmSource],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmMedium) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utmMedium',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_medium' as const,
|
|
||||||
value: [utmMedium],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmCampaign) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utmCampaign',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_campaign' as const,
|
|
||||||
value: [utmCampaign],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmContent) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utmContent',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_content' as const,
|
|
||||||
value: [utmContent],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utmTerm) {
|
|
||||||
filters.push({
|
|
||||||
id: 'utmTerm',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'properties.query.utm_term' as const,
|
|
||||||
value: [utmTerm],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (continent) {
|
|
||||||
filters.push({
|
|
||||||
id: 'continent',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'continent' as const,
|
|
||||||
value: [continent],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (country) {
|
|
||||||
filters.push({
|
|
||||||
id: 'country',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'country' as const,
|
|
||||||
value: [country],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (region) {
|
|
||||||
filters.push({
|
|
||||||
id: 'region',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'region' as const,
|
|
||||||
value: [region],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (city) {
|
|
||||||
filters.push({
|
|
||||||
id: 'city',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'city' as const,
|
|
||||||
value: [city],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (browser) {
|
|
||||||
filters.push({
|
|
||||||
id: 'browser',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'browser' as const,
|
|
||||||
value: [browser],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (browserVersion) {
|
|
||||||
filters.push({
|
|
||||||
id: 'browserVersion',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'browser_version' as const,
|
|
||||||
value: [browserVersion],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (os) {
|
|
||||||
filters.push({
|
|
||||||
id: 'os',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'os' as const,
|
|
||||||
value: [os],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (osVersion) {
|
|
||||||
filters.push({
|
|
||||||
id: 'osVersion',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'os_version' as const,
|
|
||||||
value: [osVersion],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (brand) {
|
|
||||||
filters.push({
|
|
||||||
id: 'brand',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'brand' as const,
|
|
||||||
value: [brand],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model) {
|
|
||||||
filters.push({
|
|
||||||
id: 'model',
|
|
||||||
operator: 'is',
|
|
||||||
name: 'model' as const,
|
|
||||||
value: [model],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return filters;
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
apps/web/src/hooks/useEventValues.ts
Normal file
15
apps/web/src/hooks/useEventValues.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { api } from '@/app/_trpc/client';
|
||||||
|
|
||||||
|
export function useEventValues(
|
||||||
|
projectId: string,
|
||||||
|
event: string,
|
||||||
|
property: string
|
||||||
|
) {
|
||||||
|
const query = api.chart.values.useQuery({
|
||||||
|
projectId: projectId,
|
||||||
|
event,
|
||||||
|
property,
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.data?.values ?? [];
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { IInterval } from '@/types';
|
import type { IInterval } from '@mixan/validation';
|
||||||
|
|
||||||
export function formatDateInterval(interval: IInterval, date: Date): string {
|
export function formatDateInterval(interval: IInterval, date: Date): string {
|
||||||
if (interval === 'hour' || interval === 'minute') {
|
if (interval === 'hour' || interval === 'minute') {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { authMiddleware } from '@clerk/nextjs';
|
|||||||
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
|
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
|
||||||
export default authMiddleware({
|
export default authMiddleware({
|
||||||
publicRoutes: ['/share/overview/:id', '/api/trpc(.*)'],
|
publicRoutes: ['/share/overview/:id', '/api/trpc(.*)'],
|
||||||
debug: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
|
|||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import type { IClientWithProject } from '@/types';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { IServiceClient } from '@mixan/db';
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
type EditClientProps = IClientWithProject;
|
type EditClientProps = IServiceClient;
|
||||||
|
|
||||||
const validator = z.object({
|
const validator = z.object({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
|
|||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import type { IServiceDashboardWithProject } from '@/server/services/dashboard.service';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { IServiceDashboard } from '@mixan/db';
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
type EditDashboardProps = IServiceDashboardWithProject;
|
type EditDashboardProps = Exclude<IServiceDashboard, null>;
|
||||||
|
|
||||||
const validator = z.object({
|
const validator = z.object({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
|
|||||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import type { IProject } from '@/types';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { IServiceProject } from '@mixan/db';
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
type EditProjectProps = IProject;
|
type EditProjectProps = Exclude<IServiceProject, null>;
|
||||||
|
|
||||||
const validator = z.object({
|
const validator = z.object({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user