feat(ui): view-by-view UX redesign and accessibility pass
- Overview: limits hero with progress states, spend pill, primary quick-add, mobile FAB - Logbook: day-grouped entries with subtotals, unified filter bar, Log again, human source labels - Trends: unified chart cards, tick thinning, chart aria summaries, radiogroup forecast control - Settings: sectioned layout with separated danger zone, ThemePicker aria fix - EntryModal: fieldset grouping, Field primitive, safe date validation - AuthView: autoComplete, password visibility toggle - StatHint popover replacing title-only tooltips; mobile stacked import preview - Remaining inline hex styles converted to theme tokens
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Loader2, LogIn } from "lucide-react";
|
||||
import { Eye, EyeOff, Loader2, LogIn } from "lucide-react";
|
||||
import { useState, type CSSProperties, type FormEvent } from "react";
|
||||
import { LegalFootnote } from "../LegalFootnote";
|
||||
import { ShellBackdrop } from "../ShellBackdrop";
|
||||
@@ -31,6 +31,7 @@ export function AuthView({
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
@@ -71,20 +72,47 @@ export function AuthView({
|
||||
{mode === "signup" && (
|
||||
<label className="field-label">
|
||||
Name
|
||||
<input className="field-control" type="text" value={name} onChange={(event) => setName(event.target.value)} placeholder="Ned" />
|
||||
<input className="field-control" type="text" autoComplete="name" value={name} onChange={(event) => setName(event.target.value)} placeholder="Ned" />
|
||||
</label>
|
||||
)}
|
||||
<label className="field-label">
|
||||
Email
|
||||
<input className="field-control" type="email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="you@example.com" required />
|
||||
<input className="field-control" type="email" autoComplete="email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="you@example.com" required />
|
||||
</label>
|
||||
<label className="field-label">
|
||||
Password
|
||||
<input className="field-control" minLength={8} type="password" value={password} onChange={(event) => setPassword(event.target.value)} placeholder="8+ characters" required />
|
||||
<span className="relative block">
|
||||
<input
|
||||
className="field-control pr-12"
|
||||
minLength={8}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete={mode === "signup" ? "new-password" : "current-password"}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="8+ characters"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
className="password-toggle"
|
||||
type="button"
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
aria-pressed={showPassword}
|
||||
onClick={() => setShowPassword((current) => !current)}
|
||||
>
|
||||
{showPassword ? <EyeOff size={17} aria-hidden="true" /> : <Eye size={17} aria-hidden="true" />}
|
||||
</button>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{authError && (
|
||||
<div className="rounded-md px-3 py-2 text-sm" style={{ border: "1px solid #ffc9c2", background: "#fff3f1", color: "#9f1c16" }}>
|
||||
<div
|
||||
className="rounded-md px-3 py-2 text-sm"
|
||||
style={{
|
||||
border: "1px solid color-mix(in srgb, var(--error) 35%, transparent)",
|
||||
background: "var(--error-container)",
|
||||
color: "var(--on-error-container)",
|
||||
}}
|
||||
>
|
||||
{authError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,7 @@ type LogbookViewProps = {
|
||||
onAdd: () => void;
|
||||
onEdit: (entry: RedBullEntry) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onLogAgain?: (entry: RedBullEntry) => void;
|
||||
};
|
||||
|
||||
export function LogbookView({
|
||||
@@ -25,12 +26,21 @@ export function LogbookView({
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onLogAgain,
|
||||
}: LogbookViewProps) {
|
||||
return (
|
||||
<section className="grid gap-4">
|
||||
<div className="logbook-layout grid gap-4">
|
||||
<FiltersPanel filters={filters} flavours={flavours} onChange={onFilterChange} />
|
||||
<EntryLedger entries={entries} totalEntries={totalEntries} deletingId={deletingId} onAdd={onAdd} onEdit={onEdit} onDelete={onDelete} />
|
||||
<EntryLedger
|
||||
entries={entries}
|
||||
totalEntries={totalEntries}
|
||||
deletingId={deletingId}
|
||||
onAdd={onAdd}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onLogAgain={onLogAgain}
|
||||
/>
|
||||
</div>
|
||||
<LegalFootnote />
|
||||
</section>
|
||||
|
||||
@@ -68,6 +68,12 @@ export function OverviewView({
|
||||
userLimits.dailySpendLimit != null
|
||||
? `${currency.format(todaySpendRaw)} of ${currency.format(userLimits.dailySpendLimit)} today`
|
||||
: `${summary.monthSpend} this month`;
|
||||
const chartTotalSpend = chartData.reduce((total, point) => total + point.spend, 0);
|
||||
const chartTotalCans = chartData.reduce((total, point) => total + point.cans, 0);
|
||||
const spendChartLabel = `Spend over the last ${chartData.length} logged days, total ${currency.format(chartTotalSpend)} across ${chartTotalCans} cans`;
|
||||
const flavourChartLabel = flavourData.length
|
||||
? `Cans by flavour across ${flavourData.length} flavours. Most logged: ${flavourData[0].name}`
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
@@ -84,7 +90,7 @@ export function OverviewView({
|
||||
{limitCheck.violations.length ? (
|
||||
<section className="limit-alert">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 shrink-0" size={20} aria-hidden="true" style={{ color: "#b06000" }} />
|
||||
<AlertTriangle className="mt-0.5 shrink-0" size={20} aria-hidden="true" style={{ color: "var(--warning)" }} />
|
||||
<div>
|
||||
<p className="limit-alert-title">Limit alerts</p>
|
||||
<p className="limit-alert-copy mt-1">
|
||||
@@ -111,7 +117,7 @@ export function OverviewView({
|
||||
<section className="overview-charts-grid grid gap-4">
|
||||
<AppCard title="Spend overview" subtitle="Last 30 logged days">
|
||||
{chartData.length ? (
|
||||
<div className="chart-shell chart-shell--area">
|
||||
<div className="chart-shell chart-shell--area" role="img" aria-label={spendChartLabel}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData} margin={{ top: 12, right: 12, bottom: 0, left: -18 }}>
|
||||
<defs>
|
||||
@@ -159,7 +165,7 @@ export function OverviewView({
|
||||
<section className="grid gap-4">
|
||||
<AppCard title="Flavour mix" subtitle="Cans by flavour">
|
||||
{flavourData.length ? (
|
||||
<div className="chart-shell chart-shell--pie">
|
||||
<div className="chart-shell chart-shell--pie" role="img" aria-label={flavourChartLabel}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={flavourData} dataKey="value" nameKey="name" innerRadius={70} outerRadius={104} paddingAngle={4} stroke="#080d1f" strokeWidth={4}>
|
||||
|
||||
@@ -60,8 +60,8 @@ export function SettingsView({
|
||||
}: SettingsViewProps) {
|
||||
return (
|
||||
<div className="grid gap-4 xl:grid-cols-[1fr_0.8fr]">
|
||||
<div className="grid gap-4">
|
||||
<AppCard title="Daily limits" subtitle="Personal caps for cans, spend, and stop time (BST)">
|
||||
<div className="grid content-start gap-4">
|
||||
<AppCard title="Limits" subtitle="Personal caps for cans, spend, and stop time (BST)">
|
||||
<LimitsSettingsForm
|
||||
limits={userLimits}
|
||||
check={limitCheck}
|
||||
@@ -80,10 +80,32 @@ export function SettingsView({
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<AppCard title="Appearance" subtitle={`${activeTheme.label} theme active`}>
|
||||
<AppCard title="Appearance" subtitle={`Pick a theme and colour mode · ${activeTheme.label} active`}>
|
||||
<ThemePicker themeId={themeId} themeMode={themeMode} onChange={onThemeChange} onThemeModeChange={onThemeModeChange} />
|
||||
</AppCard>
|
||||
|
||||
<AppCard title="Account" subtitle="Signed in with Appwrite">
|
||||
<div className="account-card">
|
||||
<div className="account-avatar">{userInitial(user)}</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-lg font-medium text-slate-950">{user?.name || "Appwrite user"}</p>
|
||||
<p className="truncate text-sm text-slate-500">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 rounded-2xl border border-white/10 bg-white/[0.05] p-4">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-700">
|
||||
{dataLoading ? <Loader2 className="animate-spin" size={16} aria-hidden="true" /> : <Cloud size={16} aria-hidden="true" />}
|
||||
{setupStatus.message}
|
||||
</div>
|
||||
</div>
|
||||
<button className="secondary-button mt-4 justify-center" type="button" onClick={onLogout}>
|
||||
<LogOut size={17} aria-hidden="true" />
|
||||
Log out
|
||||
</button>
|
||||
</AppCard>
|
||||
</div>
|
||||
|
||||
<div className="grid content-start gap-4">
|
||||
<AppCard title="Data & sync" subtitle={`${entries.length} entries synced for this user`}>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<MiniMetric label="All-time cans" value={summary.allTimeCans} accent={MATERIAL_ACCENTS.primary} />
|
||||
@@ -91,7 +113,7 @@ export function SettingsView({
|
||||
<MiniMetric label="Favourite" value={summary.favouriteFlavour} accent={MATERIAL_ACCENTS.secondary} />
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="mt-5 grid gap-2 sm:grid-cols-2 xl:grid-cols-2">
|
||||
<button className="secondary-button justify-center" type="button" onClick={() => window.location.reload()} disabled={dataLoading}>
|
||||
{dataLoading ? <Loader2 className="animate-spin" size={17} aria-hidden="true" /> : <RefreshCcw size={17} aria-hidden="true" />}
|
||||
Sync now
|
||||
@@ -123,34 +145,20 @@ export function SettingsView({
|
||||
<DataPair label="Collection" value={appwriteConfig.collectionId} />
|
||||
</dl>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<button className="danger-button mt-5 justify-center" type="button" onClick={onReset} disabled={!entries.length || Boolean(busyAction)}>
|
||||
<section className="app-card danger-zone p-4 sm:p-5" aria-labelledby="danger-zone-title">
|
||||
<h2 id="danger-zone-title" className="app-card-title danger-zone-title text-xl">
|
||||
Danger zone
|
||||
</h2>
|
||||
<p className="app-card-subtitle mt-1">
|
||||
Permanently delete every entry on this account. This cannot be undone.
|
||||
</p>
|
||||
<button className="danger-button mt-4 justify-center" type="button" onClick={onReset} disabled={!entries.length || Boolean(busyAction)}>
|
||||
<RotateCcw size={17} aria-hidden="true" />
|
||||
Delete all entries
|
||||
</button>
|
||||
</AppCard>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<AppCard title="Account" subtitle="Signed in with Appwrite">
|
||||
<div className="account-card">
|
||||
<div className="account-avatar">{userInitial(user)}</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-lg font-medium text-slate-950">{user?.name || "Appwrite user"}</p>
|
||||
<p className="truncate text-sm text-slate-500">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 rounded-2xl border border-white/10 bg-white/[0.05] p-4">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-700">
|
||||
{dataLoading ? <Loader2 className="animate-spin" size={16} aria-hidden="true" /> : <Cloud size={16} aria-hidden="true" />}
|
||||
{setupStatus.message}
|
||||
</div>
|
||||
</div>
|
||||
<button className="secondary-button mt-4 justify-center" type="button" onClick={onLogout}>
|
||||
<LogOut size={17} aria-hidden="true" />
|
||||
Log out
|
||||
</button>
|
||||
</AppCard>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<LegalFootnote className="mt-2" />
|
||||
@@ -162,7 +170,7 @@ function DataPair({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="grid gap-1 sm:grid-cols-[110px_1fr]">
|
||||
<dt className="text-slate-500">{label}</dt>
|
||||
<dd className="truncate font-mono text-xs" style={{ color: "#174ea6" }}>{value}</dd>
|
||||
<dd className="truncate font-mono text-xs" style={{ color: "var(--primary)" }}>{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { currency, oneDecimal, wholeNumber } from "../../lib/metrics";
|
||||
import { AppCard } from "../AppCard";
|
||||
import { ChartTooltip } from "../ChartTooltip";
|
||||
import { EmptyState } from "../EmptyState";
|
||||
@@ -24,6 +25,15 @@ import { SpendForecastCard } from "../SpendForecastCard";
|
||||
import type { Insight } from "../../lib/dashboard";
|
||||
import type { Filters, Flavour, RedBullEntry, UserLimits } from "../../types";
|
||||
|
||||
const CHART_HEIGHT = 300;
|
||||
const AXIS_PROPS = {
|
||||
stroke: "var(--subtle)",
|
||||
tickLine: false,
|
||||
axisLine: false,
|
||||
interval: "preserveStartEnd",
|
||||
minTickGap: 24,
|
||||
} as const;
|
||||
|
||||
type TrendsViewProps = {
|
||||
chartData: Array<{ label: string; spend: number; cans: number; caffeine: number; sugar: number }>;
|
||||
weekData: Array<{ label: string; spend: number; cans: number }>;
|
||||
@@ -49,32 +59,47 @@ export function TrendsView({
|
||||
onFilterChange,
|
||||
onSaveLimits,
|
||||
}: TrendsViewProps) {
|
||||
const totalSpend = chartData.reduce((total, point) => total + point.spend, 0);
|
||||
const totalCans = chartData.reduce((total, point) => total + point.cans, 0);
|
||||
const totalCaffeine = chartData.reduce((total, point) => total + point.caffeine, 0);
|
||||
const weekSpend = weekData.reduce((total, point) => total + point.spend, 0);
|
||||
const weekCans = weekData.reduce((total, point) => total + point.cans, 0);
|
||||
|
||||
const cansSpendLabel = `Spend and cans over ${chartData.length} logged days, total ${currency.format(totalSpend)} and ${oneDecimal.format(totalCans)} cans`;
|
||||
const caffeineLabel = `Estimated caffeine per day over ${chartData.length} logged days, total ${wholeNumber.format(totalCaffeine)} milligrams`;
|
||||
const weekLabel = `Weekly spend and cans across ${weekData.length} weeks, total ${currency.format(weekSpend)} and ${oneDecimal.format(weekCans)} cans`;
|
||||
const flavourLabel = flavourData.length
|
||||
? `Cans by flavour across ${flavourData.length} flavours. Most logged: ${flavourData[0].name}`
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<section className="logbook-layout grid gap-4">
|
||||
<FiltersPanel filters={filters} flavours={flavours} onChange={onFilterChange} compact />
|
||||
<AppCard title="Cans and spend" subtitle={`${entries.length} entries in view`}>
|
||||
{chartData.length ? (
|
||||
<ResponsiveContainer width="100%" height={340}>
|
||||
<AreaChart data={chartData} margin={{ top: 12, right: 16, bottom: 0, left: -12 }}>
|
||||
<defs>
|
||||
<linearGradient id="trendSpend" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="#39d5ff" stopOpacity={0.28} />
|
||||
<stop offset="100%" stopColor="#39d5ff" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
<linearGradient id="trendCans" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="#ff3448" stopOpacity={0.2} />
|
||||
<stop offset="100%" stopColor="#ff3448" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke="rgba(203,213,225,0.12)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Area type="monotone" dataKey="spend" name="Spend" stroke="#39d5ff" fill="url(#trendSpend)" strokeWidth={3} />
|
||||
<Area type="monotone" dataKey="cans" name="Cans" stroke="#ff3448" fill="url(#trendCans)" strokeWidth={3} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
<div role="img" aria-label={cansSpendLabel}>
|
||||
<ResponsiveContainer width="100%" height={CHART_HEIGHT}>
|
||||
<AreaChart data={chartData} margin={{ top: 12, right: 16, bottom: 0, left: -12 }}>
|
||||
<defs>
|
||||
<linearGradient id="trendSpend" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="#39d5ff" stopOpacity={0.28} />
|
||||
<stop offset="100%" stopColor="#39d5ff" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
<linearGradient id="trendCans" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="#ff3448" stopOpacity={0.2} />
|
||||
<stop offset="100%" stopColor="#ff3448" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke="var(--chart-grid)" vertical={false} />
|
||||
<XAxis dataKey="label" {...AXIS_PROPS} />
|
||||
<YAxis stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Area type="monotone" dataKey="spend" name="Spend" stroke="#39d5ff" fill="url(#trendSpend)" strokeWidth={3} />
|
||||
<Area type="monotone" dataKey="cans" name="Cans" stroke="#ff3448" fill="url(#trendCans)" strokeWidth={3} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState title="No trend data" copy="Filtered chart data appears here." />
|
||||
)}
|
||||
@@ -84,15 +109,17 @@ export function TrendsView({
|
||||
<section className="grid gap-4 xl:grid-cols-2">
|
||||
<AppCard title="Caffeine by day" subtitle="Estimated mg">
|
||||
{chartData.length ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData} margin={{ top: 12, right: 16, bottom: 0, left: -12 }}>
|
||||
<CartesianGrid stroke="rgba(203,213,225,0.12)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Bar dataKey="caffeine" name="Caffeine" fill="#39d5ff" radius={[8, 8, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<div role="img" aria-label={caffeineLabel}>
|
||||
<ResponsiveContainer width="100%" height={CHART_HEIGHT}>
|
||||
<BarChart data={chartData} margin={{ top: 12, right: 16, bottom: 0, left: -12 }}>
|
||||
<CartesianGrid stroke="var(--chart-grid)" vertical={false} />
|
||||
<XAxis dataKey="label" {...AXIS_PROPS} />
|
||||
<YAxis stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Bar dataKey="caffeine" name="Caffeine" fill="#39d5ff" radius={[8, 8, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState title="No caffeine data" copy="Add entries to estimate caffeine over time." />
|
||||
)}
|
||||
@@ -100,16 +127,18 @@ export function TrendsView({
|
||||
|
||||
<AppCard title="Weekly comparison" subtitle="Spend and cans">
|
||||
{weekData.length ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<RechartsLineChart data={weekData} margin={{ top: 12, right: 16, bottom: 0, left: -12 }}>
|
||||
<CartesianGrid stroke="rgba(203,213,225,0.12)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Line type="monotone" dataKey="spend" name="Spend" stroke="#ffd84d" strokeWidth={3} dot={{ r: 3 }} />
|
||||
<Line type="monotone" dataKey="cans" name="Cans" stroke="#ffb7d9" strokeWidth={3} dot={{ r: 3 }} />
|
||||
</RechartsLineChart>
|
||||
</ResponsiveContainer>
|
||||
<div role="img" aria-label={weekLabel}>
|
||||
<ResponsiveContainer width="100%" height={CHART_HEIGHT}>
|
||||
<RechartsLineChart data={weekData} margin={{ top: 12, right: 16, bottom: 0, left: -12 }}>
|
||||
<CartesianGrid stroke="var(--chart-grid)" vertical={false} />
|
||||
<XAxis dataKey="label" {...AXIS_PROPS} />
|
||||
<YAxis stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Line type="monotone" dataKey="spend" name="Spend" stroke="#ffd84d" strokeWidth={3} dot={{ r: 3 }} />
|
||||
<Line type="monotone" dataKey="cans" name="Cans" stroke="#ffb7d9" strokeWidth={3} dot={{ r: 3 }} />
|
||||
</RechartsLineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState title="No weekly comparison" copy="Weekly comparisons appear as your history grows." />
|
||||
)}
|
||||
@@ -119,16 +148,18 @@ export function TrendsView({
|
||||
<section className="grid gap-4 xl:grid-cols-[0.8fr_1.2fr]">
|
||||
<AppCard title="Flavour split" subtitle="Cans by flavour">
|
||||
{flavourData.length ? (
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<PieChart>
|
||||
<Pie data={flavourData} dataKey="value" nameKey="name" innerRadius={76} outerRadius={118} paddingAngle={4} stroke="#080d1f" strokeWidth={4}>
|
||||
{flavourData.map((entry) => (
|
||||
<Cell key={entry.name} fill={entry.accent} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div role="img" aria-label={flavourLabel}>
|
||||
<ResponsiveContainer width="100%" height={CHART_HEIGHT}>
|
||||
<PieChart>
|
||||
<Pie data={flavourData} dataKey="value" nameKey="name" innerRadius={76} outerRadius={118} paddingAngle={4} stroke="#080d1f" strokeWidth={4}>
|
||||
{flavourData.map((entry) => (
|
||||
<Cell key={entry.name} fill={entry.accent} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState title="No flavour split" copy="Entries will form a flavour mix here." />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user