refactor packages
This commit is contained in:
@@ -31,30 +31,20 @@
|
||||
"email-validator": "^2.0.4",
|
||||
"embla-carousel-autoplay": "8.0.0-rc22",
|
||||
"embla-carousel-react": "8.0.0-rc22",
|
||||
"hamburger-react": "^2.5.0",
|
||||
"lucide-react": "^0.323.0",
|
||||
"next": "~14.0.4",
|
||||
"nuqs": "^1.15.2",
|
||||
"react": "18.2.0",
|
||||
"react-animate-height": "^3.2.3",
|
||||
"react-dom": "18.2.0",
|
||||
"react-in-viewport": "1.0.0-alpha.30",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"sharp": "^0.33.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"zod": "^3.22.4"
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mixan/eslint-config": "workspace:*",
|
||||
"@mixan/prettier-config": "workspace:*",
|
||||
"@mixan/tsconfig": "workspace:*",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/ramda": "^0.29.6",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@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) {
|
||||
const match = referrers[getHostname(url)];
|
||||
const hostname = getHostname(url);
|
||||
const match = referrers[hostname] ?? referrers[hostname.replace('www.', '')];
|
||||
|
||||
return {
|
||||
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/queue/package.json packages/queue/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
|
||||
|
||||
# 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/common /app/packages/common
|
||||
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
|
||||
# Packages 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/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
|
||||
|
||||
RUN pnpm db:codegen
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"@clickhouse/client": "^0.2.9",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@mixan/common": "workspace:^",
|
||||
"@mixan/constants": "workspace:^",
|
||||
"@mixan/validation": "workspace:^",
|
||||
"@mixan/db": "workspace:^",
|
||||
"@mixan/queue": "workspace:^",
|
||||
"@mixan/types": "workspace:*",
|
||||
@@ -90,8 +92,8 @@
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/node": "^18.19.15",
|
||||
"@types/ramda": "^0.29.10",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-syntax-highlighter": "^15.5.11",
|
||||
"@types/request-ip": "^0.0.41",
|
||||
"@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 { ReportRange } from '@/components/report/ReportRange';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -14,14 +13,15 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import type { getReportsByDashboardId } from '@/server/services/reports.service';
|
||||
import type { IChartRange } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
||||
import { ChevronRight, MoreHorizontal, PlusIcon, Trash } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { getDefaultIntervalByRange } from '@mixan/constants';
|
||||
import type { getReportsByDashboardId } from '@mixan/db';
|
||||
import type { IChartRange } from '@mixan/validation';
|
||||
|
||||
interface ListReportsProps {
|
||||
reports: Awaited<ReturnType<typeof getReportsByDashboardId>>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
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 { getDashboardById, getReportsByDashboardId } from '@mixan/db';
|
||||
|
||||
import { ListReports } from './list-reports';
|
||||
|
||||
interface PageProps {
|
||||
|
||||
@@ -7,12 +7,13 @@ import { Button } from '@/components/ui/button';
|
||||
import { ToastAction } from '@/components/ui/toast';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { pushModal } from '@/modals';
|
||||
import type { IServiceDashboards } from '@/server/services/dashboard.service';
|
||||
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { IServiceDashboards } from '@mixan/db';
|
||||
|
||||
interface ListDashboardsProps {
|
||||
dashboards: IServiceDashboards;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getDashboardsByProjectId } from '@/server/services/dashboard.service';
|
||||
|
||||
import { getDashboardsByProjectId } from '@mixan/db';
|
||||
|
||||
import { HeaderDashboards } from './header-dashboards';
|
||||
import { ListDashboards } from './list-dashboards';
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import type { RouterOutputs } from '@/app/_trpc/client';
|
||||
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
||||
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
import { round } from '@/utils/math';
|
||||
import { uniq } from 'ramda';
|
||||
|
||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||
|
||||
@@ -40,7 +43,8 @@ export function EventListItem({
|
||||
meta,
|
||||
}: EventListItemProps) {
|
||||
const params = useAppParams();
|
||||
const eventQueryFilters = useEventQueryFilters({ shallow: false });
|
||||
const [, setEvents] = useEventQueryNamesFilter({ shallow: false });
|
||||
const [, setFilter] = useEventQueryFilters({ shallow: false });
|
||||
const keyValueList = [
|
||||
{
|
||||
name: 'Duration',
|
||||
@@ -50,98 +54,98 @@ export function EventListItem({
|
||||
name: 'Referrer',
|
||||
value: referrer,
|
||||
onClick() {
|
||||
eventQueryFilters.referrer.set(referrer ?? null);
|
||||
setFilter('referrer', referrer ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Referrer name',
|
||||
value: referrerName,
|
||||
onClick() {
|
||||
eventQueryFilters.referrerName.set(referrerName ?? null);
|
||||
setFilter('referrer_name', referrerName ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Referrer type',
|
||||
value: referrerType,
|
||||
onClick() {
|
||||
eventQueryFilters.referrerType.set(referrerType ?? null);
|
||||
setFilter('referrer_type', referrerType ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Brand',
|
||||
value: brand,
|
||||
onClick() {
|
||||
eventQueryFilters.brand.set(brand ?? null);
|
||||
setFilter('brand', brand ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Model',
|
||||
value: model,
|
||||
onClick() {
|
||||
eventQueryFilters.model.set(model ?? null);
|
||||
setFilter('model', model ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Browser',
|
||||
value: browser,
|
||||
onClick() {
|
||||
eventQueryFilters.browser.set(browser ?? null);
|
||||
setFilter('browser', browser ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Browser version',
|
||||
value: browserVersion,
|
||||
onClick() {
|
||||
eventQueryFilters.browserVersion.set(browserVersion ?? null);
|
||||
setFilter('browser_version', browserVersion ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'OS',
|
||||
value: os,
|
||||
onClick() {
|
||||
eventQueryFilters.os.set(os ?? null);
|
||||
setFilter('os', os ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'OS cersion',
|
||||
value: osVersion,
|
||||
onClick() {
|
||||
eventQueryFilters.osVersion.set(osVersion ?? null);
|
||||
setFilter('os_version', osVersion ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'City',
|
||||
value: city,
|
||||
onClick() {
|
||||
eventQueryFilters.city.set(city ?? null);
|
||||
setFilter('city', city ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Region',
|
||||
value: region,
|
||||
onClick() {
|
||||
eventQueryFilters.region.set(region ?? null);
|
||||
setFilter('region', region ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Country',
|
||||
value: country,
|
||||
onClick() {
|
||||
eventQueryFilters.country.set(country ?? null);
|
||||
setFilter('country', country ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Continent',
|
||||
value: continent,
|
||||
onClick() {
|
||||
eventQueryFilters.continent.set(continent ?? null);
|
||||
setFilter('continent', continent ?? '');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Device',
|
||||
value: device,
|
||||
onClick() {
|
||||
eventQueryFilters.device.set(device ?? null);
|
||||
setFilter('device', device ?? '');
|
||||
},
|
||||
},
|
||||
].filter((item) => typeof item.value === 'string' && item.value);
|
||||
@@ -156,7 +160,11 @@ export function EventListItem({
|
||||
return (
|
||||
<ExpandableListItem
|
||||
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={
|
||||
<>
|
||||
<KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
|
||||
@@ -172,7 +180,7 @@ export function EventListItem({
|
||||
name="Path"
|
||||
value={path}
|
||||
onClick={() => {
|
||||
eventQueryFilters.path.set(path);
|
||||
setFilter('path', path);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -191,6 +199,13 @@ export function EventListItem({
|
||||
key={item.name}
|
||||
name={item.name}
|
||||
value={item.value}
|
||||
onClick={() => {
|
||||
setFilter(
|
||||
`properties.${item.name}`,
|
||||
item.value ? String(item.value) : '',
|
||||
'is'
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FullPageEmptyState } from '@/components/FullPageEmptyState';
|
||||
import { Pagination } from '@/components/Pagination';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCursor } from '@/hooks/useCursor';
|
||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { GanttChartIcon } from 'lucide-react';
|
||||
|
||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||
@@ -18,8 +18,7 @@ interface EventListProps {
|
||||
}
|
||||
export function EventList({ data, count }: EventListProps) {
|
||||
const { cursor, setCursor } = useCursor();
|
||||
const filters = useEventFilters();
|
||||
|
||||
const [filters] = useEventQueryFilters();
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="p-4">
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||
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 { getEventList, getEventsCount } from '@mixan/db';
|
||||
@@ -15,27 +18,9 @@ interface PageProps {
|
||||
organizationId: string;
|
||||
};
|
||||
searchParams: {
|
||||
events?: string;
|
||||
cursor?: string;
|
||||
path?: 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;
|
||||
f?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,53 +44,13 @@ export default async function Page({
|
||||
cursor: parseQueryAsNumber(searchParams.cursor),
|
||||
projectId,
|
||||
take: 50,
|
||||
filters: getEventFilters({
|
||||
path: searchParams.path ?? null,
|
||||
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,
|
||||
}),
|
||||
events: eventQueryNamesFilter.parse(searchParams.events ?? ''),
|
||||
filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined,
|
||||
}),
|
||||
getEventsCount({
|
||||
projectId,
|
||||
filters: getEventFilters({
|
||||
path: searchParams.path ?? null,
|
||||
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,
|
||||
}),
|
||||
events: eventQueryNamesFilter.parse(searchParams.events ?? ''),
|
||||
filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined,
|
||||
}),
|
||||
getExists(organizationId, projectId),
|
||||
]);
|
||||
@@ -116,6 +61,7 @@ export default async function Page({
|
||||
<OverviewFiltersDrawer
|
||||
projectId={projectId}
|
||||
nuqsOptions={nuqsOptions}
|
||||
enableEventsFilter
|
||||
/>
|
||||
<OverviewFiltersButtons
|
||||
className="p-0 justify-end"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import type { IServiceDashboards } from '@/server/services/dashboard.service';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useUser } from '@clerk/nextjs';
|
||||
import {
|
||||
@@ -21,6 +20,8 @@ import type { LucideProps } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import type { IServiceDashboards } from '@mixan/db';
|
||||
|
||||
function LinkWithIcon({
|
||||
href,
|
||||
icon: Icon,
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import type { IServiceOrganization } from '@/server/services/organization.service';
|
||||
import { Building } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { IServiceOrganization } from '@mixan/db';
|
||||
|
||||
interface LayoutOrganizationSelectorProps {
|
||||
organizations: IServiceOrganization[];
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import type { getCurrentProjects } from '@/server/services/project.service';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
import type { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||
|
||||
interface LayoutProjectSelectorProps {
|
||||
projects: Awaited<ReturnType<typeof getCurrentProjects>>;
|
||||
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
||||
}
|
||||
export default function LayoutProjectSelector({
|
||||
projects,
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
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 { Rotate as Hamburger } from 'hamburger-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import type { IServiceDashboards, IServiceOrganization } from '@mixan/db';
|
||||
|
||||
import LayoutMenu from './layout-menu';
|
||||
import LayoutOrganizationSelector from './layout-organization-selector';
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { getDashboardsByOrganization } from '@/server/services/dashboard.service';
|
||||
import { getCurrentOrganizations } from '@/server/services/organization.service';
|
||||
import {
|
||||
getCurrentOrganizations,
|
||||
getDashboardsByOrganization,
|
||||
} from '@mixan/db';
|
||||
|
||||
import { LayoutSidebar } from './layout-sidebar';
|
||||
|
||||
|
||||
@@ -4,17 +4,18 @@ import { WidgetHead } from '@/components/overview/overview-widget';
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { Widget, WidgetBody } from '@/components/Widget';
|
||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import type { IChartInput } from '@/types';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
interface OverviewMetricsProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
const { previous, range, interval, metric, setMetric } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const [filters] = useEventQueryFilters();
|
||||
|
||||
const reports = [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getCurrentProjects } from '@/server/services/project.service';
|
||||
import { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||
|
||||
import LayoutProjectSelector from './layout-project-selector';
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async function PageLayout({
|
||||
title,
|
||||
organizationSlug,
|
||||
}: PageLayoutProps) {
|
||||
const projects = await getCurrentProjects(organizationSlug);
|
||||
const projects = await getProjectsByOrganizationSlug(organizationSlug);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function ListProfileEvents({
|
||||
projectId,
|
||||
profileId,
|
||||
}: ListProfileEvents) {
|
||||
const pagination = usePagination();
|
||||
const pagination = usePagination(50);
|
||||
const [eventFilters, setEventFilters] = useQueryState(
|
||||
'events',
|
||||
parseAsJson<string[]>().withDefault([])
|
||||
|
||||
@@ -3,13 +3,11 @@ import { ListProperties } from '@/components/events/ListProperties';
|
||||
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import {
|
||||
getProfileById,
|
||||
getProfilesByExternalId,
|
||||
} from '@/server/services/profile.service';
|
||||
import { formatDateTime } from '@/utils/date';
|
||||
import { getProfileName } from '@/utils/getters';
|
||||
|
||||
import { getProfileById, getProfilesByExternalId } from '@mixan/db';
|
||||
|
||||
import ListProfileEvents from './list-profile-events';
|
||||
|
||||
interface PageProps {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { RouterOutputs } from '@/app/_trpc/client';
|
||||
import { ListProperties } from '@/components/events/ListProperties';
|
||||
import { ExpandableListItem } from '@/components/general/ExpandableListItem';
|
||||
@@ -16,24 +15,24 @@ export function ProfileListItem(props: ProfileListItemProps) {
|
||||
const { id, properties, createdAt } = props;
|
||||
const params = useAppParams();
|
||||
|
||||
const bullets = useMemo(() => {
|
||||
const bullets: React.ReactNode[] = [
|
||||
<span>{formatDateTime(createdAt)}</span>,
|
||||
<Link
|
||||
href={`/${params.organizationId}/${params.projectId}/profiles/${id}`}
|
||||
className="text-black font-medium hover:underline"
|
||||
>
|
||||
See profile
|
||||
</Link>,
|
||||
];
|
||||
|
||||
return bullets;
|
||||
}, [createdAt, id, params]);
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<>
|
||||
<span>{formatDateTime(createdAt)}</span>
|
||||
<Link
|
||||
href={`/${params.organizationId}/${params.projectId}/profiles/${id}`}
|
||||
className="text-black font-medium hover:underline"
|
||||
>
|
||||
See profile
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ExpandableListItem
|
||||
title={getProfileName(props)}
|
||||
bullets={bullets}
|
||||
content={renderContent()}
|
||||
image={<ProfileAvatar {...props} />}
|
||||
>
|
||||
<ListProperties data={properties} className="rounded-none border-none" />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
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 { notFound } from 'next/navigation';
|
||||
|
||||
import { getOrganizationBySlug, getReportById } from '@mixan/db';
|
||||
|
||||
import ReportEditor from '../report-editor';
|
||||
|
||||
interface PageProps {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getOrganizationBySlug } from '@mixan/db';
|
||||
|
||||
import ReportEditor from './report-editor';
|
||||
|
||||
interface PageProps {
|
||||
|
||||
@@ -19,9 +19,10 @@ import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import type { IServiceReport } from '@/server/services/reports.service';
|
||||
import { GanttChartSquareIcon } from 'lucide-react';
|
||||
|
||||
import type { IServiceReport } from '@mixan/db';
|
||||
|
||||
interface ReportEditorProps {
|
||||
report: IServiceReport | null;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { DataTable } from '@/components/DataTable';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { pushModal } from '@/modals';
|
||||
import type { getClientsByOrganizationId } from '@/server/services/clients.service';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
|
||||
import type { getClientsByOrganizationId } from '@mixan/db';
|
||||
|
||||
interface ListClientsProps {
|
||||
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getClientsByOrganizationId } from '@/server/services/clients.service';
|
||||
|
||||
import { getClientsByOrganizationId } from '@mixan/db';
|
||||
|
||||
import ListClients from './list-clients';
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import type { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { getOrganizationBySlug } from '@mixan/db';
|
||||
|
||||
const validator = z.object({
|
||||
id: 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 { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { zInviteUser } from '@/utils/validation';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { SendIcon } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -10,6 +9,8 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { zInviteUser } from '@mixan/validation';
|
||||
|
||||
type IForm = z.infer<typeof zInviteUser>;
|
||||
|
||||
export function InviteUser() {
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
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';
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import {
|
||||
getInvites,
|
||||
getOrganizationBySlug,
|
||||
} from '@/server/services/organization.service';
|
||||
import { clerkClient } from '@clerk/nextjs';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getInvites, getOrganizationBySlug } from '@mixan/db';
|
||||
|
||||
import EditOrganization from './edit-organization';
|
||||
import InvitedUsers from './invited-users';
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||
import type { getUserById } from '@/server/services/user.service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { getUserById } from '@mixan/db';
|
||||
|
||||
const validator = z.object({
|
||||
firstName: z.string().min(2),
|
||||
lastName: z.string().min(2),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getUserById } from '@/server/services/user.service';
|
||||
import { auth } from '@clerk/nextjs';
|
||||
|
||||
import { getUserById } from '@mixan/db';
|
||||
|
||||
import EditProfile from './edit-profile';
|
||||
import { Logout } from './logout';
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ import { columns } from '@/components/projects/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { pushModal } from '@/modals';
|
||||
import type { getProjectsByOrganizationId } from '@/server/services/project.service';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
|
||||
import type { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||
|
||||
interface ListProjectsProps {
|
||||
projects: Awaited<ReturnType<typeof getProjectsByOrganizationId>>;
|
||||
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
||||
}
|
||||
export default function ListProjects({ projects }: ListProjectsProps) {
|
||||
const organizationId = useAppParams().organizationId;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
import { getProjectsByOrganizationSlug } from '@/server/services/project.service';
|
||||
|
||||
import { getProjectsByOrganizationSlug } from '@mixan/db';
|
||||
|
||||
import ListProjects from './list-projects';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { getProjectWithMostEvents } from '@/server/services/project.service';
|
||||
import { getOrganizationBySlug } from '@mixan/db';
|
||||
import { getProjectWithMostEvents } from '@mixan/db';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
|
||||
import PageLayout from './[projectId]/page-layout';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getCurrentOrganizations } from '@/server/services/organization.service';
|
||||
import { CreateOrganization } from '@clerk/nextjs';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { getCurrentOrganizations } from '@mixan/db';
|
||||
|
||||
export default async function Page() {
|
||||
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 OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getShareOverviewById } from '@mixan/db';
|
||||
import { getOrganizationBySlug, getShareOverviewById } from '@mixan/db';
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { appRouter } from '@/server/api/root';
|
||||
import { getSession } from '@/server/auth';
|
||||
import { getAuth } from '@clerk/nextjs/server';
|
||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
||||
|
||||
@@ -9,9 +8,7 @@ const handler = (req: Request) =>
|
||||
req,
|
||||
router: appRouter,
|
||||
async createContext({ req }) {
|
||||
console.log('------- createContext --------');
|
||||
const session = getAuth(req as any);
|
||||
console.log('session', JSON.stringify(session, null, 2));
|
||||
return {
|
||||
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 { pushModal, showConfirm } from '@/modals';
|
||||
import type { IClientWithProject } from '@/types';
|
||||
import { clipboard } from '@/utils/clipboard';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { IServiceClientWithProject } from '@mixan/db';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu';
|
||||
|
||||
export function ClientActions(client: IClientWithProject) {
|
||||
export function ClientActions(client: IServiceClientWithProject) {
|
||||
const { id } = client;
|
||||
const router = useRouter();
|
||||
const deletion = api.client.remove.useMutation({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { IClientWithProject } from '@/types';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import type { IServiceClientWithProject } from '@mixan/db';
|
||||
|
||||
import { ClientActions } from './ClientActions';
|
||||
|
||||
export const columns: ColumnDef<IClientWithProject>[] = [
|
||||
export const columns: ColumnDef<IServiceClientWithProject>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from '../ui/button';
|
||||
interface ExpandableListItemProps {
|
||||
children: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
image?: React.ReactNode;
|
||||
initialOpen?: boolean;
|
||||
className?: string;
|
||||
@@ -29,7 +29,7 @@ export function ExpandableListItem({
|
||||
<div className="p-2 sm:p-4 flex gap-4">
|
||||
<div className="flex gap-1">{image}</div>
|
||||
<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 && (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4">
|
||||
{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';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import {
|
||||
useEventQueryFilters,
|
||||
useEventQueryNamesFilter,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { X } from 'lucide-react';
|
||||
import { Options as NuqsOptions } from 'nuqs';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
interface OverviewFiltersButtonsProps {
|
||||
className?: string;
|
||||
@@ -15,25 +18,40 @@ export function OverviewFiltersButtons({
|
||||
className,
|
||||
nuqsOptions,
|
||||
}: OverviewFiltersButtonsProps) {
|
||||
const eventQueryFilters = useEventQueryFilters(nuqsOptions);
|
||||
const filters = Object.entries(eventQueryFilters).filter(
|
||||
([, filter]) => filter.get !== null
|
||||
);
|
||||
if (filters.length === 0) return null;
|
||||
const [events, setEvents] = useEventQueryNamesFilter(nuqsOptions);
|
||||
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
|
||||
if (filters.length === 0 && events.length === 0) return null;
|
||||
return (
|
||||
<div className={cn('flex flex-wrap gap-2 px-4 pb-4', className)}>
|
||||
{filters.map(([key, filter]) => (
|
||||
{events.map((event) => (
|
||||
<Button
|
||||
key={key}
|
||||
key={event}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
icon={X}
|
||||
onClick={() => filter.set(null)}
|
||||
onClick={() => setEvents((p) => p.filter((e) => e !== event))}
|
||||
>
|
||||
<span className="mr-1">{key} is</span>
|
||||
<strong>{filter.get}</strong>
|
||||
<strong>{event}</strong>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,93 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { Options as NuqsOptions } from 'nuqs';
|
||||
import type { Options as NuqsOptions } from 'nuqs';
|
||||
|
||||
import type {
|
||||
IChartEventFilter,
|
||||
IChartEventFilterOperator,
|
||||
IChartEventFilterValue,
|
||||
} from '@mixan/validation';
|
||||
|
||||
interface OverviewFiltersProps {
|
||||
projectId: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
enableEventsFilter?: boolean;
|
||||
}
|
||||
|
||||
export function OverviewFiltersDrawerContent({
|
||||
projectId,
|
||||
nuqsOptions,
|
||||
enableEventsFilter,
|
||||
}: OverviewFiltersProps) {
|
||||
const eventQueryFilters = useEventQueryFilters(nuqsOptions);
|
||||
const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
|
||||
const [event, setEvent] = useEventQueryNamesFilter(nuqsOptions);
|
||||
const eventNames = useEventNames(projectId);
|
||||
const eventProperties = useEventProperties(projectId);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-medium mb-8">Overview filters</h2>
|
||||
<Combobox
|
||||
className="w-full"
|
||||
onChange={(value) => {
|
||||
// @ts-expect-error
|
||||
eventQueryFilters[value].set('');
|
||||
}}
|
||||
value=""
|
||||
placeholder="Filter by..."
|
||||
label="What do you want to filter by?"
|
||||
items={Object.entries(eventQueryFilters)
|
||||
.filter(([, filter]) => filter.get === null)
|
||||
.map(([name]) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
<SheetHeader className="mb-8">
|
||||
<SheetTitle>Overview filters</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{enableEventsFilter && (
|
||||
<ComboboxAdvanced
|
||||
className="w-full"
|
||||
value={event}
|
||||
onChange={setEvent}
|
||||
// First items is * which is only used for report editing
|
||||
items={eventNames.slice(1).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.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">
|
||||
{Object.entries(eventQueryFilters)
|
||||
.filter(([, filter]) => filter.get !== null)
|
||||
.map(([name, filter]) => (
|
||||
<FilterOption
|
||||
key={name}
|
||||
projectId={projectId}
|
||||
name={name}
|
||||
{...filter}
|
||||
/>
|
||||
))}
|
||||
{filters
|
||||
.filter((filter) => filter.value[0] !== null)
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterOption
|
||||
key={filter.name}
|
||||
projectId={projectId}
|
||||
setFilter={setFilter}
|
||||
{...filter}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterOption({
|
||||
name,
|
||||
get,
|
||||
set,
|
||||
setFilter,
|
||||
projectId,
|
||||
}: {
|
||||
name: string;
|
||||
get: string | null;
|
||||
set: (value: string | null) => void;
|
||||
...filter
|
||||
}: IChartEventFilter & {
|
||||
projectId: string;
|
||||
setFilter: (
|
||||
name: string,
|
||||
value: IChartEventFilterValue,
|
||||
operator: IChartEventFilterOperator
|
||||
) => void;
|
||||
}) {
|
||||
const { data } = api.chart.values.useQuery({
|
||||
const values = useEventValues(
|
||||
projectId,
|
||||
event: name === 'path' ? 'screen_view' : 'session_start',
|
||||
property: name,
|
||||
});
|
||||
filter.name === 'path' ? 'screen_view' : 'session_start',
|
||||
filter.name
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<div>{name}</div>
|
||||
<div>{filter.name}</div>
|
||||
<Combobox
|
||||
className="flex-1"
|
||||
onChange={(value) => set(value)}
|
||||
onChange={(value) => setFilter(filter.name, value, filter.operator)}
|
||||
placeholder={'Select a value'}
|
||||
items={
|
||||
data?.values.filter(Boolean).map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})) ?? []
|
||||
}
|
||||
value={get}
|
||||
items={values.map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
}))}
|
||||
value={String(filter.value[0] ?? '')}
|
||||
/>
|
||||
<Button size="icon" variant="ghost" onClick={() => set(null)}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
setFilter(filter.name, filter.value[0] ?? '', filter.operator)
|
||||
}
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,20 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
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';
|
||||
|
||||
interface OverviewFiltersDrawerProps {
|
||||
projectId: string;
|
||||
nuqsOptions?: NuqsOptions;
|
||||
enableEventsFilter?: boolean;
|
||||
}
|
||||
|
||||
export function OverviewFiltersDrawer({
|
||||
projectId,
|
||||
nuqsOptions,
|
||||
enableEventsFilter,
|
||||
}: OverviewFiltersDrawerProps) {
|
||||
return (
|
||||
<Sheet>
|
||||
@@ -27,6 +29,7 @@ export function OverviewFiltersDrawer({
|
||||
<OverviewFiltersDrawerContent
|
||||
projectId={projectId}
|
||||
nuqsOptions={nuqsOptions}
|
||||
enableEventsFilter={enableEventsFilter}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import type { IChartInput } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
import { Chart } from '../report/chart';
|
||||
import { Widget, WidgetBody, WidgetHead } from '../Widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -19,9 +16,7 @@ export default function OverviewTopDevices({
|
||||
projectId,
|
||||
}: OverviewTopDevicesProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const { device, browser, browserVersion, os, osVersion } =
|
||||
useEventQueryFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
||||
devices: {
|
||||
title: 'Top devices',
|
||||
@@ -190,21 +185,21 @@ export default function OverviewTopDevices({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'devices':
|
||||
device.set(item.name);
|
||||
setFilter('device', item.name);
|
||||
break;
|
||||
case 'browser':
|
||||
setWidget('browser_version');
|
||||
browser.set(item.name);
|
||||
setFilter('browser', item.name);
|
||||
break;
|
||||
case 'browser_version':
|
||||
browserVersion.set(item.name);
|
||||
setFilter('browser_version', item.name);
|
||||
break;
|
||||
case 'os':
|
||||
setWidget('os_version');
|
||||
os.set(item.name);
|
||||
setFilter('os', item.name);
|
||||
break;
|
||||
case 'os_version':
|
||||
osVersion.set(item.name);
|
||||
setFilter('os_version', item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { useEventFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -16,7 +16,7 @@ export default function OverviewTopEvents({
|
||||
projectId,
|
||||
}: OverviewTopEventsProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const [filters] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
||||
all: {
|
||||
title: 'Top events',
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -17,8 +14,7 @@ interface OverviewTopGeoProps {
|
||||
}
|
||||
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const { region, country, city } = useEventQueryFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
||||
map: {
|
||||
title: 'Map',
|
||||
@@ -160,14 +156,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
switch (widget.key) {
|
||||
case 'countries':
|
||||
setWidget('regions');
|
||||
country.set(item.name);
|
||||
setFilter('country', item.name);
|
||||
break;
|
||||
case 'regions':
|
||||
setWidget('cities');
|
||||
region.set(item.name);
|
||||
setFilter('region', item.name);
|
||||
break;
|
||||
case 'cities':
|
||||
city.set(item.name);
|
||||
setFilter('city', item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -17,8 +14,7 @@ interface OverviewTopPagesProps {
|
||||
}
|
||||
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const filters = useEventFilters();
|
||||
const { path } = useEventQueryFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||
top: {
|
||||
title: 'Top pages',
|
||||
@@ -129,7 +125,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
{...widget.chart}
|
||||
previous={false}
|
||||
onClick={(item) => {
|
||||
path.set(item.name);
|
||||
setFilter('path', item.name);
|
||||
}}
|
||||
/>
|
||||
</WidgetBody>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import {
|
||||
useEventFilters,
|
||||
useEventQueryFilters,
|
||||
} from '@/hooks/useEventQueryFilters';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
@@ -19,17 +16,7 @@ export default function OverviewTopSources({
|
||||
projectId,
|
||||
}: OverviewTopSourcesProps) {
|
||||
const { interval, range, previous } = useOverviewOptions();
|
||||
const {
|
||||
referrer,
|
||||
referrerName,
|
||||
referrerType,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmMedium,
|
||||
utmSource,
|
||||
utmTerm,
|
||||
} = useEventQueryFilters();
|
||||
const filters = useEventFilters();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('sources', {
|
||||
all: {
|
||||
title: 'Top sources',
|
||||
@@ -282,30 +269,30 @@ export default function OverviewTopSources({
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'all':
|
||||
referrerName.set(item.name);
|
||||
setFilter('referrer_name', item.name);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'domain':
|
||||
referrer.set(item.name);
|
||||
setFilter('referrer', item.name);
|
||||
break;
|
||||
case 'type':
|
||||
referrerType.set(item.name);
|
||||
setFilter('referrer_type', item.name);
|
||||
setWidget('domain');
|
||||
break;
|
||||
case 'utm_source':
|
||||
utmSource.set(item.name);
|
||||
setFilter('utm_source', item.name);
|
||||
break;
|
||||
case 'utm_medium':
|
||||
utmMedium.set(item.name);
|
||||
setFilter('utm_medium', item.name);
|
||||
break;
|
||||
case 'utm_campaign':
|
||||
utmCampaign.set(item.name);
|
||||
setFilter('utm_campaign', item.name);
|
||||
break;
|
||||
case 'utm_term':
|
||||
utmTerm.set(item.name);
|
||||
setFilter('utm_term', item.name);
|
||||
break;
|
||||
case 'utm_content':
|
||||
utmContent.set(item.name);
|
||||
setFilter('utm_content', item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
||||
import { mapKeys } from '@/utils/validation';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
@@ -8,6 +5,9 @@ import {
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
|
||||
import { getDefaultIntervalByRange, timeRanges } from '@mixan/constants';
|
||||
import { mapKeys } from '@mixan/validation';
|
||||
|
||||
const nuqsOptions = { history: 'push' } as const;
|
||||
|
||||
export function useOverviewOptions() {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { IChartInput } from '@/types';
|
||||
import { mapKeys } from '@/utils/validation';
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
|
||||
import { mapKeys } from '@mixan/validation';
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
export function useOverviewWidget<T extends string>(
|
||||
key: string,
|
||||
widgets: Record<T, { title: string; btn: string; chart: IChartInput }>
|
||||
@@ -15,7 +16,7 @@ export function useOverviewWidget<T extends string>(
|
||||
);
|
||||
return [
|
||||
{
|
||||
...widgets[widget]!,
|
||||
...widgets[widget],
|
||||
key: widget,
|
||||
},
|
||||
setWidget,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import type { IServiceProfile } from '@/server/services/profile.service';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { AvatarImage } from '@radix-ui/react-avatar';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import type { IServiceProfile } from '@mixan/db';
|
||||
|
||||
import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||
|
||||
interface ProfileAvatarProps
|
||||
@@ -41,8 +42,8 @@ export function ProfileAvatar({
|
||||
size === 'sm'
|
||||
? 'text-xs'
|
||||
: size === 'xs'
|
||||
? 'text-[8px]'
|
||||
: 'text-base',
|
||||
? 'text-[8px]'
|
||||
: 'text-base',
|
||||
'bg-slate-200 text-slate-800'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { pushModal, showConfirm } from '@/modals';
|
||||
import type { IProject } from '@/types';
|
||||
import { clipboard } from '@/utils/clipboard';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { IServiceProject } from '@mixan/db';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu';
|
||||
|
||||
export function ProjectActions(project: IProject) {
|
||||
export function ProjectActions(project: Exclude<IServiceProject, null>) {
|
||||
const { id } = project;
|
||||
const router = useRouter();
|
||||
const deletion = api.project.remove.useMutation({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IServiceProject } from '@/server/services/project.service';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { IServiceProject } from '@mixan/db';
|
||||
import type { Project as IProject } from '@mixan/db';
|
||||
|
||||
import { ProjectActions } from './ProjectActions';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import { chartTypes } from '@/utils/constants';
|
||||
import { objectToZodEnums } from '@/utils/validation';
|
||||
import { LineChartIcon } from 'lucide-react';
|
||||
|
||||
import { chartTypes } from '@mixan/constants';
|
||||
import { objectToZodEnums } from '@mixan/validation';
|
||||
|
||||
import { Combobox } from '../ui/combobox';
|
||||
import { changeChartType } from './reportSlice';
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import type { IInterval } from '@/types';
|
||||
import { ClockIcon } from 'lucide-react';
|
||||
|
||||
import {
|
||||
isHourIntervalEnabledByRange,
|
||||
isMinuteIntervalEnabledByRange,
|
||||
} from '@/utils/constants';
|
||||
import { ClockIcon } from 'lucide-react';
|
||||
} from '@mixan/constants';
|
||||
import type { IInterval } from '@mixan/validation';
|
||||
|
||||
import { Combobox } from '../ui/combobox';
|
||||
import { changeInterval } from './reportSlice';
|
||||
@@ -32,7 +33,7 @@ export function ReportInterval({ className }: ReportIntervalProps) {
|
||||
className={className}
|
||||
placeholder="Interval"
|
||||
onChange={(value) => {
|
||||
dispatch(changeInterval(value as IInterval));
|
||||
dispatch(changeInterval(value));
|
||||
}}
|
||||
value={interval}
|
||||
items={[
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import { lineTypes } from '@/utils/constants';
|
||||
import { objectToZodEnums } from '@/utils/validation';
|
||||
import { Tv2Icon } from 'lucide-react';
|
||||
|
||||
import { lineTypes } from '@mixan/constants';
|
||||
import { objectToZodEnums } from '@mixan/validation';
|
||||
|
||||
import { Combobox } from '../ui/combobox';
|
||||
import { changeLineType } from './reportSlice';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { IChartRange } from '@/types';
|
||||
import { timeRanges } from '@/utils/constants';
|
||||
import { CalendarIcon } from 'lucide-react';
|
||||
|
||||
import { timeRanges } from '@mixan/constants';
|
||||
import type { IChartRange } from '@mixan/validation';
|
||||
|
||||
import type { ExtendedComboboxProps } from '../ui/combobox';
|
||||
import { Combobox } from '../ui/combobox';
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { IChartSerie } from '@/server/api/routers/chart';
|
||||
import type { IChartInput } from '@/types';
|
||||
|
||||
import type { IChartInput } from '@mixan/validation';
|
||||
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
import { MetricCardLoading } from './MetricCard';
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import type { IChartData } from '@/app/_trpc/client';
|
||||
import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IChartMetric } from '@/types';
|
||||
import { theme } from '@/utils/theme';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { Area, AreaChart } from 'recharts';
|
||||
|
||||
import type { IChartMetric } from '@mixan/validation';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
|
||||
interface MetricCardProps {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import type { IChartLineType, IInterval } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import {
|
||||
@@ -16,6 +15,8 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
import type { IChartLineType, IInterval } from '@mixan/validation';
|
||||
|
||||
import { getYAxisWidth } from './chart-utils';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
|
||||
@@ -5,9 +5,10 @@ import type { IChartData } from '@/app/_trpc/client';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { NOT_SET_VALUE } from '@/utils/constants';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
|
||||
import { NOT_SET_VALUE } from '@mixan/constants';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { SerieIcon } from './SerieIcon';
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
|
||||
import { useSelector } from '@/redux';
|
||||
import type { IToolTipProps } from '@/types';
|
||||
|
||||
import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
|
||||
|
||||
@@ -4,11 +4,12 @@ import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import type { IInterval } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor, theme } from '@/utils/theme';
|
||||
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import type { IInterval } from '@mixan/validation';
|
||||
|
||||
import { getYAxisWidth } from './chart-utils';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
|
||||
@@ -6,7 +6,6 @@ import { AutoSizer } from '@/components/AutoSizer';
|
||||
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
|
||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||
import type { IChartLineType, IInterval } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getChartColor } from '@/utils/theme';
|
||||
import {
|
||||
@@ -18,6 +17,8 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
import type { IChartLineType, IInterval } from '@mixan/validation';
|
||||
|
||||
import { getYAxisWidth } from './chart-utils';
|
||||
import { useChartContext } from './ChartProvider';
|
||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { NOT_SET_VALUE } from '@/utils/constants';
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||
import {
|
||||
ActivityIcon,
|
||||
@@ -15,6 +14,8 @@ import {
|
||||
TabletIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@mixan/constants';
|
||||
|
||||
interface SerieIconProps extends LucideProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import type { RouterOutputs } 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 { 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 {
|
||||
IChartBreakdown,
|
||||
IChartEvent,
|
||||
@@ -6,15 +15,7 @@ import type {
|
||||
IChartRange,
|
||||
IChartType,
|
||||
IInterval,
|
||||
} from '@/types';
|
||||
import {
|
||||
alphabetIds,
|
||||
getDefaultIntervalByRange,
|
||||
isHourIntervalEnabledByRange,
|
||||
isMinuteIntervalEnabledByRange,
|
||||
} from '@/utils/constants';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
} from '@mixan/validation';
|
||||
|
||||
type InitialState = IChartInput & {
|
||||
dirty: boolean;
|
||||
|
||||
@@ -2,10 +2,11 @@ import { api } from '@/app/_trpc/client';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDispatch } from '@/redux';
|
||||
import type { IChartEvent } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { DatabaseIcon } from 'lucide-react';
|
||||
|
||||
import type { IChartEvent } from '@mixan/validation';
|
||||
|
||||
import { changeEvent } from '../reportSlice';
|
||||
|
||||
interface EventPropertiesComboboxProps {
|
||||
|
||||
@@ -5,9 +5,10 @@ import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import type { IChartBreakdown } from '@/types';
|
||||
import { SplitIcon } from 'lucide-react';
|
||||
|
||||
import type { IChartBreakdown } from '@mixan/validation';
|
||||
|
||||
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
|
||||
import { ReportBreakdownMore } from './ReportBreakdownMore';
|
||||
import type { ReportEventMoreProps } from './ReportEventMore';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { ColorSquare } from '@/components/ColorSquare';
|
||||
import { Dropdown } from '@/components/Dropdown';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -8,10 +7,12 @@ import { Combobox } from '@/components/ui/combobox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
||||
import { useEventNames } from '@/hooks/useEventNames';
|
||||
import { useDispatch, useSelector } from '@/redux';
|
||||
import type { IChartEvent } from '@/types';
|
||||
import { GanttChart, GanttChartIcon, Users } from 'lucide-react';
|
||||
|
||||
import type { IChartEvent } from '@mixan/validation';
|
||||
|
||||
import {
|
||||
addEvent,
|
||||
changeEvent,
|
||||
@@ -29,14 +30,8 @@ export function ReportEvents() {
|
||||
const selectedEvents = useSelector((state) => state.report.events);
|
||||
const dispatch = useDispatch();
|
||||
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) => {
|
||||
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"
|
||||
/>
|
||||
<Input
|
||||
@@ -189,7 +187,10 @@ export function ReportEvents() {
|
||||
})
|
||||
);
|
||||
}}
|
||||
items={eventsCombobox}
|
||||
items={eventNames.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}))}
|
||||
placeholder="Select event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,15 @@ import { RenderDots } from '@/components/ui/RenderDots';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useMappings } from '@/hooks/useMappings';
|
||||
import { useDispatch } from '@/redux';
|
||||
import { SlidersHorizontal, Trash } from 'lucide-react';
|
||||
|
||||
import { operators } from '@mixan/constants';
|
||||
import type {
|
||||
IChartEvent,
|
||||
IChartEventFilter,
|
||||
IChartEventFilterOperator,
|
||||
IChartEventFilterValue,
|
||||
} from '@/types';
|
||||
import { operators } from '@/utils/constants';
|
||||
import { SlidersHorizontal, Trash } from 'lucide-react';
|
||||
} from '@mixan/validation';
|
||||
import { mapKeys } from '@mixan/validation';
|
||||
|
||||
import { changeEvent } from '../../reportSlice';
|
||||
|
||||
@@ -67,7 +69,7 @@ export function FilterItem({ filter, event }: FilterProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const changeFilterOperator = (operator: IChartEventFilter['operator']) => {
|
||||
const changeFilterOperator = (operator: IChartEventFilterOperator) => {
|
||||
dispatch(
|
||||
changeEvent({
|
||||
...event,
|
||||
@@ -104,9 +106,9 @@ export function FilterItem({ filter, event }: FilterProps) {
|
||||
<div className="flex gap-1">
|
||||
<Dropdown
|
||||
onChange={changeFilterOperator}
|
||||
items={Object.entries(operators).map(([key, value]) => ({
|
||||
value: key as IChartEventFilter['operator'],
|
||||
label: value,
|
||||
items={mapKeys(operators).map((key) => ({
|
||||
value: key,
|
||||
label: operators[key],
|
||||
}))}
|
||||
label="Operator"
|
||||
>
|
||||
|
||||
@@ -2,9 +2,10 @@ import { api } from '@/app/_trpc/client';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useDispatch } from '@/redux';
|
||||
import type { IChartEvent } from '@/types';
|
||||
import { FilterIcon } from 'lucide-react';
|
||||
|
||||
import type { IChartEvent } from '@mixan/validation';
|
||||
|
||||
import { changeEvent } from '../../reportSlice';
|
||||
|
||||
interface FiltersComboboxProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IChartEvent } from '@/types';
|
||||
import type { IChartEvent } from '@mixan/validation';
|
||||
|
||||
import { FilterItem } from './FilterItem';
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ interface ComboboxAdvancedProps {
|
||||
onChange: React.Dispatch<React.SetStateAction<IValue[]>>;
|
||||
items: IItem[];
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ComboboxAdvanced({
|
||||
@@ -32,6 +33,7 @@ export function ComboboxAdvanced({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
}: ComboboxAdvancedProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
@@ -81,8 +83,12 @@ export function ComboboxAdvanced({
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant={'outline'} onClick={() => setOpen((prev) => !prev)}>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<Button
|
||||
variant={'outline'}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
className={className}
|
||||
>
|
||||
<div className="flex gap-1 flex-wrap w-full">
|
||||
{value.length === 0 && placeholder}
|
||||
{value.slice(0, 2).map((value) => {
|
||||
const item = items.find((item) => item.value === value) ?? {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { api } from '@/app/_trpc/client';
|
||||
|
||||
export function useEventNames(projectId: string) {
|
||||
const filterEventsQuery = api.chart.events.useQuery({
|
||||
const query = api.chart.events.useQuery({
|
||||
projectId: projectId,
|
||||
});
|
||||
|
||||
return (filterEventsQuery.data ?? []).map((item) => ({
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
}));
|
||||
return query.data ?? [];
|
||||
}
|
||||
|
||||
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 type { IChartInput } from '@/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
// 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;
|
||||
|
||||
function useFix<T>(hook: UseQueryStateReturn<T, undefined>) {
|
||||
return useMemo(
|
||||
() => ({
|
||||
get: hook[0],
|
||||
set: hook[1],
|
||||
}),
|
||||
[hook]
|
||||
);
|
||||
}
|
||||
type Operator = 'is' | 'isNot' | 'contains' | 'doesNotContain';
|
||||
|
||||
export const eventQueryFiltersParser = createParser({
|
||||
parse: (query: string) => {
|
||||
if (query === '') return [];
|
||||
const filters = query.split(';');
|
||||
|
||||
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 = {}) {
|
||||
// Ignore prettier so that we have all one same line
|
||||
// prettier-ignore
|
||||
return {
|
||||
path: useFix(useQueryState('path', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
referrer: useFix(useQueryState('referrer', parseAsString.withOptions({...nuqsOptions, ...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}))),
|
||||
utmCampaign: useFix(useQueryState('utmCampaign',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmContent: useFix(useQueryState('utmContent',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
utmTerm: useFix(useQueryState('utmTerm', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
continent: useFix(useQueryState('continent', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
country: useFix(useQueryState('country', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
region: useFix(useQueryState('region', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
city: useFix(useQueryState('city', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
device: useFix(useQueryState('device', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
browser: useFix(useQueryState('browser', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
browserVersion: useFix(useQueryState('browserVersion',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
os: useFix(useQueryState('os', parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
osVersion: useFix(useQueryState('osVersion',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
brand: useFix(useQueryState('brand',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
model: useFix(useQueryState('model',parseAsString.withOptions({...nuqsOptions, ...options}))),
|
||||
} as const;
|
||||
const [filters, setFilters] = useQueryState(
|
||||
'f',
|
||||
eventQueryFiltersParser.withDefault([]).withOptions({
|
||||
...nuqsOptions,
|
||||
...options,
|
||||
})
|
||||
);
|
||||
|
||||
const setFilter = useCallback(
|
||||
(
|
||||
name: string,
|
||||
value: string | number | boolean | undefined | null,
|
||||
operator: Operator = 'is'
|
||||
) => {
|
||||
setFilters((prev) => {
|
||||
const exists = prev.find((filter) => filter.name === name);
|
||||
if (exists) {
|
||||
// If same value is already set, remove the filter
|
||||
if (exists.value[0] === value) {
|
||||
return prev.filter((filter) => filter.name !== name);
|
||||
}
|
||||
|
||||
return prev.map((filter) => {
|
||||
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() {
|
||||
const eventQueryFilters = useEventQueryFilters();
|
||||
export const eventQueryNamesFilter = parseAsArrayOf(parseAsString).withDefault(
|
||||
[]
|
||||
);
|
||||
|
||||
const filters = useMemo(() => {
|
||||
return getEventFilters({
|
||||
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;
|
||||
export function useEventQueryNamesFilter(options: NuqsOptions = {}) {
|
||||
return useQueryState('events', eventQueryNamesFilter.withOptions(options));
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
export default authMiddleware({
|
||||
publicRoutes: ['/share/overview/:id', '/api/trpc(.*)'],
|
||||
debug: true,
|
||||
});
|
||||
|
||||
export const config = {
|
||||
|
||||
@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { IClientWithProject } from '@/types';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IServiceClient } from '@mixan/db';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type EditClientProps = IClientWithProject;
|
||||
type EditClientProps = IServiceClient;
|
||||
|
||||
const validator = z.object({
|
||||
id: z.string().min(1),
|
||||
|
||||
@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { IServiceDashboardWithProject } from '@/server/services/dashboard.service';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IServiceDashboard } from '@mixan/db';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type EditDashboardProps = IServiceDashboardWithProject;
|
||||
type EditDashboardProps = Exclude<IServiceDashboard, null>;
|
||||
|
||||
const validator = z.object({
|
||||
id: z.string().min(1),
|
||||
|
||||
@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
|
||||
import { ButtonContainer } from '@/components/ButtonContainer';
|
||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { IProject } from '@/types';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IServiceProject } from '@mixan/db';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type EditProjectProps = IProject;
|
||||
type EditProjectProps = Exclude<IServiceProject, null>;
|
||||
|
||||
const validator = z.object({
|
||||
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