Replaces every em-dash and en-dash with regular ASCII hyphens across comments, JSX strings, and dev-facing logs. Mostly cosmetic but stops the inconsistent mix that crept in over the last few months (some files used em-dashes in comments, others didn't, some used both). Bundles two small dashboard-layout tweaks that touch a couple of already-modified files: - (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6 pb-6 so page content sits closer to the topbar. - Sidebar now receives the ports list it needs for the footer port switcher. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
'use client';
|
|
|
|
import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
|
|
|
import { CardSkeleton } from '@/components/shared/loading-skeleton';
|
|
import { EmptyState } from '@/components/shared/empty-state';
|
|
import { ChartCard } from './chart-card';
|
|
import { useLeadSource } from './use-analytics';
|
|
import type { DateRange } from '@/lib/services/analytics.service';
|
|
|
|
interface Props {
|
|
range: DateRange;
|
|
}
|
|
|
|
const COLORS = [
|
|
'hsl(var(--chart-1))',
|
|
'hsl(var(--chart-2))',
|
|
'hsl(var(--chart-3))',
|
|
'hsl(var(--chart-4))',
|
|
'hsl(var(--chart-5))',
|
|
];
|
|
|
|
const SOURCE_LABELS: Record<string, string> = {
|
|
website: 'Website',
|
|
referral: 'Referral',
|
|
manual: 'Manual',
|
|
social: 'Social',
|
|
unspecified: 'Unspecified',
|
|
};
|
|
|
|
export function LeadSourceChart({ range }: Props) {
|
|
const { data, isLoading } = useLeadSource(range);
|
|
const slices = data?.slices ?? [];
|
|
|
|
function toCsv(): string | null {
|
|
if (!slices.length) return null;
|
|
const header = 'source,count';
|
|
const rows = slices.map((s) => `${s.source},${s.count}`);
|
|
return [header, ...rows].join('\n');
|
|
}
|
|
|
|
const chartData = slices.map((s) => ({
|
|
name: SOURCE_LABELS[s.source] ?? s.source,
|
|
value: s.count,
|
|
}));
|
|
|
|
return (
|
|
<ChartCard
|
|
title="Lead Source Attribution"
|
|
description="Where new interests came from"
|
|
exportFilename={`lead-source-${range}`}
|
|
toCsv={toCsv}
|
|
>
|
|
{isLoading ? (
|
|
<CardSkeleton />
|
|
) : !slices.length ? (
|
|
<EmptyState
|
|
title="No interests in range"
|
|
description="Lights up once new interests are created - tracks where each came from (website, referral, broker)."
|
|
/>
|
|
) : (
|
|
// Percentage radii + center-anchored chart so the pie scales with
|
|
// the container instead of being clipped to a constant 90px ring at
|
|
// narrow widths. Legend is reserved a fixed footer height.
|
|
<ResponsiveContainer width="100%" height={280}>
|
|
<PieChart>
|
|
<Pie
|
|
data={chartData}
|
|
dataKey="value"
|
|
nameKey="name"
|
|
cx="50%"
|
|
cy="45%"
|
|
outerRadius="70%"
|
|
innerRadius="40%"
|
|
paddingAngle={2}
|
|
>
|
|
{chartData.map((_, i) => (
|
|
<Cell key={i} fill={COLORS[i % COLORS.length]} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip
|
|
contentStyle={{
|
|
background: 'hsl(var(--popover))',
|
|
border: '1px solid hsl(var(--border))',
|
|
borderRadius: '6px',
|
|
fontSize: 12,
|
|
}}
|
|
/>
|
|
<Legend
|
|
verticalAlign="bottom"
|
|
height={40}
|
|
wrapperStyle={{ fontSize: 12, paddingTop: 4 }}
|
|
/>
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</ChartCard>
|
|
);
|
|
}
|