a lot
This commit is contained in:
@@ -1,93 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import AnimatedNumbers from 'react-animated-numbers';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { getSafeJson } from '@mixan/common';
|
||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||
|
||||
interface LiveCounterProps {
|
||||
initialCount: number;
|
||||
}
|
||||
|
||||
export function LiveCounter({ initialCount = 0 }: LiveCounterProps) {
|
||||
const client = useQueryClient();
|
||||
const [counter, setCounter] = useState(initialCount);
|
||||
const { projectId } = useAppParams();
|
||||
const [es] = useState(
|
||||
typeof window != 'undefined' &&
|
||||
new EventSource(`http://localhost:3333/live/events/${projectId}`)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!es) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
function handler(event: MessageEvent<string>) {
|
||||
const parsed = getSafeJson<{
|
||||
visitors: number;
|
||||
event: IServiceCreateEventPayload | null;
|
||||
}>(event.data);
|
||||
|
||||
if (parsed) {
|
||||
setCounter(parsed.visitors);
|
||||
if (parsed.event) {
|
||||
client.refetchQueries({
|
||||
type: 'active',
|
||||
});
|
||||
toast('New event', {
|
||||
description: `${parsed.event.name}`,
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
es.addEventListener('message', handler);
|
||||
return () => es.removeEventListener('message', handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="border border-border rounded h-8 px-3 leading-none flex items-center font-medium gap-2">
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'bg-emerald-500 h-3 w-3 rounded-full animate-ping opacity-100 transition-all',
|
||||
counter === 0 && 'bg-destructive opacity-0'
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-emerald-500 h-3 w-3 rounded-full absolute top-0 left-0 transition-all',
|
||||
counter === 0 && 'bg-destructive'
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<AnimatedNumbers
|
||||
includeComma
|
||||
transitions={(index) => ({
|
||||
type: 'spring',
|
||||
duration: index + 0.3,
|
||||
})}
|
||||
animateToNumber={counter}
|
||||
locale="en"
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{counter} unique visitors last 5 minutes
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { OverviewFilters } from '@/components/overview/overview-filters';
|
||||
import { OverviewFiltersButtons } from '@/components/overview/overview-filters-buttons';
|
||||
import OverviewTopDevices from '@/components/overview/overview-top-devices';
|
||||
import OverviewTopEvents from '@/components/overview/overview-top-events';
|
||||
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||
import { WidgetHead } from '@/components/overview/overview-widget';
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { ChartLoading } from '@/components/report/chart/ChartLoading';
|
||||
import { MetricCardLoading } from '@/components/report/chart/MetricCard';
|
||||
import { ReportRange } from '@/components/report/ReportRange';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Widget, WidgetBody } from '@/components/Widget';
|
||||
import type { IChartInput } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Eye, FilterIcon, Globe2Icon, LockIcon, X } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { StickyBelowHeader } from './layout-sticky-below-header';
|
||||
import { LiveCounter } from './live-counter';
|
||||
interface OverviewMetricsProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export default function OverviewMetrics() {
|
||||
const { previous, range, setRange, interval, metric, setMetric, filters } =
|
||||
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
const { previous, range, interval, metric, setMetric, filters } =
|
||||
useOverviewOptions();
|
||||
|
||||
const reports = [
|
||||
{
|
||||
id: 'Unique visitors',
|
||||
projectId: '', // TODO: Remove
|
||||
projectId,
|
||||
events: [
|
||||
{
|
||||
segment: 'user',
|
||||
@@ -60,7 +42,7 @@ export default function OverviewMetrics() {
|
||||
},
|
||||
{
|
||||
id: 'Total sessions',
|
||||
projectId: '', // TODO: Remove
|
||||
projectId,
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
@@ -81,7 +63,7 @@ export default function OverviewMetrics() {
|
||||
},
|
||||
{
|
||||
id: 'Total pageviews',
|
||||
projectId: '', // TODO: Remove
|
||||
projectId,
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
@@ -102,7 +84,7 @@ export default function OverviewMetrics() {
|
||||
},
|
||||
{
|
||||
id: 'Views per session',
|
||||
projectId: '', // TODO: Remove
|
||||
projectId,
|
||||
events: [
|
||||
{
|
||||
segment: 'user_average',
|
||||
@@ -123,7 +105,7 @@ export default function OverviewMetrics() {
|
||||
},
|
||||
{
|
||||
id: 'Bounce rate',
|
||||
projectId: '', // TODO: Remove
|
||||
projectId,
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
@@ -161,7 +143,7 @@ export default function OverviewMetrics() {
|
||||
},
|
||||
{
|
||||
id: 'Visit duration',
|
||||
projectId: '', // TODO: Remove
|
||||
projectId,
|
||||
events: [
|
||||
{
|
||||
segment: 'property_average',
|
||||
@@ -196,90 +178,37 @@ export default function OverviewMetrics() {
|
||||
const selectedMetric = reports[metric]!;
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<StickyBelowHeader className="p-4 flex gap-2 justify-between">
|
||||
<div className="flex gap-2">
|
||||
<ReportRange value={range} onChange={(value) => setRange(value)} />
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" responsive icon={FilterIcon}>
|
||||
Filters
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<LiveCounter initialCount={0} />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button icon={Globe2Icon} responsive>
|
||||
Public
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_DASHBOARD_URL}/share/project/4e2798cb-e255-4e9d-960d-c9ad095aabd7`}
|
||||
>
|
||||
<Eye size={16} className="mr-2" />
|
||||
View
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={(event) => {}}>
|
||||
<LockIcon size={16} className="mr-2" />
|
||||
Make private
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</StickyBelowHeader>
|
||||
|
||||
<div className="p-4 flex gap-2 flex-wrap">
|
||||
<OverviewFiltersButtons />
|
||||
</div>
|
||||
<div className="p-4 grid gap-4 grid-cols-6">
|
||||
{reports.map((report, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="relative col-span-6 md:col-span-3 lg:col-span-2 group"
|
||||
onClick={() => {
|
||||
setMetric(index);
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<MetricCardLoading />}>
|
||||
<Chart hideID {...report} />
|
||||
</Suspense>
|
||||
{/* add active border */}
|
||||
<div
|
||||
className={cn(
|
||||
'transition-opacity top-0 left-0 right-0 bottom-0 absolute rounded-md w-full h-full border ring-1 border-chart-0 ring-chart-0',
|
||||
metric === index ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
<Widget className="col-span-6">
|
||||
<WidgetHead>
|
||||
<div className="title">{selectedMetric.events[0]?.displayName}</div>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<Chart hideID {...selectedMetric} chartType="linear" />
|
||||
</Suspense>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
<OverviewTopSources />
|
||||
<OverviewTopPages />
|
||||
<OverviewTopDevices />
|
||||
<OverviewTopEvents />
|
||||
<div className="col-span-6">
|
||||
<OverviewTopGeo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SheetContent className="!max-w-lg w-full" side="right">
|
||||
<OverviewFilters />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<>
|
||||
{reports.map((report, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="relative col-span-6 md:col-span-3 lg:col-span-2 group"
|
||||
onClick={() => {
|
||||
setMetric(index);
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<MetricCardLoading />}>
|
||||
<Chart hideID {...report} />
|
||||
</Suspense>
|
||||
<div
|
||||
className={cn(
|
||||
'transition-opacity top-0 left-0 right-0 bottom-0 absolute rounded-md w-full h-full border ring-1 border-chart-0 ring-chart-0',
|
||||
metric === index ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{/* add active border */}
|
||||
</button>
|
||||
))}
|
||||
<Widget className="col-span-6">
|
||||
<WidgetHead>
|
||||
<div className="title">{selectedMetric.events[0]?.displayName}</div>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<Chart hideID {...selectedMetric} chartType="linear" />
|
||||
</Suspense>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||
import { ReportRange } from '@/components/report/ReportRange';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SheetTrigger } from '@/components/ui/sheet';
|
||||
import { FilterIcon } from 'lucide-react';
|
||||
|
||||
export function OverviewReportRange() {
|
||||
const { previous, range, setRange, interval, metric, setMetric, filters } =
|
||||
useOverviewOptions();
|
||||
|
||||
return <ReportRange value={range} onChange={(value) => setRange(value)} />;
|
||||
}
|
||||
|
||||
export function OverviewFilterSheetTrigger() {
|
||||
const { previous, range, setRange, interval, metric, setMetric, filters } =
|
||||
useOverviewOptions();
|
||||
|
||||
return (
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" responsive icon={FilterIcon}>
|
||||
Filters
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,24 @@
|
||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||
import ServerLiveCounter from '@/components/overview/live-counter';
|
||||
import { OverviewFilters } from '@/components/overview/overview-filters';
|
||||
import { OverviewFiltersButtons } from '@/components/overview/overview-filters-buttons';
|
||||
import { OverviewShare } from '@/components/overview/overview-share';
|
||||
import OverviewTopDevices from '@/components/overview/overview-top-devices';
|
||||
import OverviewTopEvents from '@/components/overview/overview-top-events';
|
||||
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||
import { Sheet, SheetContent } from '@/components/ui/sheet';
|
||||
import { getExists } from '@/server/pageExists';
|
||||
|
||||
import { db } from '@mixan/db';
|
||||
|
||||
import { StickyBelowHeader } from './layout-sticky-below-header';
|
||||
import OverviewMetrics from './overview-metrics';
|
||||
import {
|
||||
OverviewFilterSheetTrigger,
|
||||
OverviewReportRange,
|
||||
} from './overview-sticky-header';
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
@@ -9,14 +26,49 @@ interface PageProps {
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params: { organizationId, projectId },
|
||||
}: PageProps) {
|
||||
await getExists(organizationId, projectId);
|
||||
const [share] = await Promise.all([
|
||||
db.shareOverview.findUnique({
|
||||
where: {
|
||||
project_id: projectId,
|
||||
},
|
||||
}),
|
||||
getExists(organizationId, projectId),
|
||||
]);
|
||||
|
||||
return (
|
||||
<PageLayout title="Overview" organizationSlug={organizationId}>
|
||||
<OverviewMetrics />
|
||||
<Sheet>
|
||||
<StickyBelowHeader className="p-4 flex gap-2 justify-between">
|
||||
<div className="flex gap-2">
|
||||
<OverviewReportRange />
|
||||
<OverviewFilterSheetTrigger />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ServerLiveCounter projectId={projectId} />
|
||||
<OverviewShare data={share} />
|
||||
</div>
|
||||
</StickyBelowHeader>
|
||||
<div className="p-4 grid gap-4 grid-cols-6">
|
||||
<div className="col-span-6 flex flex-wrap gap-2">
|
||||
<OverviewFiltersButtons />
|
||||
</div>
|
||||
<OverviewMetrics projectId={projectId} />
|
||||
<OverviewTopSources projectId={projectId} />
|
||||
<OverviewTopPages projectId={projectId} />
|
||||
<OverviewTopDevices projectId={projectId} />
|
||||
<OverviewTopEvents projectId={projectId} />
|
||||
<div className="col-span-6">
|
||||
<OverviewTopGeo projectId={projectId} />
|
||||
</div>
|
||||
</div>
|
||||
<SheetContent className="!max-w-lg w-full" side="right">
|
||||
<OverviewFilters projectId={projectId} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import OverviewMetrics from '@/app/(app)/[organizationId]/[projectId]/overview-metrics';
|
||||
import { Logo } from '@/components/Logo';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { getProjectById } from '@/server/services/project.service';
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params: { organizationId, projectId },
|
||||
}: PageProps) {
|
||||
const project = await getProjectById(projectId);
|
||||
const organization = await getOrganizationBySlug(organizationId);
|
||||
return (
|
||||
<div className="p-4 md:p-16 bg-gradient-to-tl from-blue-950 to-blue-600">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex justify-between items-end mb-4">
|
||||
<div className="leading-none">
|
||||
<span className="text-white mb-4">{organization?.name}</span>
|
||||
<h1 className="text-white text-xl font-medium">{project?.name}</h1>
|
||||
</div>
|
||||
<a href="https://openpanel.dev?utm_source=openpanel.dev&utm_medium=share">
|
||||
<Logo className="text-white" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<OverviewMetrics />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
apps/web/src/app/(public)/share/overview/[id]/page.tsx
Normal file
82
apps/web/src/app/(public)/share/overview/[id]/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
|
||||
import OverviewMetrics from '@/app/(app)/[organizationId]/[projectId]/overview-metrics';
|
||||
import {
|
||||
OverviewFilterSheetTrigger,
|
||||
OverviewReportRange,
|
||||
} from '@/app/(app)/[organizationId]/[projectId]/overview-sticky-header';
|
||||
import { Logo } from '@/components/Logo';
|
||||
import ServerLiveCounter from '@/components/overview/live-counter';
|
||||
import { OverviewFilters } from '@/components/overview/overview-filters';
|
||||
import { OverviewFiltersButtons } from '@/components/overview/overview-filters-buttons';
|
||||
import OverviewTopDevices from '@/components/overview/overview-top-devices';
|
||||
import OverviewTopEvents from '@/components/overview/overview-top-events';
|
||||
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||
import { Sheet, SheetContent } from '@/components/ui/sheet';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getShareOverviewById } from '@mixan/db';
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params: { id } }: PageProps) {
|
||||
const share = await getShareOverviewById(id);
|
||||
if (!share) {
|
||||
return notFound();
|
||||
}
|
||||
const projectId = share.project_id;
|
||||
const organization = await getOrganizationBySlug(share.organization_slug);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-16 bg-gradient-to-tl from-blue-950 to-blue-600">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex justify-between items-end mb-4">
|
||||
<div className="leading-none">
|
||||
<span className="text-white mb-4">{organization?.name}</span>
|
||||
<h1 className="text-white text-xl font-medium">
|
||||
{share.project?.name}
|
||||
</h1>
|
||||
</div>
|
||||
<a href="https://openpanel.dev?utm_source=openpanel.dev&utm_medium=share">
|
||||
<Logo className="text-white" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow ring-8 ring-blue-600/50">
|
||||
<Sheet>
|
||||
<StickyBelowHeader className="p-4 flex gap-2 justify-between">
|
||||
<div className="flex gap-2">
|
||||
<OverviewReportRange />
|
||||
<OverviewFilterSheetTrigger />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ServerLiveCounter projectId={projectId} />
|
||||
</div>
|
||||
</StickyBelowHeader>
|
||||
<div className="p-4 grid gap-4 grid-cols-6">
|
||||
<div className="col-span-6 flex flex-wrap gap-2">
|
||||
<OverviewFiltersButtons />
|
||||
</div>
|
||||
<OverviewMetrics projectId={projectId} />
|
||||
<OverviewTopSources projectId={projectId} />
|
||||
<OverviewTopPages projectId={projectId} />
|
||||
<OverviewTopDevices projectId={projectId} />
|
||||
<OverviewTopEvents projectId={projectId} />
|
||||
<div className="col-span-6">
|
||||
<OverviewTopGeo projectId={projectId} />
|
||||
</div>
|
||||
</div>
|
||||
<SheetContent className="!max-w-lg w-full" side="right">
|
||||
<OverviewFilters projectId={projectId} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ClerkProvider } from '@clerk/nextjs';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { httpLink } from '@trpc/client';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { Toaster } from 'sonner';
|
||||
import superjson from 'superjson';
|
||||
|
||||
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
@@ -49,6 +50,7 @@ export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
{children}
|
||||
<Toaster />
|
||||
<ModalProvider />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
Reference in New Issue
Block a user