design overhaul + expenses page
- Stripe-style theme: single indigo (#635BFF) brand, 6px radius, Inter scale; rewritten themePrimitives + MUI customizations (solid primary, near-black secondary, severity-aware Alert) - Sticky Header (brand-left nav, theme toggle, mobile drawer), inline Footer, route fade-in, per-route document.title - Home hero + feature cards (dead /dashboard,/reports links replaced) - Split-panel AuthPage with validation and password visibility - Data pages: PageHeader/EmptyState, breadcrumbed Fetch Request pages, admin table polish (numeric alignment, row-hover actions, skeletons, empty states), single-accent admin SideMenu - Global ToastProvider wired into app shell - New /expenses page: month-grouped feed, single-open accordion with inline detail (account, tags, amount, timestamps)
This commit is contained in:
@@ -1,6 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { Box, TextField, Button, Typography, IconButton, CircularProgress, Link } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
IconButton,
|
||||
CircularProgress,
|
||||
Link,
|
||||
InputAdornment,
|
||||
alpha,
|
||||
} from '@mui/material';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export type AuthMode = "login" | "register";
|
||||
|
||||
@@ -15,6 +29,12 @@ export interface AuthPageProps {
|
||||
currentUser: any;
|
||||
}
|
||||
|
||||
const FEATURES = [
|
||||
"OpenAPI-driven admin for accounts, expenses, tags & payors",
|
||||
"Import bank statements with live pipeline progress",
|
||||
"Entity & tag enrichment for clean, comparable data",
|
||||
];
|
||||
|
||||
export function AuthPage({
|
||||
mode,
|
||||
onBack,
|
||||
@@ -25,114 +45,200 @@ export function AuthPage({
|
||||
error,
|
||||
currentUser,
|
||||
}: AuthPageProps) {
|
||||
|
||||
const theme = useTheme();
|
||||
const [username, setUsername] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const [fieldErrors, setFieldErrors] = React.useState<{ username?: string; password?: string }>({});
|
||||
|
||||
const isLogin = mode === "login";
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (isLogin) {
|
||||
await login(username, password);
|
||||
} else {
|
||||
await register(username, password);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Auto-return if already logged in
|
||||
React.useEffect(() => {
|
||||
if (currentUser) onBack();
|
||||
}, [currentUser, onBack]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 400,
|
||||
mx: 'auto',
|
||||
mt: 8,
|
||||
p: 4,
|
||||
borderRadius: 3,
|
||||
boxShadow: 3,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={onBack} sx={{ mb: 2 }}>
|
||||
<ArrowBackRoundedIcon />
|
||||
const validate = (): boolean => {
|
||||
const next: { username?: string; password?: string } = {};
|
||||
if (!username.trim()) next.username = "Username is required";
|
||||
else if (username.trim().length < 3) next.username = "Username must be at least 3 characters";
|
||||
if (!password) next.password = "Password is required";
|
||||
else if (password.length < 6) next.password = "Password must be at least 6 characters";
|
||||
setFieldErrors(next);
|
||||
return Object.keys(next).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!validate()) return;
|
||||
if (isLogin) {
|
||||
await login(username.trim(), password);
|
||||
} else {
|
||||
await register(username.trim(), password);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitch = () => {
|
||||
setFieldErrors({});
|
||||
onSwitchMode();
|
||||
};
|
||||
|
||||
const form = (
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
<IconButton onClick={onBack} sx={{ mb: 2, border: '1px solid', borderColor: 'divider' }} aria-label="Go back">
|
||||
<ArrowBackRoundedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h4" fontWeight="bold" gutterBottom>
|
||||
{isLogin ? "Sign In" : "Create Account"}
|
||||
<Typography variant="h4" fontWeight={700} gutterBottom sx={{ letterSpacing: '-0.02em' }}>
|
||||
{isLogin ? "Sign in to Khata" : "Create your account"}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{isLogin
|
||||
? "Please log in to continue"
|
||||
: "Create an account to get started"}
|
||||
? "Welcome back. Enter your credentials to continue."
|
||||
: "Start managing your ledger in a few seconds."}
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Username"
|
||||
type="username"
|
||||
margin="normal"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
type="password"
|
||||
margin="normal"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => {
|
||||
setUsername(e.target.value);
|
||||
if (fieldErrors.username) setFieldErrors((p) => ({ ...p, username: undefined }));
|
||||
}}
|
||||
error={!!fieldErrors.username}
|
||||
helperText={fieldErrors.username}
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
if (fieldErrors.password) setFieldErrors((p) => ({ ...p, password: undefined }));
|
||||
}}
|
||||
error={!!fieldErrors.password}
|
||||
helperText={fieldErrors.password}
|
||||
autoComplete={isLogin ? 'current-password' : 'new-password'}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
onClick={() => setShowPassword((s) => !s)}
|
||||
edge="end"
|
||||
size="small"
|
||||
sx={{ border: 'none' }}
|
||||
>
|
||||
{showPassword ? <VisibilityOffIcon fontSize="small" /> : <VisibilityIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<Typography color="error" variant="body2" sx={{ mt: 1 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 3 }}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
) : isLogin ? (
|
||||
"Login"
|
||||
) : (
|
||||
"Register"
|
||||
{error && (
|
||||
<Typography color="error" variant="body2" sx={{ mt: 0.5 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="large"
|
||||
sx={{ mt: 1, fontWeight: 600 }}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={20} color="inherit" />
|
||||
) : isLogin ? (
|
||||
"Sign In"
|
||||
) : (
|
||||
"Create Account"
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
align="center"
|
||||
sx={{ mt: 3 }}
|
||||
>
|
||||
{isLogin ? "Don’t have an account?" : "Already have an account?"}{' '}
|
||||
<Link
|
||||
component="button"
|
||||
underline="hover"
|
||||
color="primary"
|
||||
onClick={onSwitchMode}
|
||||
sx={{ fontWeight: 500 }}
|
||||
>
|
||||
{isLogin ? "Register" : "Login"}
|
||||
<Typography variant="body2" color="text.secondary" align="center" sx={{ mt: 3 }}>
|
||||
{isLogin ? "Don't have an account?" : "Already have an account?"}{' '}
|
||||
<Link component="button" underline="hover" color="primary" onClick={handleSwitch} sx={{ fontWeight: 600 }}>
|
||||
{isLogin ? "Register" : "Sign In"}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: 'calc(100vh - 64px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: { xs: 2, md: 4 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 960,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', md: '5fr 6fr' },
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
backgroundColor: 'background.paper',
|
||||
boxShadow: (t) =>
|
||||
`0 12px 40px ${alpha(t.palette.common.black, t.palette.mode === 'dark' ? 0.4 : 0.08)}`,
|
||||
}}
|
||||
>
|
||||
{/* LEFT BRAND PANEL */}
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'flex' },
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
p: 4,
|
||||
background: 'linear-gradient(150deg, #635BFF 0%, #4F46E5 55%, #3B82F6 100%)',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h6" fontWeight={700} sx={{ letterSpacing: '-0.02em' }}>
|
||||
Khata
|
||||
</Typography>
|
||||
<Typography sx={{ mt: 3, fontSize: '1.25rem', fontWeight: 600, lineHeight: 1.4, maxWidth: 320 }}>
|
||||
The intelligent, extensible financial ledger for your cashflow.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
{FEATURES.map((f) => (
|
||||
<Box key={f} sx={{ display: 'flex', gap: 1.5, alignItems: 'flex-start' }}>
|
||||
<CheckRoundedIcon sx={{ fontSize: 18, mt: 0.25, color: alpha('#fff', 0.9) }} />
|
||||
<Typography variant="body2" sx={{ color: alpha('#fff', 0.92), lineHeight: 1.5 }}>
|
||||
{f}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* RIGHT FORM PANEL */}
|
||||
<Box sx={{ p: { xs: 3, md: 5 }, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
{form}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user