Files
beenvoice/src/app/dashboard/_components/revenue-chart.tsx
T
Claude ba14526fc5 Set up proper DB migrations and fix remaining mobile responsive issues
Migrations:
- drizzle.config.ts: add out: './drizzle' so drizzle-kit generate writes
  SQL migration files instead of only supporting push
- drizzle/0000_glossy_magneto.sql: initial migration capturing all 9
  current tables (users, accounts, sessions, verification_tokens,
  sso_providers, clients, businesses, invoices, invoice_items)
- src/server/db/migrate.ts: programmatic runner using drizzle-orm's
  migrate() — tracks applied migrations in __drizzle_migrations,
  safe to run on every deploy
- package.json: db:migrate now runs the programmatic runner instead of
  drizzle-kit migrate (CLI requires devDeps at runtime)
- start.sh: replace drizzle-kit push with bun src/server/db/migrate.ts
- Dockerfile: copy drizzle/ folder into the runner image so migrations
  are available at container startup

Mobile fixes:
- data-table.tsx: pagination buttons grow from 32px to 40px on mobile
  (h-10 w-10 md:h-8 md:w-8) to meet 44px touch-target guidelines
- floating-action-bar.tsx: stack left-content + action buttons to column
  layout on narrow screens (flex-col sm:flex-row), reduce padding on
  mobile (p-3 sm:p-4)
- revenue-chart.tsx: responsive chart height (h-48 md:h-64) so the chart
  doesn't consume too much vertical space on small screens

https://claude.ai/code/session_012sqEgNQpx676isepeoX4Mi
2026-04-05 01:59:08 +00:00

132 lines
3.4 KiB
TypeScript

"use client";
import {
Area,
AreaChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider";
interface RevenueChartProps {
data: {
month: string;
revenue: number;
monthLabel: string;
}[];
}
const CustomTooltip = ({
active,
payload,
label,
}: {
active?: boolean;
payload?: Array<{ payload: { revenue: number } }>;
label?: string;
}) => {
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
if (active && payload?.length) {
const data = payload[0]!.payload;
return (
<div className="bg-card border-border rounded-lg border p-3 shadow-lg">
<p className="font-medium">{label}</p>
<p style={{ color: "hsl(0, 0%, 60%)" }}>
Revenue: {formatCurrency(data.revenue)}
</p>
<p className="text-muted-foreground text-sm">
{/* Count not available in aggregated view currently */}
</p>
</div>
);
}
return null;
};
export function RevenueChart({ data }: RevenueChartProps) {
// Use data directly
const chartData = data;
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
const { prefersReducedMotion, animationSpeedMultiplier } =
useAnimationPreferences();
if (chartData.length === 0) {
return (
<div className="flex h-64 items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground text-sm">
No revenue data available
</p>
<p className="text-muted-foreground text-xs">
Revenue will appear here once you have paid invoices
</p>
</div>
</div>
);
}
return (
<div className="h-48 w-full md:h-64">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<defs>
<linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="hsl(217, 91%, 60%)" stopOpacity={0.4} />
<stop
offset="95%"
stopColor="hsl(217, 91%, 60%)"
stopOpacity={0.05}
/>
</linearGradient>
</defs>
<XAxis
dataKey="monthLabel"
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
tickFormatter={formatCurrency}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="revenue"
stroke="hsl(217, 91%, 60%)"
strokeWidth={2}
fill="url(#revenueGradient)"
isAnimationActive={!prefersReducedMotion}
animationDuration={Math.round(
600 / (animationSpeedMultiplier ?? 1),
)}
animationEasing="ease-out"
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}