mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 09:38:55 -04:00
ba14526fc5
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
132 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|