refactor packages

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-19 10:55:15 +01:00
parent ae8482c1e3
commit 2f3c5ddf76
142 changed files with 2234 additions and 5507 deletions

View File

@@ -31,30 +31,20 @@
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"embla-carousel-autoplay": "8.0.0-rc22", "embla-carousel-autoplay": "8.0.0-rc22",
"embla-carousel-react": "8.0.0-rc22", "embla-carousel-react": "8.0.0-rc22",
"hamburger-react": "^2.5.0",
"lucide-react": "^0.323.0", "lucide-react": "^0.323.0",
"next": "~14.0.4", "next": "~14.0.4",
"nuqs": "^1.15.2",
"react": "18.2.0", "react": "18.2.0",
"react-animate-height": "^3.2.3", "react-animate-height": "^3.2.3",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-in-viewport": "1.0.0-alpha.30",
"react-responsive": "^9.0.2",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"sharp": "^0.33.2",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7"
"usehooks-ts": "^2.9.1",
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@mixan/eslint-config": "workspace:*", "@mixan/eslint-config": "workspace:*",
"@mixan/prettier-config": "workspace:*", "@mixan/prettier-config": "workspace:*",
"@mixan/tsconfig": "workspace:*", "@mixan/tsconfig": "workspace:*",
"@types/bcrypt": "^5.0.0",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^18.16.0", "@types/node": "^18.16.0",
"@types/ramda": "^0.29.6",
"@types/react": "^18.2.20", "@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/react-syntax-highlighter": "^15.5.9", "@types/react-syntax-highlighter": "^15.5.9",

View File

@@ -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(),
});
}

View File

@@ -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';
}

View File

@@ -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);
}

View File

@@ -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(' ');
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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]!;
}

View File

@@ -1,6 +0,0 @@
export function truncate(str: string, len: number) {
if (str.length <= len) {
return str;
}
return str.slice(0, len) + '...';
}

View File

@@ -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(),
});

View File

