chore(root): migrate to biome

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-09-16 12:20:40 +02:00
parent 1f6e198336
commit 32e91959f6
383 changed files with 1943 additions and 3085 deletions

View File

@@ -30,7 +30,7 @@ import {
getDefaultIntervalByRange,
timeWindows,
} from '@openpanel/constants';
import type { getReportsByDashboardId, IServiceDashboard } from '@openpanel/db';
import type { IServiceDashboard, getReportsByDashboardId } from '@openpanel/db';
import { OverviewReportRange } from '../../overview-sticky-header';
@@ -64,7 +64,7 @@ export function ListReports({ reports, dashboard }: ListReportsProps) {
params.projectId
}/reports?${new URLSearchParams({
dashboardId: params.dashboardId,
}).toString()}`
}).toString()}`,
);
}}
>
@@ -163,7 +163,7 @@ export function ListReports({ reports, dashboard }: ListReportsProps) {
params.projectId
}/reports?${new URLSearchParams({
dashboardId: params.dashboardId,
}).toString()}`
}).toString()}`,
)
}
className="mt-14"

View File

@@ -79,7 +79,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
{dashboards.map((item) => {
const visibleReports = item.reports.slice(
0,
item.reports.length > 6 ? 5 : 6
item.reports.length > 6 ? 5 : 6,
);
return (
<Card key={item.id} hover>
@@ -97,7 +97,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
<div
className={cn(
'mt-4 grid gap-2',
'grid-cols-1 @sm:grid-cols-2'
'grid-cols-1 @sm:grid-cols-2',
)}
>
{visibleReports.map((report) => {
@@ -145,6 +145,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
<CardActions>
<CardActionsItem className="w-full" asChild>
<button
type="button"
onClick={() => {
pushModal('EditDashboard', item);
}}
@@ -155,6 +156,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
</CardActionsItem>
<CardActionsItem className="w-full text-destructive" asChild>
<button
type="button"
onClick={() => {
deletion.mutate({
id: item.id,

View File

@@ -15,7 +15,7 @@ const Conversions = ({ projectId }: Props) => {
},
{
keepPreviousData: true,
}
},
);
return <EventsTable query={query} />;

View File

@@ -23,7 +23,7 @@ const Events = ({ projectId, profileId }: Props) => {
const [eventNames] = useEventQueryNamesFilter();
const [cursor, setCursor] = useQueryState(
'cursor',
parseAsInteger.withDefault(0)
parseAsInteger.withDefault(0),
);
const query = api.event.events.useQuery(
{
@@ -36,7 +36,7 @@ const Events = ({ projectId, profileId }: Props) => {
},
{
keepPreviousData: true,
}
},
);
return (

View File

@@ -27,16 +27,16 @@ export default function Page({
<Padding>
<div className="mb-4">
<PageTabs>
<PageTabsLink href={`?tab=events`} isActive={tab === 'events'}>
<PageTabsLink href={'?tab=events'} isActive={tab === 'events'}>
Events
</PageTabsLink>
<PageTabsLink
href={`?tab=conversions`}
href={'?tab=conversions'}
isActive={tab === 'conversions'}
>
Conversions
</PageTabsLink>
<PageTabsLink href={`?tab=charts`} isActive={tab === 'charts'}>
<PageTabsLink href={'?tab=charts'} isActive={tab === 'charts'}>
Charts
</PageTabsLink>
</PageTabs>

View File

@@ -1,6 +1,5 @@
'use client';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { useAppParams } from '@/hooks/useAppParams';
import { pushModal } from '@/modals';
@@ -19,6 +18,7 @@ import {
import type { LucideProps } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import type { IServiceDashboards } from '@openpanel/db';
@@ -42,7 +42,7 @@ function LinkWithIcon({
className={cn(
'text-text flex items-center gap-2 rounded-md px-3 py-2 font-medium transition-all hover:bg-def-200',
active && 'bg-def-200',
className
className,
)}
href={href}
>

View File

@@ -18,7 +18,7 @@ export default function LayoutOrganizationSelector({
const router = useRouter();
const organization = organizations.find(
(item) => item.id === params.organizationSlug
(item) => item.id === params.organizationSlug,
);
return (

View File

@@ -1,6 +1,5 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox';
import {
@@ -24,6 +23,7 @@ import {
PlusIcon,
} from 'lucide-react';
import { usePathname, useRouter } from 'next/navigation';
import { useState } from 'react';
import type {
getCurrentOrganizations,
@@ -50,7 +50,7 @@ export default function LayoutProjectSelector({
const split = pathname
.replace(
`/${organizationSlug}/${projectId}`,
`/${organizationSlug}/${newProjectId}`
`/${organizationSlug}/${newProjectId}`,
)
.split('/');
// slicing here will remove everything after /{orgId}/{projectId}/dashboards [slice here] /xxx/xxx/xxx

View File

@@ -1,6 +1,5 @@
'use client';
import { useEffect, useState } from 'react';
import { LogoSquare } from '@/components/logo';
import SettingsToggle from '@/components/settings-toggle';
import { Button } from '@/components/ui/button';
@@ -8,11 +7,12 @@ import { cn } from '@/utils/cn';
import { Rotate as Hamburger } from 'hamburger-react';
import { MenuIcon, XIcon } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import type {
getProjectsByOrganizationSlug,
IServiceDashboards,
IServiceOrganization,
getProjectsByOrganizationSlug,
} from '@openpanel/db';
import LayoutMenu from './layout-menu';
@@ -40,19 +40,20 @@ export function LayoutSidebar({
return (
<>
<button
type="button"
onClick={() => setActive(false)}
className={cn(
'fixed bottom-0 left-0 right-0 top-0 z-50 backdrop-blur-sm transition-opacity',
active
? 'pointer-events-auto opacity-100'
: 'pointer-events-none opacity-0'
: 'pointer-events-none opacity-0',
)}
/>
<div
className={cn(
'fixed left-0 top-0 z-50 flex h-screen w-72 flex-col border-r border-border bg-card transition-transform',
'-translate-x-72 lg:-translate-x-0', // responsive
active && 'translate-x-0' // force active on mobile
active && 'translate-x-0', // force active on mobile
)}
>
<div className="absolute -right-12 flex h-16 items-center lg:hidden">
@@ -77,7 +78,7 @@ export function LayoutSidebar({
<LayoutMenu dashboards={dashboards} />
</div>
<div className="fixed bottom-0 left-0 right-0">
<div className="h-8 w-full bg-gradient-to-t from-card to-card/0"></div>
<div className="h-8 w-full bg-gradient-to-t from-card to-card/0" />
</div>
</div>
</>

View File

@@ -13,7 +13,7 @@ export function StickyBelowHeader({
<div
className={cn(
'top-0 z-20 border-b border-border bg-card md:sticky',
className
className,
)}
>
{children}

View File

@@ -1,11 +1,11 @@
'use client';
import { memo } from 'react';
import { ReportChart } from '@/components/report-chart';
import { useNumber } from '@/hooks/useNumerFormatter';
import { cn } from '@/utils/cn';
import isEqual from 'lodash.isequal';
import { ExternalLinkIcon } from 'lucide-react';
import { memo } from 'react';
import type { IServicePage } from '@openpanel/db';
@@ -38,7 +38,7 @@ export const PagesTable = memo(
className={cn(
cell,
'center-center font-mono text-lg font-semibold',
index === data.length - 1 && 'rounded-bl-md'
index === data.length - 1 && 'rounded-bl-md',
)}
>
{number.short(item.count)}
@@ -46,7 +46,7 @@ export const PagesTable = memo(
<div
className={cn(
cell,
'flex w-80 flex-col justify-center gap-2 text-left'
'flex w-80 flex-col justify-center gap-2 text-left',
)}
>
<span className="truncate font-medium">{item.title}</span>
@@ -68,7 +68,7 @@ export const PagesTable = memo(
className={cn(
cell,
'p-1',
index === data.length - 1 && 'rounded-br-md'
index === data.length - 1 && 'rounded-br-md',
)}
>
<ReportChart
@@ -122,7 +122,7 @@ export const PagesTable = memo(
},
(prevProps, nextProps) => {
return isEqual(prevProps.data, nextProps.data);
}
},
);
PagesTable.displayName = 'PagesTable';

View File

@@ -15,7 +15,7 @@ export function Pages({ projectId }: { projectId: string }) {
const take = 20;
const [cursor, setCursor] = useQueryState(
'cursor',
parseAsInteger.withDefault(0)
parseAsInteger.withDefault(0),
);
const [search, setSearch] = useQueryState('search', {
defaultValue: '',
@@ -31,7 +31,7 @@ export function Pages({ projectId }: { projectId: string }) {
},
{
keepPreviousData: true,
}
},
);
const data = query.data ?? [];
@@ -56,7 +56,7 @@ export function Pages({ projectId }: { projectId: string }) {
className="mt-2"
setCursor={setCursor}
cursor={cursor}
count={Infinity}
count={Number.POSITIVE_INFINITY}
take={take}
loading={query.isFetching}
/>

View File

@@ -1,7 +1,7 @@
import withLoadingWidget from '@/hocs/with-loading-widget';
import { escape } from 'sqlstring';
import { chQuery, TABLE_NAMES } from '@openpanel/db';
import { TABLE_NAMES, chQuery } from '@openpanel/db';
import MostEvents from './most-events';
@@ -12,7 +12,7 @@ type Props = {
const MostEventsServer = async ({ projectId, profileId }: Props) => {
const data = await chQuery<{ count: number; name: string }>(
`SELECT count(*) as count, name FROM ${TABLE_NAMES.events} WHERE name NOT IN ('screen_view', 'session_start', 'session_end') AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY name ORDER BY count DESC`
`SELECT count(*) as count, name FROM ${TABLE_NAMES.events} WHERE name NOT IN ('screen_view', 'session_start', 'session_end') AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY name ORDER BY count DESC`,
);
return <MostEvents data={data} />;
};

View File

@@ -22,7 +22,7 @@ const MostEvents = ({ data }: Props) => {
style={{
width: `${(item.count / max) * 100}%`,
}}
></div>
/>
<div className="relative flex justify-between ">
<div>{item.name}</div>
<div>{item.count}</div>

View File

@@ -1,7 +1,7 @@
import withLoadingWidget from '@/hocs/with-loading-widget';
import { escape } from 'sqlstring';
import { chQuery, TABLE_NAMES } from '@openpanel/db';
import { TABLE_NAMES, chQuery } from '@openpanel/db';
import PopularRoutes from './popular-routes';
@@ -12,7 +12,7 @@ type Props = {
const PopularRoutesServer = async ({ projectId, profileId }: Props) => {
const data = await chQuery<{ count: number; path: string }>(
`SELECT count(*) as count, path FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY path ORDER BY count DESC`
`SELECT count(*) as count, path FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY path ORDER BY count DESC`,
);
return <PopularRoutes data={data} />;
};

View File

@@ -22,7 +22,7 @@ const PopularRoutes = ({ data }: Props) => {
style={{
width: `${(item.count / max) * 100}%`,
}}
></div>
/>
<div className="relative flex justify-between ">
<div>{item.path}</div>
<div>{item.count}</div>

View File

@@ -1,7 +1,7 @@
import withLoadingWidget from '@/hocs/with-loading-widget';
import { escape } from 'sqlstring';
import { chQuery, TABLE_NAMES } from '@openpanel/db';
import { TABLE_NAMES, chQuery } from '@openpanel/db';
import ProfileActivity from './profile-activity';
@@ -12,7 +12,7 @@ type Props = {
const ProfileActivityServer = async ({ projectId, profileId }: Props) => {
const data = await chQuery<{ count: number; date: string }>(
`SELECT count(*) as count, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY date ORDER BY date DESC`
`SELECT count(*) as count, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY date ORDER BY date DESC`,
);
return <ProfileActivity data={data} />;
};

View File

@@ -1,6 +1,5 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Widget,
@@ -20,6 +19,7 @@ import {
subMonths,
} from 'date-fns';
import { ActivityIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
import { useState } from 'react';
type Props = {
data: { count: number; date: string }[];
@@ -64,17 +64,17 @@ const ProfileActivity = ({ data }: Props) => {
}).map((date) => {
const hit = data.find((item) =>
item.date.includes(
formatISO(date, { representation: 'date' })
)
formatISO(date, { representation: 'date' }),
),
);
return (
<div
key={date.toISOString()}
className={cn(
'aspect-square w-full rounded',
hit ? 'bg-highlight' : 'bg-def-200'
hit ? 'bg-highlight' : 'bg-def-200',
)}
></div>
/>
);
})}
</div>
@@ -90,17 +90,17 @@ const ProfileActivity = ({ data }: Props) => {
}).map((date) => {
const hit = data.find((item) =>
item.date.includes(
formatISO(date, { representation: 'date' })
)
formatISO(date, { representation: 'date' }),
),
);
return (
<div
key={date.toISOString()}
className={cn(
'aspect-square w-full rounded',
hit ? 'bg-highlight' : 'bg-def-200'
hit ? 'bg-highlight' : 'bg-def-200',
)}
></div>
/>
);
})}
</div>
@@ -116,17 +116,17 @@ const ProfileActivity = ({ data }: Props) => {
}).map((date) => {
const hit = data.find((item) =>
item.date.includes(
formatISO(date, { representation: 'date' })
)
formatISO(date, { representation: 'date' }),
),
);
return (
<div
key={date.toISOString()}
className={cn(
'aspect-square w-full rounded',
hit ? 'bg-highlight' : 'bg-def-200'
hit ? 'bg-highlight' : 'bg-def-200',
)}
></div>
/>
);
})}
</div>
@@ -140,17 +140,17 @@ const ProfileActivity = ({ data }: Props) => {
}).map((date) => {
const hit = data.find((item) =>
item.date.includes(
formatISO(date, { representation: 'date' })
)
formatISO(date, { representation: 'date' }),
),
);
return (
<div
key={date.toISOString()}
className={cn(
'aspect-square w-full rounded',
hit ? 'bg-highlight' : 'bg-def-200'
hit ? 'bg-highlight' : 'bg-def-200',
)}
></div>
/>
);
})}
</div>

View File

@@ -1,8 +1,8 @@
'use client';
import { memo } from 'react';
import { ReportChart } from '@/components/report-chart';
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
import { memo } from 'react';
import type { IChartProps } from '@openpanel/validation';

View File

@@ -24,7 +24,7 @@ const Events = ({ projectId, profileId }: Props) => {
const [eventNames] = useEventQueryNamesFilter();
const [cursor, setCursor] = useQueryState(
'cursor',
parseAsInteger.withDefault(0)
parseAsInteger.withDefault(0),
);
const query = api.event.events.useQuery(
{
@@ -37,7 +37,7 @@ const Events = ({ projectId, profileId }: Props) => {
},
{
keepPreviousData: true,
}
},
);
return (

View File

@@ -35,7 +35,7 @@ function Info({ title, value }: { title: string; value: string }) {
const ProfileMetrics = ({ data, profile }: Props) => {
const [tab, setTab] = useQueryState(
'tab',
parseAsStringEnum(['profile', 'properties']).withDefault('profile')
parseAsStringEnum(['profile', 'properties']).withDefault('profile'),
);
const number = useNumber();
return (
@@ -44,24 +44,26 @@ const ProfileMetrics = ({ data, profile }: Props) => {
<div className="col-span-2 @xl:col-span-3 @4xl:col-span-6">
<div className="row border-b">
<button
type="button"
onClick={() => setTab('profile')}
className={cn(
'p-4',
'opacity-50',
tab === 'profile' &&
'border-b border-foreground text-foreground opacity-100'
'border-b border-foreground text-foreground opacity-100',
)}
>
Profile
</button>
<div className="h-full w-px bg-border" />
<button
type="button"
onClick={() => setTab('properties')}
className={cn(
'p-4',
'opacity-50',
tab === 'properties' &&
'border-b border-foreground text-foreground opacity-100'
'border-b border-foreground text-foreground opacity-100',
)}
>
Properties
@@ -81,15 +83,12 @@ const ProfileMetrics = ({ data, profile }: Props) => {
<ListPropertiesIcon {...profile.properties} />
</>
)}
{tab === 'properties' && (
<>
{Object.entries(profile.properties)
.filter(([key, value]) => value !== undefined)
.map(([key, value]) => (
<Info key={key} title={key} value={value as string} />
))}
</>
)}
{tab === 'properties' &&
Object.entries(profile.properties)
.filter(([key, value]) => value !== undefined)
.map(([key, value]) => (
<Info key={key} title={key} value={value as string} />
))}
</div>
</div>
<Card

View File

@@ -26,11 +26,11 @@ export default function Page({
<Padding>
<div className="mb-4">
<PageTabs>
<PageTabsLink href={`?tab=profiles`} isActive={tab === 'profiles'}>
<PageTabsLink href={'?tab=profiles'} isActive={tab === 'profiles'}>
Profiles
</PageTabsLink>
<PageTabsLink
href={`?tab=power-users`}
href={'?tab=power-users'}
isActive={tab === 'power-users'}
>
Power users

View File

@@ -12,7 +12,7 @@ type Props = {
const Events = ({ projectId }: Props) => {
const [cursor, setCursor] = useQueryState(
'cursor',
parseAsInteger.withDefault(0)
parseAsInteger.withDefault(0),
);
const query = api.profile.powerUsers.useQuery(
{
@@ -23,7 +23,7 @@ const Events = ({ projectId }: Props) => {
},
{
keepPreviousData: true,
}
},
);
return (

View File

@@ -7,7 +7,7 @@ import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
import { cn } from '@/utils/cn';
import { escape } from 'sqlstring';
import { chQuery, TABLE_NAMES } from '@openpanel/db';
import { TABLE_NAMES, chQuery } from '@openpanel/db';
interface Props {
projectId: string;
@@ -21,7 +21,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
// Days since last event from users
// group by days
const res = await chQuery<Row>(
`SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM ${TABLE_NAMES.events} where project_id = ${escape(projectId)} group by days order by days ASC LIMIT 51`
`SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM ${TABLE_NAMES.events} where project_id = ${escape(projectId)} group by days order by days ASC LIMIT 51`,
);
const maxValue = Math.max(...res.map((x) => x.count));
@@ -29,7 +29,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
const calculateRatio = (currentValue: number) =>
Math.max(
0.1,
Math.min(1, (currentValue - minValue) / (maxValue - minValue))
Math.min(1, (currentValue - minValue) / (maxValue - minValue)),
);
const renderItem = (item: Row) => (
@@ -41,7 +41,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
style={{
opacity: calculateRatio(item.count),
}}
></div>
/>
</TooltipTrigger>
<TooltipContent>
{item.count} profiles last seen{' '}

View File

@@ -15,7 +15,7 @@ type Props = {
const Events = ({ projectId }: Props) => {
const [cursor, setCursor] = useQueryState(
'cursor',
parseAsInteger.withDefault(0)
parseAsInteger.withDefault(0),
);
const [search, setSearch] = useQueryState('search', {
defaultValue: '',
@@ -31,7 +31,7 @@ const Events = ({ projectId }: Props) => {
},
{
keepPreviousData: true,
}
},
);
return (

View File

@@ -5,7 +5,7 @@ export type Coordinate = {
export function haversineDistance(
coord1: Coordinate,
coord2: Coordinate
coord2: Coordinate,
): number {
const R = 6371; // Earth's radius in kilometers
const lat1Rad = coord1.lat * (Math.PI / 180);
@@ -25,7 +25,7 @@ export function haversineDistance(
}
export function findFarthestPoints(
coordinates: Coordinate[]
coordinates: Coordinate[],
): [Coordinate, Coordinate] {
if (coordinates.length < 2) {
throw new Error('At least two coordinates are required');
@@ -80,12 +80,12 @@ function cross(o: Coordinate, a: Coordinate, b: Coordinate): number {
// convex hull
export function getOuterMarkers(coordinates: Coordinate[]): Coordinate[] {
coordinates = coordinates.sort(sortCoordinates);
const sorted = coordinates.sort(sortCoordinates);
if (coordinates.length <= 3) return coordinates;
if (sorted.length <= 3) return sorted;
const lower: Coordinate[] = [];
for (const coord of coordinates) {
for (const coord of sorted) {
while (
lower.length >= 2 &&
cross(lower[lower.length - 2]!, lower[lower.length - 1]!, coord) <= 0
@@ -99,15 +99,11 @@ export function getOuterMarkers(coordinates: Coordinate[]): Coordinate[] {
for (let i = coordinates.length - 1; i >= 0; i--) {
while (
upper.length >= 2 &&
cross(
upper[upper.length - 2]!,
upper[upper.length - 1]!,
coordinates[i]!
) <= 0
cross(upper[upper.length - 2]!, upper[upper.length - 1]!, sorted[i]!) <= 0
) {
upper.pop();
}
upper.push(coordinates[i]!);
upper.push(sorted[i]!);
}
upper.pop();
@@ -148,22 +144,22 @@ export function calculateCentroid(polygon: Coordinate[]): Coordinate {
}
export function calculateGeographicMidpoint(
coordinate: Coordinate[]
coordinate: Coordinate[],
): Coordinate {
let minLat = Infinity,
maxLat = -Infinity,
minLong = Infinity,
maxLong = -Infinity;
let minLat = Number.POSITIVE_INFINITY;
let maxLat = Number.NEGATIVE_INFINITY;
let minLong = Number.POSITIVE_INFINITY;
let maxLong = Number.NEGATIVE_INFINITY;
coordinate.forEach(({ lat, long }) => {
for (const { lat, long } of coordinate) {
if (lat < minLat) minLat = lat;
if (lat > maxLat) maxLat = lat;
if (long < minLong) minLong = long;
if (long > maxLong) maxLong = long;
});
}
// Handling the wrap around the international date line
let midLong;
let midLong: number;
if (maxLong > minLong) {
midLong = (maxLong + minLong) / 2;
} else {
@@ -211,7 +207,7 @@ export function clusterCoordinates(coordinates: Coordinate[], radius = 25) {
long: center.long + cur.long / cluster.members.length,
};
},
{ lat: 0, long: 0 }
{ lat: 0, long: 0 },
);
clusters.push(cluster);

View File

@@ -1,7 +1,7 @@
import { subMinutes } from 'date-fns';
import { escape } from 'sqlstring';
import { chQuery, formatClickhouseDate, TABLE_NAMES } from '@openpanel/db';
import { TABLE_NAMES, chQuery, formatClickhouseDate } from '@openpanel/db';
import type { Coordinate } from './coordinates';
import Map from './map';
@@ -11,7 +11,7 @@ type Props = {
};
const RealtimeMap = async ({ projectId }: Props) => {
const res = await chQuery<Coordinate>(
`SELECT DISTINCT city, longitude as long, latitude as lat FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND created_at >= '${formatClickhouseDate(subMinutes(new Date(), 30))}' ORDER BY created_at DESC`
`SELECT DISTINCT city, longitude as long, latitude as lat FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND created_at >= '${formatClickhouseDate(subMinutes(new Date(), 30))}' ORDER BY created_at DESC`,
);
return <Map markers={res} />;

View File

@@ -49,7 +49,7 @@ export const getBoundingBox = (coordinates: Coordinate[]) => {
export const determineZoom = (
bbox: ReturnType<typeof getBoundingBox>,
aspectRatio = 1.0
aspectRatio = 1.0,
): number => {
const latDiff = bbox.maxLat - bbox.minLat;
const longDiff = bbox.maxLong - bbox.minLong;

View File

@@ -1,11 +1,11 @@
'use client';
import { Fragment, useEffect, useRef, useState } from 'react';
import { useFullscreen } from '@/components/fullscreen-toggle';
import { Tooltiper } from '@/components/ui/tooltip';
import { cn } from '@/utils/cn';
import { bind } from 'bind-event-listener';
import { useTheme } from 'next-themes';
import { Fragment, useEffect, useRef, useState } from 'react';
import {
ComposableMap,
Geographies,
@@ -22,8 +22,8 @@ import {
} from './coordinates';
import {
CustomZoomableGroup,
determineZoom,
GEO_MAP_URL,
determineZoom,
getBoundingBox,
useAnimatedState,
} from './map.helpers';
@@ -37,7 +37,7 @@ const Map = ({ markers }: Props) => {
const showCenterMarker = false;
const ref = useRef<HTMLDivElement>(null);
const [size, setSize] = useState<{ width: number; height: number } | null>(
null
null,
);
// const { markers, toggle } = useActiveMarkers(_m);
@@ -50,7 +50,7 @@ const Map = ({ markers }: Props) => {
const [zoom] = useAnimatedState(
markers.length === 1
? 20
: determineZoom(boundingBox, size ? size?.height / size?.width : 1)
: determineZoom(boundingBox, size ? size?.height / size?.width : 1),
);
const [long] = useAnimatedState(center.long);
@@ -99,7 +99,7 @@ const Map = ({ markers }: Props) => {
<div
className={cn(
'fixed bottom-0 left-0 right-0 top-0',
!isFullscreen && 'lg:left-72'
!isFullscreen && 'lg:left-72',
)}
ref={ref}
>
@@ -142,7 +142,7 @@ const Map = ({ markers }: Props) => {
)}
{clusterCoordinates(markers).map((marker) => {
const size = adjustSizeBasedOnZoom(
calculateMarkerSize(marker.count)
calculateMarkerSize(marker.count),
);
const coordinates: [number, number] = [
marker.center.long,

View File

@@ -11,7 +11,7 @@ const useActiveMarkers = (initialMarkers: Coordinate[]) => {
// Cut the array in half randomly to simulate changes in active markers
const selected = shuffled.slice(
0,
Math.floor(Math.random() * shuffled.length) + 1
Math.floor(Math.random() * shuffled.length) + 1,
);
setActiveMarkers(selected);
}, [activeMarkers]);

View File

@@ -1,10 +1,10 @@
import { Suspense } from 'react';
import {
Fullscreen,
FullscreenClose,
FullscreenOpen,
} from '@/components/fullscreen-toggle';
import { ReportChart } from '@/components/report-chart';
import { Suspense } from 'react';
import RealtimeMap from './map';
import RealtimeLiveEventsServer from './realtime-live-events';

View File

@@ -1,6 +1,6 @@
import { escape } from 'sqlstring';
import { getEvents, TABLE_NAMES } from '@openpanel/db';
import { TABLE_NAMES, getEvents } from '@openpanel/db';
import LiveEvents from './live-events';
@@ -13,7 +13,7 @@ const RealtimeLiveEventsServer = async ({ projectId, limit = 30 }: Props) => {
`SELECT * FROM ${TABLE_NAMES.events} WHERE created_at > now() - INTERVAL 2 HOUR AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT ${limit}`,
{
profile: true,
}
},
);
return <LiveEvents events={events} projectId={projectId} limit={limit} />;
};

View File

@@ -1,9 +1,9 @@
'use client';
import { useState } from 'react';
import { EventListItem } from '@/components/events/event-list-item';
import useWS from '@/hooks/useWS';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db';
@@ -19,7 +19,7 @@ const RealtimeLiveEvents = ({ events, projectId, limit }: Props) => {
`/live/events/${projectId}`,
(event) => {
setState((p) => [event, ...p].slice(0, limit));
}
},
);
return (
<AnimatePresence mode="popLayout" initial={false}>

View File

@@ -71,18 +71,16 @@ export function RealtimeLiveHistogram({
const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0;
if (res.isInitialLoading || countRes.isInitialLoading || liveCount === 0) {
// prettier-ignore
const staticArray = [
10, 25, 30, 45, 20, 5, 55, 18, 40, 12,
50, 35, 8, 22, 38, 42, 15, 28, 52, 5,
48, 14, 32, 58, 7, 19, 33, 56, 24, 5
10, 25, 30, 45, 20, 5, 55, 18, 40, 12, 50, 35, 8, 22, 38, 42, 15, 28, 52,
5, 48, 14, 32, 58, 7, 19, 33, 56, 24, 5,
];
return (
<Wrapper count={0}>
{staticArray.map((percent, i) => (
<div
key={i}
key={i as number}
className="flex-1 animate-pulse rounded bg-def-200"
style={{ height: `${percent}%` }}
/>
@@ -104,7 +102,7 @@ export function RealtimeLiveHistogram({
<div
className={cn(
'flex-1 rounded transition-all ease-in-out hover:scale-110',
minute.count === 0 ? 'bg-def-200' : 'bg-highlight'
minute.count === 0 ? 'bg-def-200' : 'bg-highlight',
)}
style={{
height:

View File

@@ -27,7 +27,7 @@ const RealtimeReloader = ({ projectId }: Props) => {
maxWait: 15000,
delay: 15000,
},
}
},
);
return null;

View File

@@ -1,6 +1,5 @@
'use client';
import { useEffect } from 'react';
import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header';
import { ReportChart } from '@/components/report-chart';
import { ReportChartType } from '@/components/report/ReportChartType';
@@ -25,6 +24,7 @@ import { useDispatch, useSelector } from '@/redux';
import { bind } from 'bind-event-listener';
import { endOfDay, startOfDay } from 'date-fns';
import { GanttChartSquareIcon } from 'lucide-react';
import { useEffect } from 'react';
import type { IServiceReport } from '@openpanel/db';

View File

@@ -55,12 +55,12 @@ const Chart = ({ data }: Props) => {
offset="0%"
stopColor={getChartColor(0)}
stopOpacity={0.8}
></stop>
/>
<stop
offset="100%"
stopColor={getChartColor(0)}
stopOpacity={0.1}
></stop>
/>
</linearGradient>
</defs>
@@ -70,7 +70,7 @@ const Chart = ({ data }: Props) => {
dataKey="users"
stroke={getChartColor(0)}
strokeWidth={2}
fill={`url(#bg)`}
fill={'url(#bg)'}
isAnimationActive={false}
/>
<XAxis

View File

@@ -72,36 +72,36 @@ const Chart = ({ data }: Props) => {
offset="0%"
stopColor={getChartColor(0)}
stopOpacity={0.8}
></stop>
/>
<stop
offset="100%"
stopColor={getChartColor(0)}
stopOpacity={0.1}
></stop>
/>
</linearGradient>
<linearGradient id="wau" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(1)}
stopOpacity={0.8}
></stop>
/>
<stop
offset="100%"
stopColor={getChartColor(1)}
stopOpacity={0.1}
></stop>
/>
</linearGradient>
<linearGradient id="mau" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={getChartColor(2)}
stopOpacity={0.8}
></stop>
/>
<stop
offset="100%"
stopColor={getChartColor(2)}
stopOpacity={0.1}
></stop>
/>
</linearGradient>
</defs>
@@ -111,21 +111,21 @@ const Chart = ({ data }: Props) => {
dataKey="dau"
stroke={getChartColor(0)}
strokeWidth={2}
fill={`url(#dau)`}
fill={'url(#dau)'}
isAnimationActive={false}
/>
<Area
dataKey="wau"
stroke={getChartColor(1)}
strokeWidth={2}
fill={`url(#wau)`}
fill={'url(#wau)'}
isAnimationActive={false}
/>
<Area
dataKey="mau"
stroke={getChartColor(2)}
strokeWidth={2}
fill={`url(#mau)`}
fill={'url(#mau)'}
isAnimationActive={false}
/>
<XAxis {...xAxisProps} dataKey="date" />

View File

@@ -70,12 +70,12 @@ const Chart = ({ data }: Props) => {
offset="0%"
stopColor={getChartColor(0)}
stopOpacity={0.8}
></stop>
/>
<stop
offset="100%"
stopColor={getChartColor(0)}
stopOpacity={0.1}
></stop>
/>
</linearGradient>
</defs>
@@ -85,7 +85,7 @@ const Chart = ({ data }: Props) => {
dataKey="retention"
stroke={getChartColor(0)}
strokeWidth={2}
fill={`url(#bg)`}
fill={'url(#bg)'}
isAnimationActive={false}
/>
<XAxis

View File

@@ -17,7 +17,7 @@ const Cell = ({ value, ratio }: { value: number; ratio: number }) => {
<div
className="absolute inset-0 z-0 bg-highlight"
style={{ opacity: ratio }}
></div>
/>
<div className="relative z-10">{value}</div>
</td>
);
@@ -39,7 +39,7 @@ const WeeklyCohortsServer = async ({ projectId }: Props) => {
row.period_7,
row.period_8,
row.period_9,
])
]),
);
const calculateRatio = (currentValue: number) =>
@@ -47,7 +47,7 @@ const WeeklyCohortsServer = async ({ projectId }: Props) => {
? 0
: Math.max(
0.1,
Math.min(1, (currentValue - minValue) / (maxValue - minValue))
Math.min(1, (currentValue - minValue) / (maxValue - minValue)),
);
return (

View File

@@ -6,7 +6,6 @@ import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import {
closeSheet,
Sheet,
SheetContent,
SheetDescription,
@@ -14,6 +13,7 @@ import {
SheetHeader,
SheetTitle,
SheetTrigger,
closeSheet,
} from '@/components/ui/sheet';
import { useAppParams } from '@/hooks/useAppParams';
import { api } from '@/trpc/client';

View File

@@ -52,7 +52,7 @@ export default async function Page({
}
const member = organization.members.find(
(member) => member.userId === session.userId
(member) => member.userId === session.userId,
);
const hasAccess = member?.role === 'org:admin';
@@ -69,13 +69,13 @@ export default async function Page({
return (
<Padding>
<PageTabs className="mb-4">
<PageTabsLink href={`?tab=org`} isActive={tab === 'org'}>
<PageTabsLink href={'?tab=org'} isActive={tab === 'org'}>
Organization
</PageTabsLink>
<PageTabsLink href={`?tab=members`} isActive={tab === 'members'}>
<PageTabsLink href={'?tab=members'} isActive={tab === 'members'}>
Members
</PageTabsLink>
<PageTabsLink href={`?tab=invites`} isActive={tab === 'invites'}>
<PageTabsLink href={'?tab=invites'} isActive={tab === 'invites'}>
Invites
</PageTabsLink>
</PageTabs>

View File

@@ -10,9 +10,7 @@ export function Logout() {
<span className="title">Sad part</span>
</WidgetHead>
<WidgetBody>
<p className="mb-4">
Sometimes you need to go. See you next time
</p>
<p className="mb-4">Sometimes you need to go. See you next time</p>
<SignOutButton />
</WidgetBody>
</Widget>

View File

@@ -44,7 +44,7 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) {
<Accordion type="single" collapsible className="-mx-4">
{projects.map((project) => {
const pClients = clients.filter(
(client) => client.projectId === project.id
(client) => client.projectId === project.id,
);
return (
<AccordionItem
@@ -61,7 +61,7 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) {
: 'No clients created yet'}
</span>
</div>
<div className="mx-4"></div>
<div className="mx-4" />
</AccordionTrigger>
<AccordionContent className="px-4">
<ProjectActions {...project} />
@@ -91,6 +91,7 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) {
);
})}
<button
type="button"
onClick={() => {
pushModal('AddClient', {
projectId: project.id,

View File

@@ -1,9 +1,9 @@
'use client';
import { useEffect } from 'react';
import { pushModal, useOnPushModal } from '@/modals';
import { useUser } from '@clerk/nextjs';
import { differenceInDays } from 'date-fns';
import { useEffect } from 'react';
import { useOpenPanel } from '@openpanel/nextjs';
@@ -12,7 +12,7 @@ export default function SideEffects() {
const { user } = useUser();
const accountAgeInDays = differenceInDays(
new Date(),
user?.createdAt || new Date()
user?.createdAt || new Date(),
);
useOnPushModal('Testimonial', (open) => {
if (!open) {

View File

@@ -1,8 +1,8 @@
'use client';
import { useEffect, useState } from 'react';
import { EventListItem } from '@/components/events/event-list-item';
import { AnimatePresence, motion } from 'framer-motion';
import { useEffect, useState } from 'react';
import type { IServiceEventMinimal } from '@openpanel/db';

View File

@@ -11,8 +11,8 @@ const Page = ({ children }: Props) => {
return (
<>
<div className="fixed inset-0 hidden md:grid md:grid-cols-[30vw_1fr] lg:grid-cols-[30vw_1fr]">
<div className=""></div>
<div className="border-l border-border bg-card"></div>
<div className="" />
<div className="border-l border-border bg-card" />
</div>
<div className="relative min-h-screen bg-card md:bg-transparent">
<FullWidthNavbar>

View File

@@ -21,6 +21,7 @@ const ConnectApp = ({ client }: Props) => {
<div className="mt-4 grid gap-4 md:grid-cols-2">
{frameworks.app.map((framework) => (
<button
type="button"
onClick={() =>
pushModal('Instructions', {
framework,

View File

@@ -21,6 +21,7 @@ const ConnectBackend = ({ client }: Props) => {
<div className="mt-4 grid gap-4 md:grid-cols-2">
{frameworks.backend.map((framework) => (
<button
type="button"
onClick={() =>
pushModal('Instructions', {
framework,

View File

@@ -21,6 +21,7 @@ const ConnectWeb = ({ client }: Props) => {
<div className="mt-4 grid gap-4 md:grid-cols-2">
{frameworks.website.map((framework) => (
<button
type="button"
onClick={() =>
pushModal('Instructions', {
framework,

View File

@@ -1,12 +1,12 @@
'use client';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import useWS from '@/hooks/useWS';
import { pushModal } from '@/modals';
import { cn } from '@/utils/cn';
import { timeAgo } from '@/utils/date';
import { CheckCircle2Icon, CheckIcon, Loader2 } from 'lucide-react';
import { useState } from 'react';
import type {
IServiceClient,
@@ -28,7 +28,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
(data) => {
setEvents((prev) => [...prev, data]);
onVerified(true);
}
},
);
const isConnected = events.length > 0;
@@ -70,7 +70,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
'my-6 flex gap-6 rounded-xl p-4 md:p-6',
isConnected
? 'bg-emerald-100 dark:bg-emerald-700'
: 'bg-blue-100 dark:bg-blue-700'
: 'bg-blue-100 dark:bg-blue-700',
)}
>
{renderIcon()}
@@ -87,7 +87,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
</div>
)}
{events.slice(-5).map((event, index) => (
<div key={index} className="flex items-center gap-2 ">
<div key={event.id} className="flex items-center gap-2 ">
<CheckIcon size={14} />{' '}
<span className="font-medium">{event.name}</span>{' '}
<span className="ml-auto text-emerald-800">
@@ -108,6 +108,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
<div className="mt-2 text-sm text-muted-foreground">
You can{' '}
<button
type="button"
className="underline"
onClick={() => {
pushModal('OnboardingTroubleshoot', {

View File

@@ -1,10 +1,10 @@
'use client';
import { useState } from 'react';
import { ButtonContainer } from '@/components/button-container';
import { LinkButton } from '@/components/ui/button';
import { cn } from '@/utils/cn';
import Link from 'next/link';
import { useState } from 'react';
import type { IServiceEvent, IServiceProjectWithClients } from '@openpanel/db';
@@ -84,7 +84,7 @@ const Verify = ({ project, events }: Props) => {
size="lg"
className={cn(
'min-w-28 self-start',
!verified && 'pointer-events-none select-none opacity-20'
!verified && 'pointer-events-none select-none opacity-20',
)}
>
Your dashboard

View File

@@ -2,10 +2,10 @@ import { cookies } from 'next/headers';
import { escape } from 'sqlstring';
import {
TABLE_NAMES,
getCurrentOrganizations,
getEvents,
getProjectWithClients,
TABLE_NAMES,
} from '@openpanel/db';
import OnboardingVerify from './onboarding-verify';
@@ -25,7 +25,7 @@ const Verify = async ({ params: { projectId } }: Props) => {
const [project, events] = await Promise.all([
await getProjectWithClients(projectId),
getEvents(
`SELECT * FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} LIMIT 100`
`SELECT * FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} LIMIT 100`,
),
]);

View File

@@ -1,6 +1,5 @@
'use client';
import { useEffect } from 'react';
import AnimateHeight from '@/components/animate-height';
import { ButtonContainer } from '@/components/button-container';
import { CheckboxItem } from '@/components/forms/checkbox-item';
@@ -19,6 +18,7 @@ import {
SmartphoneIcon,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { Controller, useForm, useWatch } from 'react-hook-form';
import type { z } from 'zod';
@@ -185,7 +185,7 @@ const Tracking = ({
}
return `https://${trimmed}`;
})
.join(',')
.join(','),
);
}}
/>

View File

@@ -1,11 +1,11 @@
'use client';
import { useEffect } from 'react';
import { showConfirm } from '@/modals';
import { api } from '@/trpc/client';
import { useAuth } from '@clerk/nextjs';
import { ChevronLastIcon } from 'lucide-react';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect } from 'react';
const SkipOnboarding = () => {
const router = useRouter();
@@ -20,6 +20,7 @@ const SkipOnboarding = () => {
return (
<button
type="button"
onClick={() => {
if (res.data?.canSkip && res.data?.url) {
router.push(res.data.url);

View File

@@ -39,7 +39,7 @@ function useSteps(path: string) {
];
const matchIndex = steps.findLastIndex((step) =>
path.match(new RegExp(step.match))
path.match(new RegExp(step.match)),
);
return steps.map((step, index) => {
@@ -59,17 +59,17 @@ const Steps = ({ className }: Props) => {
const currentIndex = steps.findIndex((i) => i.status === 'current');
return (
<div className="relative">
<div className="absolute bottom-4 left-4 top-4 w-px bg-def-200"></div>
<div className="absolute bottom-4 left-4 top-4 w-px bg-def-200" />
<div
className="absolute left-4 top-4 w-px bg-highlight"
style={{
height: `calc(${((currentIndex + 1) / steps.length) * 100}% - 3.5rem)`,
}}
></div>
/>
<div
className={cn(
'relative flex gap-4 overflow-hidden md:-ml-3 md:flex-col md:gap-8',
className
className,
)}
>
{steps.map((step, index) => (
@@ -80,32 +80,31 @@ const Steps = ({ className }: Props) => {
'rounded-xl border border-border bg-card',
step.status === 'completed' &&
index !== currentIndex - 1 &&
'max-md:hidden'
'max-md:hidden',
)}
key={step.name}
>
<div
className={cn(
'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white'
'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white',
)}
>
<div
className={cn(
'absolute inset-0 z-0 rounded-full bg-highlight',
step.status === 'pending' && 'bg-def-400'
step.status === 'pending' && 'bg-def-400',
)}
></div>
/>
{step.status === 'current' && (
<div className="absolute inset-1 z-0 animate-ping-slow rounded-full bg-highlight"></div>
<div className="absolute inset-1 z-0 animate-ping-slow rounded-full bg-highlight" />
)}
<div className="relative">
{step.status === 'completed' && <CheckCheckIcon size={14} />}
{/* {step.status === 'current' && (
<ArrowRightCircleIcon size={14} />
)} */}
{(step.status === 'pending' || step.status === 'current') && (
<>{index + 1}</>
)}
{(step.status === 'pending' || step.status === 'current') &&
index + 1}
</div>
</div>

View File

@@ -15,7 +15,7 @@ export async function POST(request: Request) {
if (!email) {
return Response.json(
{ message: 'No email address found' },
{ status: 400 }
{ status: 400 },
);
}

View File

@@ -4,7 +4,7 @@ import NextTopLoader from 'nextjs-toploader';
import Providers from './providers';
import '@/styles/globals.css';
import '/node_modules/flag-icons/css/flag-icons.min.css';
import 'flag-icons/css/flag-icons.min.css';
import { GeistMono } from 'geist/font/mono';
import { GeistSans } from 'geist/font/sans';
@@ -31,7 +31,7 @@ export default function RootLayout({
className={cn(
'grainy min-h-screen bg-def-100 font-sans text-base antialiased',
GeistSans.variable,
GeistMono.variable
GeistMono.variable,
)}
>
<NextTopLoader

View File

@@ -1,6 +1,5 @@
'use client';
import React, { useRef, useState } from 'react';
import { TooltipProvider } from '@/components/ui/tooltip';
import { ModalProvider } from '@/modals';
import type { AppStore } from '@/redux';
@@ -10,6 +9,8 @@ import { ClerkProvider, useAuth } from '@clerk/nextjs';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpLink } from '@trpc/client';
import { ThemeProvider } from 'next-themes';
import type React from 'react';
import { useRef, useState } from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import { Toaster } from 'sonner';
import superjson from 'superjson';
@@ -28,7 +29,7 @@ function AllProviders({ children }: { children: React.ReactNode }) {
refetchOnWindowFocus: false,
},
},
})
}),
);
const [trpcClient] = useState(() =>
api.createClient({
@@ -47,7 +48,7 @@ function AllProviders({ children }: { children: React.ReactNode }) {
},
}),
],
})
}),
);
const storeRef = useRef<AppStore>();