Stripe-style UI overhaul + Expenses feed with month grouping (#15)
## Title Stripe-style UI overhaul + Expenses feed with month grouping ## Summary Rework the khata-ui frontend to a Stripe-grade design system and add a rich, monthly-grouped Expenses experience driven by react-openapi's shared, spec-configured field renderers. ## Highlights ### Design overhaul - New design system: single indigo (#635BFF), 6px radius, Inter type scale, streamlined surfaces/inputs/feedback tokens - Sticky app bar with brand-left header + inline footer, route titles - Split-panel auth pages, Stripe-style Home hero + feature cards - Toast provider + shared PageHeader/EmptyState UI primitives - Admin polish: right-aligned numerics, row-hover actions, skeletons, richer empty states, breadcrumbed fetch-request flows ### Expenses page - Monthly grouped feed with stat cards (total spent / this month / income) - Month cards as single-open accordions with red-spent / green-income totals - Flat transaction rows: logo, name/date, account, amount (no accordion) - Floating month–year selector pill (scroll-spy) that expands + scrolls to the selected month - Strict DD-MM-YYYY date parsing (day-first), no format guessing ### Shared react-openapi fields - New `CurrencyField` + `formatCurrency` (cached Intl, sign-colored, sign-less amounts; form editing reuses NumberField) - `resourceConfig.fieldTypes` override mechanism (amount → currency) - Expenses page rendered via ListCellRenderer / DetailFieldRenderer / applyDisplayFormat — zero custom field renderers - `resolveMediaUrl` resolves relative `/uploads/...` against the API base for every image field (Admin + Expenses logos) ### Fixes - Perf: memoized rows, formatter caches, single-open accordions — cuts slow-click INP and DOM-nesting warnings - Removed unused @mui/x-data-grid@7, which hoisted @mui/system@7 and crashed Box/createTheme at runtime (v5/v7 mix) ## Commits (10) -9808a1fdesign overhaul + expenses page -d6856a5fix expenses list performance + dom nesting -3dd833ashared currency field + expenses rendered from react-openapi fields -e8c585bfix expenses grouping to parse occurred_at as DD-MM-YYYY strictly -cc940eeexpense list: split month totals, sticky headers + month pill -47d799fexpense list: clickable month pill + fix scroll-to-month -537aef9resolve relative media URLs against the API base in react-openapi -8e9a13dexpense list: replace accordion with flat Stripe-style rows -62dd06cexpense list: month accordions + month-year selector; drop unused x-data-grid -806bd42dropped amount sign from CurrencyField.tsx Reviewed-on: #15 Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com> Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
This commit is contained in:
236
src/Home.tsx
236
src/Home.tsx
@@ -1,11 +1,11 @@
|
||||
import * as React from "react";
|
||||
import { Box, Typography, Button, Container, Grid, Paper, Chip } from "@mui/material";
|
||||
import { useTheme, alpha } from "@mui/material/styles";
|
||||
import { Box, Typography, Button, Container, Grid, Paper, alpha } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import DashboardIcon from "@mui/icons-material/Dashboard";
|
||||
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import BarChartIcon from "@mui/icons-material/BarChart";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet";
|
||||
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
|
||||
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
|
||||
import { useAuth } from "../react-auth";
|
||||
|
||||
@@ -14,11 +14,9 @@ interface FeatureCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
path: string;
|
||||
label?: string;
|
||||
accent: string;
|
||||
}
|
||||
|
||||
function FeatureCard({ icon, title, description, path, label, accent }: FeatureCardProps) {
|
||||
function FeatureCard({ icon, title, description, path }: FeatureCardProps) {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -35,206 +33,154 @@ function FeatureCard({ icon, title, description, path, label, accent }: FeatureC
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
transition: "all 0.25s ease",
|
||||
"&::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
background: accent,
|
||||
opacity: 0,
|
||||
transition: "opacity 0.25s ease",
|
||||
},
|
||||
transition: "border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease",
|
||||
"&:hover": {
|
||||
transform: "translateY(-4px)",
|
||||
boxShadow: `0 12px 32px ${alpha(theme.palette.common.black, theme.palette.mode === "dark" ? 0.3 : 0.08)}`,
|
||||
borderColor: "transparent",
|
||||
"&::before": { opacity: 1 },
|
||||
transform: "translateY(-2px)",
|
||||
boxShadow: `0 8px 24px ${alpha(theme.palette.common.black, theme.palette.mode === "dark" ? 0.35 : 0.08)}`,
|
||||
"& .feature-arrow": {
|
||||
opacity: 1,
|
||||
transform: "translateX(0)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5, mb: 1.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: alpha(accent, 0.12),
|
||||
color: accent,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
<Typography variant="subtitle1" fontWeight={700}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "action.hover",
|
||||
color: "primary.main",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
|
||||
<Typography variant="subtitle1" fontWeight={600} sx={{ mb: 0.75 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ flex: 1, lineHeight: 1.6 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
|
||||
{label && (
|
||||
<Chip
|
||||
label={label}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ mt: 2, alignSelf: "flex-start", textTransform: "capitalize" }}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
className="feature-arrow"
|
||||
sx={{
|
||||
mt: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
color: "primary.main",
|
||||
fontWeight: 600,
|
||||
opacity: 0,
|
||||
transform: "translateX(-4px)",
|
||||
transition: "opacity 180ms ease, transform 180ms ease",
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" fontWeight={600} color="primary">
|
||||
Open
|
||||
</Typography>
|
||||
<ArrowForwardIcon sx={{ fontSize: 14 }} />
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { currentUser } = useAuth();
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <DashboardIcon />,
|
||||
title: "Dashboard",
|
||||
description: "Visualise inflows and outflows with interactive charts, drill into categories, and track trends over daily, weekly, and monthly periods.",
|
||||
path: "/dashboard",
|
||||
accent: theme.palette.mode === "dark" ? "#818cf8" : "#6366f1",
|
||||
icon: <ReceiptLongIcon />,
|
||||
title: "Expenses",
|
||||
description:
|
||||
"Browse every transaction imported from your accounts — grouped by month, with entity, account, tags, and amount at a glance.",
|
||||
path: "/expenses",
|
||||
},
|
||||
{
|
||||
icon: <SyncIcon />,
|
||||
title: "Fetch Requests",
|
||||
description: "Upload bank statements or configure email ingestion to auto-import transactions. Track pipeline status from pending through to completion.",
|
||||
description:
|
||||
"Upload bank statements or configure email ingestion to auto-import transactions. Track pipeline status from pending through to completion.",
|
||||
path: "/fetch-requests",
|
||||
accent: theme.palette.mode === "dark" ? "#34d399" : "#10b981",
|
||||
},
|
||||
{
|
||||
icon: <BarChartIcon />,
|
||||
title: "Report Snapshots",
|
||||
description: "Generate cached report snapshots with custom filters — accounts, date ranges, amount bounds — then pin a snapshot on the dashboard for consistent comparisons.",
|
||||
path: "/reports",
|
||||
accent: theme.palette.mode === "dark" ? "#fbbf24" : "#f59e0b",
|
||||
},
|
||||
{
|
||||
icon: <SettingsIcon />,
|
||||
title: "Admin",
|
||||
description: "Full CRUD over accounts, expenses, tags, and payors. Manage your data programmatically through the OpenAPI-driven admin panel.",
|
||||
icon: <AccountBalanceWalletIcon />,
|
||||
title: "Accounts & Entities",
|
||||
description:
|
||||
"Manage your accounts, expenses, tags, and payors through the OpenAPI-driven admin panel with full CRUD and server-side filters.",
|
||||
path: "/admin",
|
||||
accent: theme.palette.mode === "dark" ? "#e879f9" : "#d946ef",
|
||||
},
|
||||
{
|
||||
icon: <AccountCircleIcon />,
|
||||
title: "Profile",
|
||||
description:
|
||||
"View and edit your user profile — username, email, and account preferences — from the profile page.",
|
||||
path: "/profile/me",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "calc(100vh - 64px)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
"&::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: "-15%",
|
||||
left: "-8%",
|
||||
width: "45%",
|
||||
height: "55%",
|
||||
background: "radial-gradient(circle, rgba(99,102,241,0.12) 0%, transparent 70%)",
|
||||
zIndex: 0,
|
||||
},
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: "-15%",
|
||||
right: "-8%",
|
||||
width: "45%",
|
||||
height: "55%",
|
||||
background: "radial-gradient(circle, rgba(236,72,153,0.1) 0%, transparent 70%)",
|
||||
zIndex: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg" sx={{ position: "relative", zIndex: 1, flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", py: 6 }}>
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
mb: 6,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Container maxWidth="lg" sx={{ py: { xs: 6, md: 10 } }}>
|
||||
<Box sx={{ maxWidth: 680, mx: { xs: "auto", md: 0 }, textAlign: { xs: "center", md: "left" } }}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
sx={{ color: "primary.main", mb: 1.5, display: "block", fontWeight: 700 }}
|
||||
>
|
||||
Khata · Financial Ledger
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
fontSize: { xs: "2.5rem", sm: "3.5rem", md: "5rem" },
|
||||
background: "linear-gradient(135deg, #6366f1 0%, #ec4899 50%, #f59e0b 100%)",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
color: "text.primary",
|
||||
fontSize: { xs: "2.25rem", md: "3.25rem" },
|
||||
letterSpacing: "-0.03em",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
Welcome to Khata
|
||||
Your intelligent, extensible financial ledger.
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="h6"
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
maxWidth: 580,
|
||||
mx: "auto",
|
||||
lineHeight: 1.7,
|
||||
fontWeight: 400,
|
||||
fontSize: { xs: "1rem", md: "1.15rem" },
|
||||
}}
|
||||
sx={{ maxWidth: 540, lineHeight: 1.7, fontSize: "1.0625rem" }}
|
||||
>
|
||||
Your intelligent, extensible financial ledger. Import transactions, generate reports, and stay on top of your cashflow.
|
||||
Import transactions from bank statements, enrich them with entity and tag intelligence,
|
||||
and stay on top of your cashflow — all in one place.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mt: 4, display: "flex", gap: 2, justifyContent: "center", flexWrap: "wrap" }}>
|
||||
<Box sx={{ mt: 4, display: "flex", gap: 2, justifyContent: { xs: "center", md: "flex-start" }, flexWrap: "wrap" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
endIcon={<ArrowForwardIcon />}
|
||||
onClick={() => navigate("/dashboard")}
|
||||
sx={{
|
||||
px: 4,
|
||||
py: 1.4,
|
||||
borderRadius: "50px",
|
||||
fontWeight: 700,
|
||||
background: "linear-gradient(135deg, #6366f1 0%, #ec4899 100%)",
|
||||
transition: "transform 0.2s ease, box-shadow 0.2s",
|
||||
"&:hover": {
|
||||
transform: "translateY(-2px)",
|
||||
boxShadow: `0 8px 24px ${alpha(theme.palette.primary.main, 0.35)}`,
|
||||
},
|
||||
}}
|
||||
onClick={() => navigate(currentUser ? "/fetch-requests" : "/login")}
|
||||
sx={{ px: 3.5, py: 1.4, fontWeight: 600 }}
|
||||
>
|
||||
Enter Dashboard
|
||||
{currentUser ? "Enter App" : "Get Started"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
onClick={() => navigate("/fetch-requests")}
|
||||
sx={{
|
||||
px: 4,
|
||||
py: 1.4,
|
||||
borderRadius: "50px",
|
||||
fontWeight: 600,
|
||||
borderWidth: 2,
|
||||
"&:hover": { borderWidth: 2 },
|
||||
}}
|
||||
sx={{ px: 3.5, py: 1.4, fontWeight: 600 }}
|
||||
>
|
||||
Import Data
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid container spacing={3} sx={{ mt: { xs: 4, md: 8 } }}>
|
||||
{features.map((f) => (
|
||||
<Grid key={f.title} item xs={12} sm={6} md={3}>
|
||||
<FeatureCard {...f} />
|
||||
|
||||
Reference in New Issue
Block a user