Skip to content

Commit

Permalink
clients/home: more analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
emilwidlund committed Jan 28, 2025
1 parent 1dfa827 commit 4a11b4a
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,72 +12,157 @@ import { ActivityWidget } from '@/components/Widgets/ActivityWidget'
import { OrdersWidget } from '@/components/Widgets/OrdersWidget'
import { RevenueWidget } from '@/components/Widgets/RevenueWidget'
import { SubscribersWidget } from '@/components/Widgets/SubscribersWidget'
import { useMetrics, useProducts } from '@/hooks/queries'
import { ParsedMetricPeriod, useMetrics, useProducts } from '@/hooks/queries'
import { MaintainerOrganizationContext } from '@/providers/maintainerOrganization'
import { defaultMetricMarks } from '@/utils/metrics'
import { ChevronRight } from '@mui/icons-material'
import { Organization } from '@polar-sh/api'
import { Metric, Metrics, MetricType, Organization } from '@polar-sh/api'
import Button from '@polar-sh/ui/components/atoms/Button'
import FormattedDateTime from '@polar-sh/ui/components/atoms/FormattedDateTime'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@polar-sh/ui/components/atoms/Select'
import ShadowBox from '@polar-sh/ui/components/atoms/ShadowBox'
import { getCentsInDollarString } from '@polar-sh/ui/lib/money'
import Link from 'next/link'
import React, { useContext } from 'react'
import React, { useCallback, useContext, useMemo } from 'react'