@@ -15,7 +15,8 @@ function getHostname(url: string | undefined) {
} }
export function parseReferrer(url: string | undefined) { export function parseReferrer(url: string | undefined) {
const match = referrers[getHostname(url)]; const hostname = getHostname(url);
const match = referrers[hostname] ?? referrers[hostname.replace('www.', '')];
return { return {
name: match?.name ?? '', name: match?.name ?? '',

View File

@@ -55,6 +55,8 @@ COPY packages/db/package.json packages/db/package.json
COPY packages/redis/package.json packages/redis/package.json COPY packages/redis/package.json packages/redis/package.json
COPY packages/queue/package.json packages/queue/package.json COPY packages/queue/package.json packages/queue/package.json
COPY packages/common/package.json packages/common/package.json COPY packages/common/package.json packages/common/package.json
COPY packages/constants/package.json packages/constants/package.json
COPY packages/validation/package.json packages/validation/package.json
COPY packages/types/package.json packages/types/package.json COPY packages/types/package.json packages/types/package.json
# BUILD # BUILD
@@ -92,11 +94,15 @@ COPY --from=build /app/packages/db /app/packages/db
COPY --from=build /app/packages/redis /app/packages/redis COPY --from=build /app/packages/redis /app/packages/redis
COPY --from=build /app/packages/common /app/packages/common COPY --from=build /app/packages/common /app/packages/common
COPY --from=build /app/packages/queue /app/packages/queue COPY --from=build /app/packages/queue /app/packages/queue
COPY --from=build /app/packages/constants /app/packages/constants
COPY --from=build /app/packages/validation /app/packages/validation
COPY --from=build /app/packages/types /app/packages/types COPY --from=build /app/packages/types /app/packages/types
# Packages node_modules # Packages node_modules
COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules COPY --from=prod /app/packages/db/node_modules /app/packages/db/node_modules
COPY --from=prod /app/packages/redis/node_modules /app/packages/redis/node_modules COPY --from=prod /app/packages/redis/node_modules /app/packages/redis/node_modules
COPY --from=prod /app/packages/common/node_modules /app/packages/common/node_modules COPY --from=prod /app/packages/common/node_modules /app/packages/common/node_modules
COPY --from=prod /app/packages/constants/node_modules /app/packages/constants/node_modules
COPY --from=prod /app/packages/validation/node_modules /app/packages/validation/node_modules
COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules COPY --from=prod /app/packages/queue/node_modules /app/packages/queue/node_modules
RUN pnpm db:codegen RUN pnpm db:codegen

View File

@@ -16,6 +16,8 @@
"@clickhouse/client": "^0.2.9", "@clickhouse/client": "^0.2.9",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@mixan/common": "workspace:^", "@mixan/common": "workspace:^",
"@mixan/constants": "workspace:^",
"@mixan/validation": "workspace:^",
"@mixan/db": "workspace:^", "@mixan/db": "workspace:^",
"@mixan/queue": "workspace:^", "@mixan/queue": "workspace:^",
"@mixan/types": "workspace:*", "@mixan/types": "workspace:*",
@@ -90,8 +92,8 @@
"@types/lodash.throttle": "^4.1.9", "@types/lodash.throttle": "^4.1.9",
"@types/node": "^18.19.15", "@types/node": "^18.19.15",
"@types/ramda": "^0.29.10", "@types/ramda": "^0.29.10",
"@types/react": "^18.2.55", "@types/react": "^18.2.20",
"@types/react-dom": "^18.2.19", "@types/react-dom": "^18.2.7",
"@types/react-syntax-highlighter": "^15.5.11", "@types/react-syntax-highlighter": "^15.5.11",
"@types/request-ip": "^0.0.41", "@types/request-ip": "^0.0.41",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",

View File

@@ -5,7 +5,6 @@ import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layo
import { LazyChart } from '@/components/report/chart/LazyChart'; import { LazyChart } from '@/components/report/chart/LazyChart';
import { ReportRange } from '@/components/report/ReportRange'; import { ReportRange } from '@/components/report/ReportRange';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -14,14 +13,15 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import type { getReportsByDashboardId } from '@/server/services/reports.service';
import type { IChartRange } from '@/types';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
import { ChevronRight, MoreHorizontal, PlusIcon, Trash } from 'lucide-react'; import { ChevronRight, MoreHorizontal, PlusIcon, Trash } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { getDefaultIntervalByRange } from '@mixan/constants';
import type { getReportsByDashboardId } from '@mixan/db';
import type { IChartRange } from '@mixan/validation';
interface ListReportsProps { interface ListReportsProps {
reports: Awaited<ReturnType<typeof getReportsByDashboardId>>; reports: Awaited<ReturnType<typeof getReportsByDashboardId>>;
} }

View File

@@ -1,9 +1,9 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { getDashboardById } from '@/server/services/dashboard.service';
import { getReportsByDashboardId } from '@/server/services/reports.service';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getDashboardById, getReportsByDashboardId } from '@mixan/db';
import { ListReports } from './list-reports'; import { ListReports } from './list-reports';
interface PageProps { interface PageProps {

View File

@@ -7,12 +7,13 @@ import { Button } from '@/components/ui/button';
import { ToastAction } from '@/components/ui/toast'; import { ToastAction } from '@/components/ui/toast';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { pushModal } from '@/modals'; import { pushModal } from '@/modals';
import type { IServiceDashboards } from '@/server/services/dashboard.service';
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react'; import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { IServiceDashboards } from '@mixan/db';
interface ListDashboardsProps { interface ListDashboardsProps {
dashboards: IServiceDashboards; dashboards: IServiceDashboards;
} }

View File

@@ -1,6 +1,7 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { getDashboardsByProjectId } from '@/server/services/dashboard.service';
import { getDashboardsByProjectId } from '@mixan/db';
import { HeaderDashboards } from './header-dashboards'; import { HeaderDashboards } from './header-dashboards';
import { ListDashboards } from './list-dashboards'; import { ListDashboards } from './list-dashboards';

View File

@@ -1,13 +1,16 @@
'use client'; 'use client';
import type { RouterOutputs } from '@/app/_trpc/client';
import { ExpandableListItem } from '@/components/general/ExpandableListItem'; import { ExpandableListItem } from '@/components/general/ExpandableListItem';
import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value'; import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; import {
useEventQueryFilters,
useEventQueryNamesFilter,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { getProfileName } from '@/utils/getters'; import { getProfileName } from '@/utils/getters';
import { round } from '@/utils/math'; import { round } from '@/utils/math';
import { uniq } from 'ramda';
import type { IServiceCreateEventPayload } from '@mixan/db'; import type { IServiceCreateEventPayload } from '@mixan/db';
@@ -40,7 +43,8 @@ export function EventListItem({
meta, meta,
}: EventListItemProps) { }: EventListItemProps) {
const params = useAppParams(); const params = useAppParams();
const eventQueryFilters = useEventQueryFilters({ shallow: false }); const [, setEvents] = useEventQueryNamesFilter({ shallow: false });
const [, setFilter] = useEventQueryFilters({ shallow: false });
const keyValueList = [ const keyValueList = [
{ {
name: 'Duration', name: 'Duration',
@@ -50,98 +54,98 @@ export function EventListItem({
name: 'Referrer', name: 'Referrer',
value: referrer, value: referrer,
onClick() { onClick() {
eventQueryFilters.referrer.set(referrer ?? null); setFilter('referrer', referrer ?? '');
}, },
}, },
{ {
name: 'Referrer name', name: 'Referrer name',
value: referrerName, value: referrerName,
onClick() { onClick() {
eventQueryFilters.referrerName.set(referrerName ?? null); setFilter('referrer_name', referrerName ?? '');
}, },
}, },
{ {
name: 'Referrer type', name: 'Referrer type',
value: referrerType, value: referrerType,
onClick() { onClick() {
eventQueryFilters.referrerType.set(referrerType ?? null); setFilter('referrer_type', referrerType ?? '');
}, },
}, },
{ {
name: 'Brand', name: 'Brand',
value: brand, value: brand,
onClick() { onClick() {
eventQueryFilters.brand.set(brand ?? null); setFilter('brand', brand ?? '');
}, },
}, },
{ {
name: 'Model', name: 'Model',
value: model, value: model,
onClick() { onClick() {
eventQueryFilters.model.set(model ?? null); setFilter('model', model ?? '');
}, },
}, },
{ {
name: 'Browser', name: 'Browser',
value: browser, value: browser,
onClick() { onClick() {
eventQueryFilters.browser.set(browser ?? null); setFilter('browser', browser ?? '');
}, },
}, },
{ {
name: 'Browser version', name: 'Browser version',
value: browserVersion, value: browserVersion,
onClick() { onClick() {
eventQueryFilters.browserVersion.set(browserVersion ?? null); setFilter('browser_version', browserVersion ?? '');
}, },
}, },
{ {
name: 'OS', name: 'OS',
value: os, value: os,
onClick() { onClick() {
eventQueryFilters.os.set(os ?? null); setFilter('os', os ?? '');
}, },
}, },
{ {
name: 'OS cersion', name: 'OS cersion',
value: osVersion, value: osVersion,
onClick() { onClick() {
eventQueryFilters.osVersion.set(osVersion ?? null); setFilter('os_version', osVersion ?? '');
}, },
}, },
{ {
name: 'City', name: 'City',
value: city, value: city,
onClick() { onClick() {
eventQueryFilters.city.set(city ?? null); setFilter('city', city ?? '');
}, },
}, },
{ {
name: 'Region', name: 'Region',
value: region, value: region,
onClick() { onClick() {
eventQueryFilters.region.set(region ?? null); setFilter('region', region ?? '');
}, },
}, },
{ {
name: 'Country', name: 'Country',
value: country, value: country,
onClick() { onClick() {
eventQueryFilters.country.set(country ?? null); setFilter('country', country ?? '');
}, },
}, },
{ {
name: 'Continent', name: 'Continent',
value: continent, value: continent,
onClick() { onClick() {
eventQueryFilters.continent.set(continent ?? null); setFilter('continent', continent ?? '');
}, },
}, },
{ {
name: 'Device', name: 'Device',
value: device, value: device,
onClick() { onClick() {
eventQueryFilters.device.set(device ?? null); setFilter('device', device ?? '');
}, },
}, },
].filter((item) => typeof item.value === 'string' && item.value); ].filter((item) => typeof item.value === 'string' && item.value);
@@ -156,7 +160,11 @@ export function EventListItem({
return ( return (
<ExpandableListItem <ExpandableListItem
className={cn(meta?.conversion && 'ring-2 ring-primary-500')} className={cn(meta?.conversion && 'ring-2 ring-primary-500')}
title={name.split('_').join(' ')} title={
<button onClick={() => setEvents((p) => uniq([...p, name]))}>
{name.split('_').join(' ')}
</button>
}
content={ content={
<> <>
<KeyValueSubtle name="Time" value={createdAt.toLocaleString()} /> <KeyValueSubtle name="Time" value={createdAt.toLocaleString()} />
@@ -172,7 +180,7 @@ export function EventListItem({
name="Path" name="Path"
value={path} value={path}
onClick={() => { onClick={() => {
eventQueryFilters.path.set(path); setFilter('path', path);
}} }}
/> />
)} )}
@@ -191,6 +199,13 @@ export function EventListItem({
key={item.name} key={item.name}
name={item.name} name={item.name}
value={item.value} value={item.value}
onClick={() => {
setFilter(
`properties.${item.name}`,
item.value ? String(item.value) : '',
'is'
);
}}
/> />
))} ))}
</div> </div>

View File

@@ -5,7 +5,7 @@ import { FullPageEmptyState } from '@/components/FullPageEmptyState';
import { Pagination } from '@/components/Pagination'; import { Pagination } from '@/components/Pagination';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useCursor } from '@/hooks/useCursor'; import { useCursor } from '@/hooks/useCursor';
import { useEventFilters } from '@/hooks/useEventQueryFilters'; import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
import { GanttChartIcon } from 'lucide-react'; import { GanttChartIcon } from 'lucide-react';
import type { IServiceCreateEventPayload } from '@mixan/db'; import type { IServiceCreateEventPayload } from '@mixan/db';
@@ -18,8 +18,7 @@ interface EventListProps {
} }
export function EventList({ data, count }: EventListProps) { export function EventList({ data, count }: EventListProps) {
const { cursor, setCursor } = useCursor(); const { cursor, setCursor } = useCursor();
const filters = useEventFilters(); const [filters] = useEventQueryFilters();
return ( return (
<Suspense> <Suspense>
<div className="p-4"> <div className="p-4">

View File

@@ -1,7 +1,10 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
import { getEventFilters } from '@/hooks/useEventQueryFilters'; import {
eventQueryFiltersParser,
eventQueryNamesFilter,
} from '@/hooks/useEventQueryFilters';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { getEventList, getEventsCount } from '@mixan/db'; import { getEventList, getEventsCount } from '@mixan/db';
@@ -15,27 +18,9 @@ interface PageProps {
organizationId: string; organizationId: string;
}; };
searchParams: { searchParams: {
events?: string;
cursor?: string; cursor?: string;
path?: string; f?: string;
device?: string;
referrer?: string;
referrerName?: string;
referrerType?: string;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
utmContent?: string;
utmTerm?: string;
continent?: string;
country?: string;
region?: string;
city?: string;
browser?: string;
browserVersion?: string;
os?: string;
osVersion?: string;
brand?: string;
model?: string;
}; };
} }
@@ -59,53 +44,13 @@ export default async function Page({
cursor: parseQueryAsNumber(searchParams.cursor), cursor: parseQueryAsNumber(searchParams.cursor),
projectId, projectId,
take: 50, take: 50,
filters: getEventFilters({ events: eventQueryNamesFilter.parse(searchParams.events ?? ''),
path: searchParams.path ?? null, filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined,
device: searchParams.device ?? null,
referrer: searchParams.referrer ?? null,
referrerName: searchParams.referrerName ?? null,
referrerType: searchParams.referrerType ?? null,
utmSource: searchParams.utmSource ?? null,
utmMedium: searchParams.utmMedium ?? null,
utmCampaign: searchParams.utmCampaign ?? null,
utmContent: searchParams.utmContent ?? null,
utmTerm: searchParams.utmTerm ?? null,
continent: searchParams.continent ?? null,
country: searchParams.country ?? null,
region: searchParams.region ?? null,
city: searchParams.city ?? null,
browser: searchParams.browser ?? null,
browserVersion: searchParams.browserVersion ?? null,
os: searchParams.os ?? null,
osVersion: searchParams.osVersion ?? null,
brand: searchParams.brand ?? null,
model: searchParams.model ?? null,
}),
}), }),
getEventsCount({ getEventsCount({
projectId, projectId,
filters: getEventFilters({ events: eventQueryNamesFilter.parse(searchParams.events ?? ''),
path: searchParams.path ?? null, filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined,
device: searchParams.device ?? null,
referrer: searchParams.referrer ?? null,
referrerName: searchParams.referrerName ?? null,
referrerType: searchParams.referrerType ?? null,
utmSource: searchParams.utmSource ?? null,
utmMedium: searchParams.utmMedium ?? null,
utmCampaign: searchParams.utmCampaign ?? null,
utmContent: searchParams.utmContent ?? null,
utmTerm: searchParams.utmTerm ?? null,
continent: searchParams.continent ?? null,
country: searchParams.country ?? null,
region: searchParams.region ?? null,
city: searchParams.city ?? null,
browser: searchParams.browser ?? null,
browserVersion: searchParams.browserVersion ?? null,
os: searchParams.os ?? null,
osVersion: searchParams.osVersion ?? null,
brand: searchParams.brand ?? null,
model: searchParams.model ?? null,
}),
}), }),
getExists(organizationId, projectId), getExists(organizationId, projectId),
]); ]);
@@ -116,6 +61,7 @@ export default async function Page({
<OverviewFiltersDrawer <OverviewFiltersDrawer
projectId={projectId} projectId={projectId}
nuqsOptions={nuqsOptions} nuqsOptions={nuqsOptions}
enableEventsFilter
/> />
<OverviewFiltersButtons <OverviewFiltersButtons
className="p-0 justify-end" className="p-0 justify-end"

View File

@@ -2,7 +2,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import type { IServiceDashboards } from '@/server/services/dashboard.service';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { useUser } from '@clerk/nextjs'; import { useUser } from '@clerk/nextjs';
import { import {
@@ -21,6 +20,8 @@ import type { LucideProps } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import type { IServiceDashboards } from '@mixan/db';
function LinkWithIcon({ function LinkWithIcon({
href, href,
icon: Icon, icon: Icon,

View File

@@ -2,10 +2,11 @@
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import type { IServiceOrganization } from '@/server/services/organization.service';
import { Building } from 'lucide-react'; import { Building } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import type { IServiceOrganization } from '@mixan/db';
interface LayoutOrganizationSelectorProps { interface LayoutOrganizationSelectorProps {
organizations: IServiceOrganization[]; organizations: IServiceOrganization[];
} }

View File

@@ -2,11 +2,12 @@
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import type { getCurrentProjects } from '@/server/services/project.service';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import type { getProjectsByOrganizationSlug } from '@mixan/db';
interface LayoutProjectSelectorProps { interface LayoutProjectSelectorProps {
projects: Awaited<ReturnType<typeof getCurrentProjects>>; projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
} }
export default function LayoutProjectSelector({ export default function LayoutProjectSelector({
projects, projects,

View File

@@ -2,13 +2,13 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Logo } from '@/components/Logo'; import { Logo } from '@/components/Logo';
import type { IServiceDashboards } from '@/server/services/dashboard.service';
import type { IServiceOrganization } from '@/server/services/organization.service';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { Rotate as Hamburger } from 'hamburger-react'; import { Rotate as Hamburger } from 'hamburger-react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import type { IServiceDashboards, IServiceOrganization } from '@mixan/db';
import LayoutMenu from './layout-menu'; import LayoutMenu from './layout-menu';
import LayoutOrganizationSelector from './layout-organization-selector'; import LayoutOrganizationSelector from './layout-organization-selector';

View File

@@ -1,5 +1,7 @@
import { getDashboardsByOrganization } from '@/server/services/dashboard.service'; import {
import { getCurrentOrganizations } from '@/server/services/organization.service'; getCurrentOrganizations,
getDashboardsByOrganization,
} from '@mixan/db';
import { LayoutSidebar } from './layout-sidebar'; import { LayoutSidebar } from './layout-sidebar';

View File

@@ -4,17 +4,18 @@ import { WidgetHead } from '@/components/overview/overview-widget';
import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
import { Chart } from '@/components/report/chart'; import { Chart } from '@/components/report/chart';
import { Widget, WidgetBody } from '@/components/Widget'; import { Widget, WidgetBody } from '@/components/Widget';
import { useEventFilters } from '@/hooks/useEventQueryFilters'; import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
import type { IChartInput } from '@/types';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import type { IChartInput } from '@mixan/validation';
interface OverviewMetricsProps { interface OverviewMetricsProps {
projectId: string; projectId: string;
} }
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) { export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
const { previous, range, interval, metric, setMetric } = useOverviewOptions(); const { previous, range, interval, metric, setMetric } = useOverviewOptions();
const filters = useEventFilters(); const [filters] = useEventQueryFilters();
const reports = [ const reports = [
{ {

View File

@@ -1,4 +1,4 @@
import { getCurrentProjects } from '@/server/services/project.service'; import { getProjectsByOrganizationSlug } from '@mixan/db';
import LayoutProjectSelector from './layout-project-selector'; import LayoutProjectSelector from './layout-project-selector';
@@ -13,7 +13,7 @@ export default async function PageLayout({
title, title,
organizationSlug, organizationSlug,
}: PageLayoutProps) { }: PageLayoutProps) {
const projects = await getCurrentProjects(organizationSlug); const projects = await getProjectsByOrganizationSlug(organizationSlug);
return ( return (
<> <>

View File

@@ -18,7 +18,7 @@ export default function ListProfileEvents({
projectId, projectId,
profileId, profileId,
}: ListProfileEvents) { }: ListProfileEvents) {
const pagination = usePagination(); const pagination = usePagination(50);
const [eventFilters, setEventFilters] = useQueryState( const [eventFilters, setEventFilters] = useQueryState(
'events', 'events',
parseAsJson<string[]>().withDefault([]) parseAsJson<string[]>().withDefault([])

View File

@@ -3,13 +3,11 @@ import { ListProperties } from '@/components/events/ListProperties';
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar'; import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget'; import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import {
getProfileById,
getProfilesByExternalId,
} from '@/server/services/profile.service';
import { formatDateTime } from '@/utils/date'; import { formatDateTime } from '@/utils/date';
import { getProfileName } from '@/utils/getters'; import { getProfileName } from '@/utils/getters';
import { getProfileById, getProfilesByExternalId } from '@mixan/db';
import ListProfileEvents from './list-profile-events'; import ListProfileEvents from './list-profile-events';
interface PageProps { interface PageProps {

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import { useMemo } from 'react';
import type { RouterOutputs } from '@/app/_trpc/client'; import type { RouterOutputs } from '@/app/_trpc/client';
import { ListProperties } from '@/components/events/ListProperties'; import { ListProperties } from '@/components/events/ListProperties';
import { ExpandableListItem } from '@/components/general/ExpandableListItem'; import { ExpandableListItem } from '@/components/general/ExpandableListItem';
@@ -16,24 +15,24 @@ export function ProfileListItem(props: ProfileListItemProps) {
const { id, properties, createdAt } = props; const { id, properties, createdAt } = props;
const params = useAppParams(); const params = useAppParams();
const bullets = useMemo(() => { const renderContent = () => {
const bullets: React.ReactNode[] = [ return (
<span>{formatDateTime(createdAt)}</span>, <>
<span>{formatDateTime(createdAt)}</span>
<Link <Link
href={`/${params.organizationId}/${params.projectId}/profiles/${id}`} href={`/${params.organizationId}/${params.projectId}/profiles/${id}`}
className="text-black font-medium hover:underline" className="text-black font-medium hover:underline"
> >
See profile See profile
</Link>, </Link>
]; </>
);
return bullets; };
}, [createdAt, id, params]);
return ( return (
<ExpandableListItem <ExpandableListItem
title={getProfileName(props)} title={getProfileName(props)}
bullets={bullets} content={renderContent()}
image={<ProfileAvatar {...props} />} image={<ProfileAvatar {...props} />}
> >
<ListProperties data={properties} className="rounded-none border-none" /> <ListProperties data={properties} className="rounded-none border-none" />

View File

@@ -1,10 +1,10 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { getOrganizationBySlug } from '@/server/services/organization.service';
import { getReportById } from '@/server/services/reports.service';
import { Pencil } from 'lucide-react'; import { Pencil } from 'lucide-react';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getOrganizationBySlug, getReportById } from '@mixan/db';
import ReportEditor from '../report-editor'; import ReportEditor from '../report-editor';
interface PageProps { interface PageProps {

View File

@@ -1,9 +1,10 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { getOrganizationBySlug } from '@/server/services/organization.service';
import { Pencil } from 'lucide-react'; import { Pencil } from 'lucide-react';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getOrganizationBySlug } from '@mixan/db';
import ReportEditor from './report-editor'; import ReportEditor from './report-editor';
interface PageProps { interface PageProps {

View File

@@ -19,9 +19,10 @@ import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import type { IServiceReport } from '@/server/services/reports.service';
import { GanttChartSquareIcon } from 'lucide-react'; import { GanttChartSquareIcon } from 'lucide-react';
import type { IServiceReport } from '@mixan/db';
interface ReportEditorProps { interface ReportEditorProps {
report: IServiceReport | null; report: IServiceReport | null;
} }

View File

@@ -6,9 +6,10 @@ import { DataTable } from '@/components/DataTable';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { pushModal } from '@/modals'; import { pushModal } from '@/modals';
import type { getClientsByOrganizationId } from '@/server/services/clients.service';
import { PlusIcon } from 'lucide-react'; import { PlusIcon } from 'lucide-react';
import type { getClientsByOrganizationId } from '@mixan/db';
interface ListClientsProps { interface ListClientsProps {
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>; clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
} }

View File

@@ -1,6 +1,7 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { getClientsByOrganizationId } from '@/server/services/clients.service';
import { getClientsByOrganizationId } from '@mixan/db';
import ListClients from './list-clients'; import ListClients from './list-clients';

View File

@@ -4,12 +4,13 @@ import { api, handleError } from '@/app/_trpc/client';
import { InputWithLabel } from '@/components/forms/InputWithLabel'; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget'; import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import type { getOrganizationBySlug } from '@/server/services/organization.service';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { z } from 'zod'; import { z } from 'zod';
import type { getOrganizationBySlug } from '@mixan/db';
const validator = z.object({ const validator = z.object({
id: z.string().min(2), id: z.string().min(2),
name: z.string().min(2), name: z.string().min(2),

View File

@@ -2,7 +2,6 @@ import { api } from '@/app/_trpc/client';
import { InputWithLabel } from '@/components/forms/InputWithLabel'; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { zInviteUser } from '@/utils/validation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { SendIcon } from 'lucide-react'; import { SendIcon } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -10,6 +9,8 @@ import { useForm } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { z } from 'zod'; import type { z } from 'zod';
import { zInviteUser } from '@mixan/validation';
type IForm = z.infer<typeof zInviteUser>; type IForm = z.infer<typeof zInviteUser>;
export function InviteUser() { export function InviteUser() {

View File

@@ -9,7 +9,8 @@ import {
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget'; import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import type { IServiceInvites } from '@/server/services/organization.service';
import type { IServiceInvites } from '@mixan/db';
import { InviteUser } from './invite-user'; import { InviteUser } from './invite-user';

View File

@@ -1,11 +1,9 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import {
getInvites,
getOrganizationBySlug,
} from '@/server/services/organization.service';
import { clerkClient } from '@clerk/nextjs'; import { clerkClient } from '@clerk/nextjs';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getInvites, getOrganizationBySlug } from '@mixan/db';
import EditOrganization from './edit-organization'; import EditOrganization from './edit-organization';
import InvitedUsers from './invited-users'; import InvitedUsers from './invited-users';

View File

@@ -4,13 +4,14 @@ import { api, handleError } from '@/app/_trpc/client';
import { InputWithLabel } from '@/components/forms/InputWithLabel'; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget'; import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import type { getUserById } from '@/server/services/user.service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { z } from 'zod'; import { z } from 'zod';
import type { getUserById } from '@mixan/db';
const validator = z.object({ const validator = z.object({
firstName: z.string().min(2), firstName: z.string().min(2),
lastName: z.string().min(2), lastName: z.string().min(2),

View File

@@ -1,8 +1,9 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { getUserById } from '@/server/services/user.service';
import { auth } from '@clerk/nextjs'; import { auth } from '@clerk/nextjs';
import { getUserById } from '@mixan/db';
import EditProfile from './edit-profile'; import EditProfile from './edit-profile';
import { Logout } from './logout'; import { Logout } from './logout';

View File

@@ -6,11 +6,12 @@ import { columns } from '@/components/projects/table';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { pushModal } from '@/modals'; import { pushModal } from '@/modals';
import type { getProjectsByOrganizationId } from '@/server/services/project.service';
import { PlusIcon } from 'lucide-react'; import { PlusIcon } from 'lucide-react';
import type { getProjectsByOrganizationSlug } from '@mixan/db';
interface ListProjectsProps { interface ListProjectsProps {
projects: Awaited<ReturnType<typeof getProjectsByOrganizationId>>; projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
} }
export default function ListProjects({ projects }: ListProjectsProps) { export default function ListProjects({ projects }: ListProjectsProps) {
const organizationId = useAppParams().organizationId; const organizationId = useAppParams().organizationId;

View File

@@ -1,6 +1,7 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { getProjectsByOrganizationSlug } from '@/server/services/project.service';
import { getProjectsByOrganizationSlug } from '@mixan/db';
import ListProjects from './list-projects'; import ListProjects from './list-projects';

View File

@@ -1,5 +1,5 @@
import { getOrganizationBySlug } from '@/server/services/organization.service'; import { getOrganizationBySlug } from '@mixan/db';
import { getProjectWithMostEvents } from '@/server/services/project.service'; import { getProjectWithMostEvents } from '@mixan/db';
import { notFound, redirect } from 'next/navigation'; import { notFound, redirect } from 'next/navigation';
import PageLayout from './[projectId]/page-layout'; import PageLayout from './[projectId]/page-layout';

View File

@@ -1,7 +1,8 @@
import { getCurrentOrganizations } from '@/server/services/organization.service';
import { CreateOrganization } from '@clerk/nextjs'; import { CreateOrganization } from '@clerk/nextjs';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { getCurrentOrganizations } from '@mixan/db';
export default async function Page() { export default async function Page() {
const organizations = await getCurrentOrganizations(); const organizations = await getCurrentOrganizations();

View File

@@ -11,10 +11,9 @@ import OverviewTopEvents from '@/components/overview/overview-top-events';
import OverviewTopGeo from '@/components/overview/overview-top-geo'; import OverviewTopGeo from '@/components/overview/overview-top-geo';
import OverviewTopPages from '@/components/overview/overview-top-pages'; import OverviewTopPages from '@/components/overview/overview-top-pages';
import OverviewTopSources from '@/components/overview/overview-top-sources'; import OverviewTopSources from '@/components/overview/overview-top-sources';
import { getOrganizationBySlug } from '@/server/services/organization.service';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getShareOverviewById } from '@mixan/db'; import { getOrganizationBySlug, getShareOverviewById } from '@mixan/db';
interface PageProps { interface PageProps {
params: { params: {

View File

@@ -1,5 +1,4 @@
import { appRouter } from '@/server/api/root'; import { appRouter } from '@/server/api/root';
import { getSession } from '@/server/auth';
import { getAuth } from '@clerk/nextjs/server'; import { getAuth } from '@clerk/nextjs/server';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
@@ -9,9 +8,7 @@ const handler = (req: Request) =>
req, req,
router: appRouter, router: appRouter,
async createContext({ req }) { async createContext({ req }) {
console.log('------- createContext --------');
const session = getAuth(req as any); const session = getAuth(req as any);
console.log('session', JSON.stringify(session, null, 2));
return { return {
session, session,
}; };

View 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',
},
],
};
}

View File

@@ -2,12 +2,13 @@
import { api } from '@/app/_trpc/client'; import { api } from '@/app/_trpc/client';
import { pushModal, showConfirm } from '@/modals'; import { pushModal, showConfirm } from '@/modals';
import type { IClientWithProject } from '@/types';
import { clipboard } from '@/utils/clipboard'; import { clipboard } from '@/utils/clipboard';
import { MoreHorizontal } from 'lucide-react'; import { MoreHorizontal } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { IServiceClientWithProject } from '@mixan/db';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { import {
DropdownMenu, DropdownMenu,
@@ -18,7 +19,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '../ui/dropdown-menu'; } from '../ui/dropdown-menu';
export function ClientActions(client: IClientWithProject) { export function ClientActions(client: IServiceClientWithProject) {
const { id } = client; const { id } = client;
const router = useRouter(); const router = useRouter();
const deletion = api.client.remove.useMutation({ const deletion = api.client.remove.useMutation({

View File

@@ -1,10 +1,11 @@
import type { IClientWithProject } from '@/types';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import type { IServiceClientWithProject } from '@mixan/db';
import { ClientActions } from './ClientActions'; import { ClientActions } from './ClientActions';
export const columns: ColumnDef<IClientWithProject>[] = [ export const columns: ColumnDef<IServiceClientWithProject>[] = [
{ {
accessorKey: 'name', accessorKey: 'name',
header: 'Name', header: 'Name',

View File

@@ -8,7 +8,7 @@ import { Button } from '../ui/button';
interface ExpandableListItemProps { interface ExpandableListItemProps {
children: React.ReactNode; children: React.ReactNode;
content: React.ReactNode; content: React.ReactNode;
title: string; title: React.ReactNode;
image?: React.ReactNode; image?: React.ReactNode;
initialOpen?: boolean; initialOpen?: boolean;
className?: string; className?: string;
@@ -29,7 +29,7 @@ export function ExpandableListItem({
<div className="p-2 sm:p-4 flex gap-4"> <div className="p-2 sm:p-4 flex gap-4">
<div className="flex gap-1">{image}</div> <div className="flex gap-1">{image}</div>
<div className="flex flex-col flex-1 gap-1 min-w-0"> <div className="flex flex-col flex-1 gap-1 min-w-0">
<span className="text-md font-medium leading-none mb-1">{title}</span> <div className="text-md font-medium leading-none mb-1">{title}</div>
{!!content && ( {!!content && (
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4"> <div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4">
{content} {content}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -1,10 +1,13 @@
'use client'; 'use client';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; import {
useEventQueryFilters,
useEventQueryNamesFilter,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { Options as NuqsOptions } from 'nuqs'; import type { Options as NuqsOptions } from 'nuqs';
interface OverviewFiltersButtonsProps { interface OverviewFiltersButtonsProps {
className?: string; className?: string;
@@ -15,25 +18,40 @@ export function OverviewFiltersButtons({
className, className,
nuqsOptions, nuqsOptions,
}: OverviewFiltersButtonsProps) { }: OverviewFiltersButtonsProps) {
const eventQueryFilters = useEventQueryFilters(nuqsOptions); const [events, setEvents] = useEventQueryNamesFilter(nuqsOptions);
const filters = Object.entries(eventQueryFilters).filter( const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
([, filter]) => filter.get !== null if (filters.length === 0 && events.length === 0) return null;
);
if (filters.length === 0) return null;
return ( return (
<div className={cn('flex flex-wrap gap-2 px-4 pb-4', className)}> <div className={cn('flex flex-wrap gap-2 px-4 pb-4', className)}>
{filters.map(([key, filter]) => ( {events.map((event) => (
<Button <Button
key={key} key={event}
size="sm" size="sm"
variant="outline" variant="outline"
icon={X} icon={X}
onClick={() => filter.set(null)} onClick={() => setEvents((p) => p.filter((e) => e !== event))}
> >
<span className="mr-1">{key} is</span> <strong>{event}</strong>
<strong>{filter.get}</strong>
</Button> </Button>
))} ))}
{filters.map((filter) => {
if (!filter.value[0]) {
return null;
}
return (
<Button
key={filter.name}
size="sm"
variant="outline"
icon={X}
onClick={() => setFilter(filter.name, filter.value[0], 'is')}
>
<span className="mr-1">{filter.name} is</span>
<strong>{filter.value[0]}</strong>
</Button>
);
})}
</div> </div>
); );
} }

View File

@@ -1,93 +1,131 @@
'use client';
import { api } from '@/app/_trpc/client';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
import { SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { useEventNames } from '@/hooks/useEventNames';
import { useEventProperties } from '@/hooks/useEventProperties';
import {
useEventQueryFilters,
useEventQueryNamesFilter,
} from '@/hooks/useEventQueryFilters';
import { useEventValues } from '@/hooks/useEventValues';
import { XIcon } from 'lucide-react'; import { XIcon } from 'lucide-react';
import { Options as NuqsOptions } from 'nuqs'; import type { Options as NuqsOptions } from 'nuqs';
import type {
IChartEventFilter,
IChartEventFilterOperator,
IChartEventFilterValue,
} from '@mixan/validation';
interface OverviewFiltersProps { interface OverviewFiltersProps {
projectId: string; projectId: string;
nuqsOptions?: NuqsOptions; nuqsOptions?: NuqsOptions;
enableEventsFilter?: boolean;
} }
export function OverviewFiltersDrawerContent({ export function OverviewFiltersDrawerContent({
projectId, projectId,
nuqsOptions, nuqsOptions,
enableEventsFilter,
}: OverviewFiltersProps) { }: OverviewFiltersProps) {
const eventQueryFilters = useEventQueryFilters(nuqsOptions); const [filters, setFilter] = useEventQueryFilters(nuqsOptions);
const [event, setEvent] = useEventQueryNamesFilter(nuqsOptions);
const eventNames = useEventNames(projectId);
const eventProperties = useEventProperties(projectId);
return ( return (
<div> <div>
<h2 className="text-xl font-medium mb-8">Overview filters</h2> <SheetHeader className="mb-8">
<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 <Combobox
className="w-full" className="w-full"
onChange={(value) => { onChange={(value) => {
// @ts-expect-error setFilter(value, '');
eventQueryFilters[value].set('');
}} }}
value="" value=""
placeholder="Filter by..." placeholder="Filter by property"
label="What do you want to filter by?" label="What do you want to filter by?"
items={Object.entries(eventQueryFilters) items={eventProperties.map((item) => ({
.filter(([, filter]) => filter.get === null) label: item,
.map(([name]) => ({ value: item,
label: name,
value: name,
}))} }))}
searchable searchable
/> />
</div>
<div className="flex flex-col gap-4 mt-8"> <div className="flex flex-col gap-4 mt-8">
{Object.entries(eventQueryFilters) {filters
.filter(([, filter]) => filter.get !== null) .filter((filter) => filter.value[0] !== null)
.map(([name, filter]) => ( .map((filter) => {
return (
<FilterOption <FilterOption
key={name} key={filter.name}
projectId={projectId} projectId={projectId}
name={name} setFilter={setFilter}
{...filter} {...filter}
/> />
))} );
})}
</div> </div>
</div> </div>
); );
} }
export function FilterOption({ export function FilterOption({
name, setFilter,
get,
set,
projectId, projectId,
}: { ...filter
name: string; }: IChartEventFilter & {
get: string | null;
set: (value: string | null) => void;
projectId: string; projectId: string;
setFilter: (
name: string,
value: IChartEventFilterValue,
operator: IChartEventFilterOperator
) => void;
}) { }) {
const { data } = api.chart.values.useQuery({ const values = useEventValues(
projectId, projectId,
event: name === 'path' ? 'screen_view' : 'session_start', filter.name === 'path' ? 'screen_view' : 'session_start',
property: name, filter.name
}); );
return ( return (
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<div>{name}</div> <div>{filter.name}</div>
<Combobox <Combobox
className="flex-1" className="flex-1"
onChange={(value) => set(value)} onChange={(value) => setFilter(filter.name, value, filter.operator)}
placeholder={'Select a value'} placeholder={'Select a value'}
items={ items={values.map((value) => ({
data?.values.filter(Boolean).map((value) => ({
value, value,
label: value, label: value,
})) ?? [] }))}
} value={String(filter.value[0] ?? '')}
value={get}
/> />
<Button size="icon" variant="ghost" onClick={() => set(null)}> <Button
size="icon"
variant="ghost"
onClick={() =>
setFilter(filter.name, filter.value[0] ?? '', filter.operator)
}
>
<XIcon /> <XIcon />
</Button> </Button>
</div> </div>

View File

@@ -3,18 +3,20 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { FilterIcon } from 'lucide-react'; import { FilterIcon } from 'lucide-react';
import { Options as NuqsOptions } from 'nuqs'; import type { Options as NuqsOptions } from 'nuqs';
import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content'; import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
interface OverviewFiltersDrawerProps { interface OverviewFiltersDrawerProps {
projectId: string; projectId: string;
nuqsOptions?: NuqsOptions; nuqsOptions?: NuqsOptions;
enableEventsFilter?: boolean;
} }
export function OverviewFiltersDrawer({ export function OverviewFiltersDrawer({
projectId, projectId,
nuqsOptions, nuqsOptions,
enableEventsFilter,
}: OverviewFiltersDrawerProps) { }: OverviewFiltersDrawerProps) {
return ( return (
<Sheet> <Sheet>
@@ -27,6 +29,7 @@ export function OverviewFiltersDrawer({
<OverviewFiltersDrawerContent <OverviewFiltersDrawerContent
projectId={projectId} projectId={projectId}
nuqsOptions={nuqsOptions} nuqsOptions={nuqsOptions}
enableEventsFilter={enableEventsFilter}
/> />
</SheetContent> </SheetContent>
</Sheet> </Sheet>

View File

@@ -1,10 +1,11 @@
'use client'; 'use client';
import type { IChartInput } from '@/types';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { ChevronsUpDownIcon } from 'lucide-react'; import { ChevronsUpDownIcon } from 'lucide-react';
import AnimateHeight from 'react-animate-height'; import AnimateHeight from 'react-animate-height';
import type { IChartInput } from '@mixan/validation';
import { Chart } from '../report/chart'; import { Chart } from '../report/chart';
import { Widget, WidgetBody, WidgetHead } from '../Widget'; import { Widget, WidgetBody, WidgetHead } from '../Widget';
import { useOverviewOptions } from './useOverviewOptions'; import { useOverviewOptions } from './useOverviewOptions';

View File

@@ -1,10 +1,7 @@
'use client'; 'use client';
import { Chart } from '@/components/report/chart'; import { Chart } from '@/components/report/chart';
import { import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
useEventFilters,
useEventQueryFilters,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget'; import { Widget, WidgetBody } from '../Widget';
@@ -19,9 +16,7 @@ export default function OverviewTopDevices({
projectId, projectId,
}: OverviewTopDevicesProps) { }: OverviewTopDevicesProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous } = useOverviewOptions();
const filters = useEventFilters(); const [filters, setFilter] = useEventQueryFilters();
const { device, browser, browserVersion, os, osVersion } =
useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('tech', { const [widget, setWidget, widgets] = useOverviewWidget('tech', {
devices: { devices: {
title: 'Top devices', title: 'Top devices',
@@ -190,21 +185,21 @@ export default function OverviewTopDevices({
onClick={(item) => { onClick={(item) => {
switch (widget.key) { switch (widget.key) {
case 'devices': case 'devices':
device.set(item.name); setFilter('device', item.name);
break; break;
case 'browser': case 'browser':
setWidget('browser_version'); setWidget('browser_version');
browser.set(item.name); setFilter('browser', item.name);
break; break;
case 'browser_version': case 'browser_version':
browserVersion.set(item.name); setFilter('browser_version', item.name);
break; break;
case 'os': case 'os':
setWidget('os_version'); setWidget('os_version');
os.set(item.name); setFilter('os', item.name);
break; break;
case 'os_version': case 'os_version':
osVersion.set(item.name); setFilter('os_version', item.name);
break; break;
} }
}} }}

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { Chart } from '@/components/report/chart'; import { Chart } from '@/components/report/chart';
import { useEventFilters } from '@/hooks/useEventQueryFilters'; import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget'; import { Widget, WidgetBody } from '../Widget';
@@ -16,7 +16,7 @@ export default function OverviewTopEvents({
projectId, projectId,
}: OverviewTopEventsProps) { }: OverviewTopEventsProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous } = useOverviewOptions();
const filters = useEventFilters(); const [filters] = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('ev', { const [widget, setWidget, widgets] = useOverviewWidget('ev', {
all: { all: {
title: 'Top events', title: 'Top events',

View File

@@ -1,10 +1,7 @@
'use client'; 'use client';
import { Chart } from '@/components/report/chart'; import { Chart } from '@/components/report/chart';
import { import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
useEventFilters,
useEventQueryFilters,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget'; import { Widget, WidgetBody } from '../Widget';
@@ -17,8 +14,7 @@ interface OverviewTopGeoProps {
} }
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) { export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous } = useOverviewOptions();
const filters = useEventFilters(); const [filters, setFilter] = useEventQueryFilters();
const { region, country, city } = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('geo', { const [widget, setWidget, widgets] = useOverviewWidget('geo', {
map: { map: {
title: 'Map', title: 'Map',
@@ -160,14 +156,14 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
switch (widget.key) { switch (widget.key) {
case 'countries': case 'countries':
setWidget('regions'); setWidget('regions');
country.set(item.name); setFilter('country', item.name);
break; break;
case 'regions': case 'regions':
setWidget('cities'); setWidget('cities');
region.set(item.name); setFilter('region', item.name);
break; break;
case 'cities': case 'cities':
city.set(item.name); setFilter('city', item.name);
break; break;
} }
}} }}

View File

@@ -1,10 +1,7 @@
'use client'; 'use client';
import { Chart } from '@/components/report/chart'; import { Chart } from '@/components/report/chart';
import { import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
useEventFilters,
useEventQueryFilters,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget'; import { Widget, WidgetBody } from '../Widget';
@@ -17,8 +14,7 @@ interface OverviewTopPagesProps {
} }
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) { export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous } = useOverviewOptions();
const filters = useEventFilters(); const [filters, setFilter] = useEventQueryFilters();
const { path } = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('pages', { const [widget, setWidget, widgets] = useOverviewWidget('pages', {
top: { top: {
title: 'Top pages', title: 'Top pages',
@@ -129,7 +125,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
{...widget.chart} {...widget.chart}
previous={false} previous={false}
onClick={(item) => { onClick={(item) => {
path.set(item.name); setFilter('path', item.name);
}} }}
/> />
</WidgetBody> </WidgetBody>

View File

@@ -1,10 +1,7 @@
'use client'; 'use client';
import { Chart } from '@/components/report/chart'; import { Chart } from '@/components/report/chart';
import { import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
useEventFilters,
useEventQueryFilters,
} from '@/hooks/useEventQueryFilters';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { Widget, WidgetBody } from '../Widget'; import { Widget, WidgetBody } from '../Widget';
@@ -19,17 +16,7 @@ export default function OverviewTopSources({
projectId, projectId,
}: OverviewTopSourcesProps) { }: OverviewTopSourcesProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous } = useOverviewOptions();
const { const [filters, setFilter] = useEventQueryFilters();
referrer,
referrerName,
referrerType,
utmCampaign,
utmContent,
utmMedium,
utmSource,
utmTerm,
} = useEventQueryFilters();
const filters = useEventFilters();
const [widget, setWidget, widgets] = useOverviewWidget('sources', { const [widget, setWidget, widgets] = useOverviewWidget('sources', {
all: { all: {
title: 'Top sources', title: 'Top sources',
@@ -282,30 +269,30 @@ export default function OverviewTopSources({
onClick={(item) => { onClick={(item) => {
switch (widget.key) { switch (widget.key) {
case 'all': case 'all':
referrerName.set(item.name); setFilter('referrer_name', item.name);
setWidget('domain'); setWidget('domain');
break; break;
case 'domain': case 'domain':
referrer.set(item.name); setFilter('referrer', item.name);
break; break;
case 'type': case 'type':
referrerType.set(item.name); setFilter('referrer_type', item.name);
setWidget('domain'); setWidget('domain');
break; break;
case 'utm_source': case 'utm_source':
utmSource.set(item.name); setFilter('utm_source', item.name);
break; break;
case 'utm_medium': case 'utm_medium':
utmMedium.set(item.name); setFilter('utm_medium', item.name);
break; break;
case 'utm_campaign': case 'utm_campaign':
utmCampaign.set(item.name); setFilter('utm_campaign', item.name);
break; break;
case 'utm_term': case 'utm_term':
utmTerm.set(item.name); setFilter('utm_term', item.name);
break; break;
case 'utm_content': case 'utm_content':
utmContent.set(item.name); setFilter('utm_content', item.name);
break; break;
} }
}} }}

View File

@@ -1,6 +1,3 @@
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
import { mapKeys } from '@/utils/validation';
import { import {
parseAsBoolean, parseAsBoolean,
parseAsInteger, parseAsInteger,
@@ -8,6 +5,9 @@ import {
useQueryState, useQueryState,
} from 'nuqs'; } from 'nuqs';
import { getDefaultIntervalByRange, timeRanges } from '@mixan/constants';
import { mapKeys } from '@mixan/validation';
const nuqsOptions = { history: 'push' } as const; const nuqsOptions = { history: 'push' } as const;
export function useOverviewOptions() { export function useOverviewOptions() {

View File

@@ -1,7 +1,8 @@
import type { IChartInput } from '@/types';
import { mapKeys } from '@/utils/validation';
import { parseAsStringEnum, useQueryState } from 'nuqs'; import { parseAsStringEnum, useQueryState } from 'nuqs';
import { mapKeys } from '@mixan/validation';
import type { IChartInput } from '@mixan/validation';
export function useOverviewWidget<T extends string>( export function useOverviewWidget<T extends string>(
key: string, key: string,
widgets: Record<T, { title: string; btn: string; chart: IChartInput }> widgets: Record<T, { title: string; btn: string; chart: IChartInput }>
@@ -15,7 +16,7 @@ export function useOverviewWidget<T extends string>(
); );
return [ return [
{ {
...widgets[widget]!, ...widgets[widget],
key: widget, key: widget,
}, },
setWidget, setWidget,

View File

@@ -1,11 +1,12 @@
'use client'; 'use client';
import type { IServiceProfile } from '@/server/services/profile.service';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { AvatarImage } from '@radix-ui/react-avatar'; import { AvatarImage } from '@radix-ui/react-avatar';
import type { VariantProps } from 'class-variance-authority'; import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority'; import { cva } from 'class-variance-authority';
import type { IServiceProfile } from '@mixan/db';
import { Avatar, AvatarFallback } from '../ui/avatar'; import { Avatar, AvatarFallback } from '../ui/avatar';
interface ProfileAvatarProps interface ProfileAvatarProps

View File

@@ -2,12 +2,13 @@
import { api } from '@/app/_trpc/client'; import { api } from '@/app/_trpc/client';
import { pushModal, showConfirm } from '@/modals'; import { pushModal, showConfirm } from '@/modals';
import type { IProject } from '@/types';
import { clipboard } from '@/utils/clipboard'; import { clipboard } from '@/utils/clipboard';
import { MoreHorizontal } from 'lucide-react'; import { MoreHorizontal } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { IServiceProject } from '@mixan/db';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { import {
DropdownMenu, DropdownMenu,
@@ -18,7 +19,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '../ui/dropdown-menu'; } from '../ui/dropdown-menu';
export function ProjectActions(project: IProject) { export function ProjectActions(project: Exclude<IServiceProject, null>) {
const { id } = project; const { id } = project;
const router = useRouter(); const router = useRouter();
const deletion = api.project.remove.useMutation({ const deletion = api.project.remove.useMutation({

View File

@@ -1,7 +1,7 @@
import { IServiceProject } from '@/server/services/project.service';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import { IServiceProject } from '@mixan/db';
import type { Project as IProject } from '@mixan/db'; import type { Project as IProject } from '@mixan/db';
import { ProjectActions } from './ProjectActions'; import { ProjectActions } from './ProjectActions';

View File

@@ -1,8 +1,9 @@
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import { chartTypes } from '@/utils/constants';
import { objectToZodEnums } from '@/utils/validation';
import { LineChartIcon } from 'lucide-react'; import { LineChartIcon } from 'lucide-react';
import { chartTypes } from '@mixan/constants';
import { objectToZodEnums } from '@mixan/validation';
import { Combobox } from '../ui/combobox'; import { Combobox } from '../ui/combobox';
import { changeChartType } from './reportSlice'; import { changeChartType } from './reportSlice';

View File

@@ -1,10 +1,11 @@
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import type { IInterval } from '@/types'; import { ClockIcon } from 'lucide-react';
import { import {
isHourIntervalEnabledByRange, isHourIntervalEnabledByRange,
isMinuteIntervalEnabledByRange, isMinuteIntervalEnabledByRange,
} from '@/utils/constants'; } from '@mixan/constants';
import { ClockIcon } from 'lucide-react'; import type { IInterval } from '@mixan/validation';
import { Combobox } from '../ui/combobox'; import { Combobox } from '../ui/combobox';
import { changeInterval } from './reportSlice'; import { changeInterval } from './reportSlice';
@@ -32,7 +33,7 @@ export function ReportInterval({ className }: ReportIntervalProps) {
className={className} className={className}
placeholder="Interval" placeholder="Interval"
onChange={(value) => { onChange={(value) => {
dispatch(changeInterval(value as IInterval)); dispatch(changeInterval(value));
}} }}
value={interval} value={interval}
items={[ items={[

View File

@@ -1,8 +1,9 @@
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import { lineTypes } from '@/utils/constants';
import { objectToZodEnums } from '@/utils/validation';
import { Tv2Icon } from 'lucide-react'; import { Tv2Icon } from 'lucide-react';
import { lineTypes } from '@mixan/constants';
import { objectToZodEnums } from '@mixan/validation';
import { Combobox } from '../ui/combobox'; import { Combobox } from '../ui/combobox';
import { changeLineType } from './reportSlice'; import { changeLineType } from './reportSlice';

View File

@@ -1,7 +1,8 @@
import type { IChartRange } from '@/types';
import { timeRanges } from '@/utils/constants';
import { CalendarIcon } from 'lucide-react'; import { CalendarIcon } from 'lucide-react';
import { timeRanges } from '@mixan/constants';
import type { IChartRange } from '@mixan/validation';
import type { ExtendedComboboxProps } from '../ui/combobox'; import type { ExtendedComboboxProps } from '../ui/combobox';
import { Combobox } from '../ui/combobox'; import { Combobox } from '../ui/combobox';

View File

@@ -10,7 +10,8 @@ import {
useState, useState,
} from 'react'; } from 'react';
import type { IChartSerie } from '@/server/api/routers/chart'; import type { IChartSerie } from '@/server/api/routers/chart';
import type { IChartInput } from '@/types';
import type { IChartInput } from '@mixan/validation';
import { ChartLoading } from './ChartLoading'; import { ChartLoading } from './ChartLoading';
import { MetricCardLoading } from './MetricCard'; import { MetricCardLoading } from './MetricCard';

View File

@@ -3,11 +3,12 @@
import type { IChartData } from '@/app/_trpc/client'; import type { IChartData } from '@/app/_trpc/client';
import { ColorSquare } from '@/components/ColorSquare'; import { ColorSquare } from '@/components/ColorSquare';
import { useNumber } from '@/hooks/useNumerFormatter'; import { useNumber } from '@/hooks/useNumerFormatter';
import type { IChartMetric } from '@/types';
import { theme } from '@/utils/theme'; import { theme } from '@/utils/theme';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { Area, AreaChart } from 'recharts'; import { Area, AreaChart } from 'recharts';
import type { IChartMetric } from '@mixan/validation';
import { PreviousDiffIndicator } from '../PreviousDiffIndicator'; import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
interface MetricCardProps { interface MetricCardProps {

View File

@@ -4,7 +4,6 @@ import { AutoSizer } from '@/components/AutoSizer';
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval'; import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import { useRechartDataModel } from '@/hooks/useRechartDataModel'; import { useRechartDataModel } from '@/hooks/useRechartDataModel';
import { useVisibleSeries } from '@/hooks/useVisibleSeries'; import { useVisibleSeries } from '@/hooks/useVisibleSeries';
import type { IChartLineType, IInterval } from '@/types';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { getChartColor } from '@/utils/theme'; import { getChartColor } from '@/utils/theme';
import { import {
@@ -16,6 +15,8 @@ import {
YAxis, YAxis,
} from 'recharts'; } from 'recharts';
import type { IChartLineType, IInterval } from '@mixan/validation';
import { getYAxisWidth } from './chart-utils'; import { getYAxisWidth } from './chart-utils';
import { useChartContext } from './ChartProvider'; import { useChartContext } from './ChartProvider';
import { ReportChartTooltip } from './ReportChartTooltip'; import { ReportChartTooltip } from './ReportChartTooltip';

View File

@@ -5,9 +5,10 @@ import type { IChartData } from '@/app/_trpc/client';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { useNumber } from '@/hooks/useNumerFormatter'; import { useNumber } from '@/hooks/useNumerFormatter';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { NOT_SET_VALUE } from '@/utils/constants';
import { getChartColor } from '@/utils/theme'; import { getChartColor } from '@/utils/theme';
import { NOT_SET_VALUE } from '@mixan/constants';
import { PreviousDiffIndicator } from '../PreviousDiffIndicator'; import { PreviousDiffIndicator } from '../PreviousDiffIndicator';
import { useChartContext } from './ChartProvider'; import { useChartContext } from './ChartProvider';
import { SerieIcon } from './SerieIcon'; import { SerieIcon } from './SerieIcon';

View File

@@ -3,7 +3,6 @@ import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import { useMappings } from '@/hooks/useMappings'; import { useMappings } from '@/hooks/useMappings';
import { useNumber } from '@/hooks/useNumerFormatter'; import { useNumber } from '@/hooks/useNumerFormatter';
import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel'; import type { IRechartPayloadItem } from '@/hooks/useRechartDataModel';
import { useSelector } from '@/redux';
import type { IToolTipProps } from '@/types'; import type { IToolTipProps } from '@/types';
import { PreviousDiffIndicator } from '../PreviousDiffIndicator'; import { PreviousDiffIndicator } from '../PreviousDiffIndicator';

View File

@@ -4,11 +4,12 @@ import { AutoSizer } from '@/components/AutoSizer';
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval'; import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import { useRechartDataModel } from '@/hooks/useRechartDataModel'; import { useRechartDataModel } from '@/hooks/useRechartDataModel';
import { useVisibleSeries } from '@/hooks/useVisibleSeries'; import { useVisibleSeries } from '@/hooks/useVisibleSeries';
import type { IInterval } from '@/types';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { getChartColor, theme } from '@/utils/theme'; import { getChartColor, theme } from '@/utils/theme';
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
import type { IInterval } from '@mixan/validation';
import { getYAxisWidth } from './chart-utils'; import { getYAxisWidth } from './chart-utils';
import { useChartContext } from './ChartProvider'; import { useChartContext } from './ChartProvider';
import { ReportChartTooltip } from './ReportChartTooltip'; import { ReportChartTooltip } from './ReportChartTooltip';

View File

@@ -6,7 +6,6 @@ import { AutoSizer } from '@/components/AutoSizer';
import { useFormatDateInterval } from '@/hooks/useFormatDateInterval'; import { useFormatDateInterval } from '@/hooks/useFormatDateInterval';
import { useRechartDataModel } from '@/hooks/useRechartDataModel'; import { useRechartDataModel } from '@/hooks/useRechartDataModel';
import { useVisibleSeries } from '@/hooks/useVisibleSeries'; import { useVisibleSeries } from '@/hooks/useVisibleSeries';
import type { IChartLineType, IInterval } from '@/types';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { getChartColor } from '@/utils/theme'; import { getChartColor } from '@/utils/theme';
import { import {
@@ -18,6 +17,8 @@ import {
YAxis, YAxis,
} from 'recharts'; } from 'recharts';
import type { IChartLineType, IInterval } from '@mixan/validation';
import { getYAxisWidth } from './chart-utils'; import { getYAxisWidth } from './chart-utils';
import { useChartContext } from './ChartProvider'; import { useChartContext } from './ChartProvider';
import { ReportChartTooltip } from './ReportChartTooltip'; import { ReportChartTooltip } from './ReportChartTooltip';

View File

@@ -1,5 +1,4 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { NOT_SET_VALUE } from '@/utils/constants';
import type { LucideIcon, LucideProps } from 'lucide-react'; import type { LucideIcon, LucideProps } from 'lucide-react';
import { import {
ActivityIcon, ActivityIcon,
@@ -15,6 +14,8 @@ import {
TabletIcon, TabletIcon,
} from 'lucide-react'; } from 'lucide-react';
import { NOT_SET_VALUE } from '@mixan/constants';
interface SerieIconProps extends LucideProps { interface SerieIconProps extends LucideProps {
name: string; name: string;
} }

View File

@@ -3,7 +3,8 @@
import { memo, useEffect, useState } from 'react'; import { memo, useEffect, useState } from 'react';
import type { RouterOutputs } from '@/app/_trpc/client'; import type { RouterOutputs } from '@/app/_trpc/client';
import { api } from '@/app/_trpc/client'; import { api } from '@/app/_trpc/client';
import type { IChartInput } from '@/types';
import type { IChartInput } from '@mixan/validation';
import { ChartEmpty } from './ChartEmpty'; import { ChartEmpty } from './ChartEmpty';
import { ChartLoading } from './ChartLoading'; import { ChartLoading } from './ChartLoading';

View File

@@ -1,3 +1,12 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import {
alphabetIds,
getDefaultIntervalByRange,
isHourIntervalEnabledByRange,
isMinuteIntervalEnabledByRange,
} from '@mixan/constants';
import type { import type {
IChartBreakdown, IChartBreakdown,
IChartEvent, IChartEvent,
@@ -6,15 +15,7 @@ import type {
IChartRange, IChartRange,
IChartType, IChartType,
IInterval, IInterval,
} from '@/types'; } from '@mixan/validation';
import {
alphabetIds,
getDefaultIntervalByRange,
isHourIntervalEnabledByRange,
isMinuteIntervalEnabledByRange,
} from '@/utils/constants';
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
type InitialState = IChartInput & { type InitialState = IChartInput & {
dirty: boolean; dirty: boolean;

View File

@@ -2,10 +2,11 @@ import { api } from '@/app/_trpc/client';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { useDispatch } from '@/redux'; import { useDispatch } from '@/redux';
import type { IChartEvent } from '@/types';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { DatabaseIcon } from 'lucide-react'; import { DatabaseIcon } from 'lucide-react';
import type { IChartEvent } from '@mixan/validation';
import { changeEvent } from '../reportSlice'; import { changeEvent } from '../reportSlice';
interface EventPropertiesComboboxProps { interface EventPropertiesComboboxProps {

View File

@@ -5,9 +5,10 @@ import { ColorSquare } from '@/components/ColorSquare';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import type { IChartBreakdown } from '@/types';
import { SplitIcon } from 'lucide-react'; import { SplitIcon } from 'lucide-react';
import type { IChartBreakdown } from '@mixan/validation';
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice'; import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
import { ReportBreakdownMore } from './ReportBreakdownMore'; import { ReportBreakdownMore } from './ReportBreakdownMore';
import type { ReportEventMoreProps } from './ReportEventMore'; import type { ReportEventMoreProps } from './ReportEventMore';

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import { api } from '@/app/_trpc/client';
import { ColorSquare } from '@/components/ColorSquare'; import { ColorSquare } from '@/components/ColorSquare';
import { Dropdown } from '@/components/Dropdown'; import { Dropdown } from '@/components/Dropdown';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
@@ -8,10 +7,12 @@ import { Combobox } from '@/components/ui/combobox';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { useDebounceFn } from '@/hooks/useDebounceFn'; import { useDebounceFn } from '@/hooks/useDebounceFn';
import { useEventNames } from '@/hooks/useEventNames';
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import type { IChartEvent } from '@/types';
import { GanttChart, GanttChartIcon, Users } from 'lucide-react'; import { GanttChart, GanttChartIcon, Users } from 'lucide-react';
import type { IChartEvent } from '@mixan/validation';
import { import {
addEvent, addEvent,
changeEvent, changeEvent,
@@ -29,14 +30,8 @@ export function ReportEvents() {
const selectedEvents = useSelector((state) => state.report.events); const selectedEvents = useSelector((state) => state.report.events);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { projectId } = useAppParams(); const { projectId } = useAppParams();
const eventNames = useEventNames(projectId);
const eventsQuery = api.chart.events.useQuery({
projectId,
});
const eventsCombobox = (eventsQuery.data ?? []).map((item) => ({
value: item.name,
label: item.name,
}));
const dispatchChangeEvent = useDebounceFn((event: IChartEvent) => { const dispatchChangeEvent = useDebounceFn((event: IChartEvent) => {
dispatch(changeEvent(event)); dispatch(changeEvent(event));
}); });
@@ -76,7 +71,10 @@ export function ReportEvents() {
}) })
); );
}} }}
items={eventsCombobox} items={eventNames.map((item) => ({
label: item.name,
value: item.name,
}))}
placeholder="Select event" placeholder="Select event"
/> />
<Input <Input
@@ -189,7 +187,10 @@ export function ReportEvents() {
}) })
); );
}} }}
items={eventsCombobox} items={eventNames.map((item) => ({
label: item.name,
value: item.name,
}))}
placeholder="Select event" placeholder="Select event"
/> />
</div> </div>

View File

@@ -7,13 +7,15 @@ import { RenderDots } from '@/components/ui/RenderDots';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { useMappings } from '@/hooks/useMappings'; import { useMappings } from '@/hooks/useMappings';
import { useDispatch } from '@/redux'; import { useDispatch } from '@/redux';
import { SlidersHorizontal, Trash } from 'lucide-react';
import { operators } from '@mixan/constants';
import type { import type {
IChartEvent, IChartEvent,
IChartEventFilter, IChartEventFilterOperator,
IChartEventFilterValue, IChartEventFilterValue,
} from '@/types'; } from '@mixan/validation';
import { operators } from '@/utils/constants'; import { mapKeys } from '@mixan/validation';
import { SlidersHorizontal, Trash } from 'lucide-react';
import { changeEvent } from '../../reportSlice'; import { changeEvent } from '../../reportSlice';
@@ -67,7 +69,7 @@ export function FilterItem({ filter, event }: FilterProps) {
); );
}; };
const changeFilterOperator = (operator: IChartEventFilter['operator']) => { const changeFilterOperator = (operator: IChartEventFilterOperator) => {
dispatch( dispatch(
changeEvent({ changeEvent({
...event, ...event,
@@ -104,9 +106,9 @@ export function FilterItem({ filter, event }: FilterProps) {
<div className="flex gap-1"> <div className="flex gap-1">
<Dropdown <Dropdown
onChange={changeFilterOperator} onChange={changeFilterOperator}
items={Object.entries(operators).map(([key, value]) => ({ items={mapKeys(operators).map((key) => ({
value: key as IChartEventFilter['operator'], value: key,
label: value, label: operators[key],
}))} }))}
label="Operator" label="Operator"
> >

View File

@@ -2,9 +2,10 @@ import { api } from '@/app/_trpc/client';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { useDispatch } from '@/redux'; import { useDispatch } from '@/redux';
import type { IChartEvent } from '@/types';
import { FilterIcon } from 'lucide-react'; import { FilterIcon } from 'lucide-react';
import type { IChartEvent } from '@mixan/validation';
import { changeEvent } from '../../reportSlice'; import { changeEvent } from '../../reportSlice';
interface FiltersComboboxProps { interface FiltersComboboxProps {

View File

@@ -1,4 +1,4 @@
import type { IChartEvent } from '@/types'; import type { IChartEvent } from '@mixan/validation';
import { FilterItem } from './FilterItem'; import { FilterItem } from './FilterItem';

View File

@@ -25,6 +25,7 @@ interface ComboboxAdvancedProps {
onChange: React.Dispatch<React.SetStateAction<IValue[]>>; onChange: React.Dispatch<React.SetStateAction<IValue[]>>;
items: IItem[]; items: IItem[];
placeholder: string; placeholder: string;
className?: string;
} }
export function ComboboxAdvanced({ export function ComboboxAdvanced({
@@ -32,6 +33,7 @@ export function ComboboxAdvanced({
value, value,
onChange, onChange,
placeholder, placeholder,
className,
}: ComboboxAdvancedProps) { }: ComboboxAdvancedProps) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState(''); const [inputValue, setInputValue] = React.useState('');
@@ -81,8 +83,12 @@ export function ComboboxAdvanced({
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant={'outline'} onClick={() => setOpen((prev) => !prev)}> <Button
<div className="flex gap-1 flex-wrap"> variant={'outline'}
onClick={() => setOpen((prev) => !prev)}
className={className}
>
<div className="flex gap-1 flex-wrap w-full">
{value.length === 0 && placeholder} {value.length === 0 && placeholder}
{value.slice(0, 2).map((value) => { {value.slice(0, 2).map((value) => {
const item = items.find((item) => item.value === value) ?? { const item = items.find((item) => item.value === value) ?? {

View File

@@ -1,12 +1,9 @@
import { api } from '@/app/_trpc/client'; import { api } from '@/app/_trpc/client';
export function useEventNames(projectId: string) { export function useEventNames(projectId: string) {
const filterEventsQuery = api.chart.events.useQuery({ const query = api.chart.events.useQuery({
projectId: projectId, projectId: projectId,
}); });
return (filterEventsQuery.data ?? []).map((item) => ({ return query.data ?? [];
value: item.name,
label: item.name,
}));
} }

View 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 ?? [];
}

View File

@@ -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,
};
}

View File

@@ -1,326 +1,102 @@
import { useMemo } from 'react'; import { useCallback } from 'react';
import type { IChartInput } from '@/types';
// prettier-ignore // prettier-ignore
import type { Options as NuqsOptions, UseQueryStateReturn } from 'nuqs'; import type { Options as NuqsOptions } from 'nuqs';
import { parseAsString, useQueryState } from 'nuqs'; import {
createParser,
parseAsArrayOf,
parseAsString,
useQueryState,
} from 'nuqs';
const nuqsOptions = { history: 'push' } as const; const nuqsOptions = { history: 'push' } as const;
function useFix<T>(hook: UseQueryStateReturn<T, undefined>) { type Operator = 'is' | 'isNot' | 'contains' | 'doesNotContain';
return useMemo(
() => ({ export const eventQueryFiltersParser = createParser({
get: hook[0], parse: (query: string) => {
set: hook[1], if (query === '') return [];
}), const filters = query.split(';');
[hook]
return (
filters.map((filter) => {
const [key, operator, value] = filter.split(',');
return {
id: key!,
name: key!,
operator: (operator ?? 'is') as Operator,
value: [decodeURIComponent(value!)],
};
}) ?? []
); );
} },
serialize: (value) => {
return value
.map(
(filter) =>
`${filter.id},${filter.operator},${encodeURIComponent(filter.value[0] ?? '')}`
)
.join(';');
},
});
export function useEventQueryFilters(options: NuqsOptions = {}) { export function useEventQueryFilters(options: NuqsOptions = {}) {
// Ignore prettier so that we have all one same line const [filters, setFilters] = useQueryState(
// prettier-ignore 'f',
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 { return {
path: useFix(useQueryState('path', parseAsString.withOptions({...nuqsOptions, ...options}))), ...filter,
referrer: useFix(useQueryState('referrer', parseAsString.withOptions({...nuqsOptions, ...options}))), operator,
referrerName: useFix(useQueryState('referrerName',parseAsString.withOptions({...nuqsOptions, ...options}))), value: [String(value)],
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;
} }
return filter;
export function useEventFilters() {
const eventQueryFilters = useEventQueryFilters();
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) { return [
filters.push({ ...prev,
id: 'device', {
operator: 'is', id: name,
name: 'device' as const, name,
value: [device], operator,
value: [String(value)],
},
];
}); });
},
[setFilters]
);
return [filters, setFilter, setFilters] as const;
} }
if (referrer) { export const eventQueryNamesFilter = parseAsArrayOf(parseAsString).withDefault(
filters.push({ []
id: 'referrer', );
operator: 'is',
name: 'referrer' as const,
value: [referrer],
});
}
if (referrerName) { export function useEventQueryNamesFilter(options: NuqsOptions = {}) {
filters.push({ return useQueryState('events', eventQueryNamesFilter.withOptions(options));
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;
} }

View 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 ?? [];
}

View File

@@ -1,4 +1,4 @@
import type { IInterval } from '@/types'; import type { IInterval } from '@mixan/validation';
export function formatDateInterval(interval: IInterval, date: Date): string { export function formatDateInterval(interval: IInterval, date: Date): string {
if (interval === 'hour' || interval === 'minute') { if (interval === 'hour' || interval === 'minute') {

View File

@@ -5,7 +5,6 @@ import { authMiddleware } from '@clerk/nextjs';
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
export default authMiddleware({ export default authMiddleware({
publicRoutes: ['/share/overview/:id', '/api/trpc(.*)'], publicRoutes: ['/share/overview/:id', '/api/trpc(.*)'],
debug: true,
}); });
export const config = { export const config = {

View File

@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
import { ButtonContainer } from '@/components/ButtonContainer'; import { ButtonContainer } from '@/components/ButtonContainer';
import { InputWithLabel } from '@/components/forms/InputWithLabel'; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import type { IClientWithProject } from '@/types';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { z } from 'zod'; import { z } from 'zod';
import type { IServiceClient } from '@mixan/db';
import { popModal } from '.'; import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container'; import { ModalContent, ModalHeader } from './Modal/Container';
type EditClientProps = IClientWithProject; type EditClientProps = IServiceClient;
const validator = z.object({ const validator = z.object({
id: z.string().min(1), id: z.string().min(1),

View File

@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
import { ButtonContainer } from '@/components/ButtonContainer'; import { ButtonContainer } from '@/components/ButtonContainer';
import { InputWithLabel } from '@/components/forms/InputWithLabel'; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import type { IServiceDashboardWithProject } from '@/server/services/dashboard.service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { z } from 'zod'; import { z } from 'zod';
import type { IServiceDashboard } from '@mixan/db';
import { popModal } from '.'; import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container'; import { ModalContent, ModalHeader } from './Modal/Container';
type EditDashboardProps = IServiceDashboardWithProject; type EditDashboardProps = Exclude<IServiceDashboard, null>;
const validator = z.object({ const validator = z.object({
id: z.string().min(1), id: z.string().min(1),

View File

@@ -4,17 +4,18 @@ import { api, handleError } from '@/app/_trpc/client';
import { ButtonContainer } from '@/components/ButtonContainer'; import { ButtonContainer } from '@/components/ButtonContainer';
import { InputWithLabel } from '@/components/forms/InputWithLabel'; import { InputWithLabel } from '@/components/forms/InputWithLabel';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import type { IProject } from '@/types';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { z } from 'zod'; import { z } from 'zod';
import type { IServiceProject } from '@mixan/db';
import { popModal } from '.'; import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container'; import { ModalContent, ModalHeader } from './Modal/Container';
type EditProjectProps = IProject; type EditProjectProps = Exclude<IServiceProject, null>;
const validator = z.object({ const validator = z.object({
id: z.string().min(1), id: z.string().min(1),

Some files were not shown because too many files have changed in this diff Show More