interface OverviewPageProps {
const metricDisplayNames: Record<keyof Metrics, string> = {
revenue: 'Revenue',
orders: 'Orders',
cumulative_revenue: 'Cumulative Revenue',
average_order_value: 'Average Order Value',
one_time_products: 'One-Time Products',
one_time_products_revenue: 'One-Time Products Revenue',
new_subscriptions: 'New Subscriptions',
new_subscriptions_revenue: 'New Subscriptions Revenue',
renewed_subscriptions: 'Renewed Subscriptions',
renewed_subscriptions_revenue: 'Renewed Subscriptions Revenue',
active_subscriptions: 'Active Subscriptions',
monthly_recurring_revenue: 'Monthly Recurring Revenue',
}

interface HeroChartProps {
organization: Organization
startDate: Date
endDate: Date
}

export default function OverviewPage({ organization }: OverviewPageProps) {
const { data: products } = useProducts(organization.id)
const HeroChart = ({ organization }: HeroChartProps) => {
const [selectedMetric, setSelectedMetric] =
React.useState<keyof Metrics>('cumulative_revenue')
const [hoveredMetricPeriod, setHoveredMetricPeriod] =
React.useState<ParsedMetricPeriod | null>(null)

const { data: metricsData, isLoading: metricsLoading } = useMetrics({
organizationId: organization.id,
startDate: new Date(new Date().setDate(new Date().getDate() - 14)),
startDate: new Date(new Date().setDate(new Date().getDate() - 31)),
endDate: new Date(),
interval: 'day',
})

const getMetricValue = useCallback((metric?: Metric, value?: number) => {
if (metric?.type === MetricType.CURRENCY) {
return `$${getCentsInDollarString(value ?? 0)}`
} else {
return value
}
}, [])

const currentMetricPeriod = useMemo(() => {
return metricsData?.periods[metricsData.periods.length - 1]
}, [metricsData])

if (metricsLoading) return null

return (
<DashboardBody className="gap-y-16 pb-16">
<ShadowBox className="dark:bg-polar-800 flex flex-col bg-gray-50 p-2">
<div className="flex flex-row justify-between p-6">
<div className="flex flex-col gap-2">
<h2 className="text-2xl">Revenue</h2>
<ShadowBox className="dark:bg-polar-800 flex flex-col bg-gray-50 p-2 shadow-sm">
<div className="flex flex-row justify-between p-6">
<div className="flex flex-col gap-3">
<Select
value={selectedMetric}
onValueChange={(value) => setSelectedMetric(value as keyof Metrics)}
>
<SelectTrigger className="h-fit w-fit border-0 border-none bg-transparent p-0 shadow-none ring-0 hover:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 dark:hover:bg-transparent">
<SelectValue placeholder="Select a metric" />
</SelectTrigger>
<SelectContent className="dark:bg-polar-800 dark:ring-polar-700 ring-1 ring-gray-200">
{Object.entries(metricDisplayNames).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
<h2 className="text-3xl">
{getMetricValue(
metricsData?.metrics[selectedMetric],
currentMetricPeriod?.[selectedMetric],
)}
</h2>
<div className="flex flex-row items-center gap-x-6">
<div className="flex flex-row items-center gap-x-2">
<span className="h-3 w-3 rounded-full border-2 border-blue-500" />
<span className="dark:text-polar-500 text-sm text-gray-500">
Last 14 days
Current Period
</span>
</div>
{hoveredMetricPeriod && (
<div className="flex flex-row items-center gap-x-2">
<span className="h-3 w-3 rounded-full border-2 border-gray-500 dark:border-gray-700" />
<span className="dark:text-polar-500 text-sm text-gray-500">
<FormattedDateTime
datetime={hoveredMetricPeriod.timestamp}
dateStyle="medium"
/>
{' — '}
{getMetricValue(
metricsData?.metrics[selectedMetric],
hoveredMetricPeriod[selectedMetric],
)}
</span>
</div>
)}
</div>
<Link href={`/dashboard/${organization.slug}/analytics`}>
<Button>View Analytics</Button>
</Link>
</div>
<div className="dark:bg-polar-900 flex flex-col gap-y-2 rounded-3xl bg-white p-4">
<Link href={`/dashboard/${organization.slug}/analytics`}>
<Button>View Analytics</Button>
</Link>
</div>
<div className="dark:bg-polar-900 flex flex-col gap-y-2 rounded-3xl bg-white p-4">
{metricsData && (
<MetricChart
height={350}
data={metricsData?.periods ?? []}
data={metricsData.periods}
interval="day"
marks={defaultMetricMarks}
metric={{
slug: 'revenue',
display_name: 'Revenue',
type: 'currency',
}}
metric={metricsData.metrics[selectedMetric]}
onDataIndexHover={(period) =>
setHoveredMetricPeriod(
metricsData.periods[period as number] ?? null,
)
}
/>
</div>
</ShadowBox>
)}
</div>
</ShadowBox>
)
}

interface OverviewPageProps {
organization: Organization
startDate: Date
endDate: Date
}

export default function OverviewPage({ organization }: OverviewPageProps) {
const { data: products } = useProducts(organization.id)

return (
<DashboardBody className="gap-y-16 pb-16">
<HeroChart organization={organization} />
<div className="grid grid-cols-1 gap-6 md:grid-cols-3 md:gap-10">
<ActivityWidget className="col-span-2" />
<OrdersWidget />
Expand Down
2 changes: 1 addition & 1 deletion clients/apps/web/src/components/Widgets/ActivityWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const ActivityWidget = ({ className }: ActivityWidgetProps) => {
<Tooltip key={i} delayDuration={0}>
<TooltipTrigger
className={twMerge(
'dark:bg-polar-600 h-1 w-1 rounded-full bg-gray-300 xl:h-2 xl:w-2',
'dark:bg-polar-700 h-1 w-1 rounded-full bg-gray-300 xl:h-2 xl:w-2',
activeClass,
)}
/>
Expand Down
2 changes: 1 addition & 1 deletion clients/apps/web/src/components/Widgets/RevenueWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const RevenueWidget = ({ className }: RevenueWidgetProps) => {
)}%`,
}}
className={twMerge(
'dark:bg-polar-600 w-3 flex-shrink rounded-full bg-gray-300',
'dark:bg-polar-700 w-3 flex-shrink rounded-full bg-gray-300',
activeClass,
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const SubscribersWidget = ({ className }: SubscribersWidgetProps) => {
)}%`,
}}
className={twMerge(
'dark:bg-polar-600 w-3 flex-shrink rounded-full bg-gray-300',
'dark:bg-polar-700 w-3 flex-shrink rounded-full bg-gray-300',
activeClass,
)}
/>
Expand Down
2 changes: 1 addition & 1 deletion clients/apps/web/src/utils/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export const defaultMetricMarks: MetricMarksResolver = ({
strokeOpacity: 0.5,
}),
),
Plot.dot(data, {
Plot.circle(data, {
x: 'timestamp',
y: metric.slug,
fill: primaryColor,
Expand Down

0 comments on commit 4a11b4a

Please sign in to comment.