Redesign login screen with the app's ledgr theme
Navy brand panel (ledger-grid texture, purple glow, product pitch) + sign-in form on the canvas: input icons, show/hide password, inline error, entrance animation (motion-safe). Mobile collapses to the form with a compact brand header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>main
parent
11bceedaca
commit
e984cd09ed
|
|
@ -1,12 +1,18 @@
|
|||
import { FormEvent, useState } from "react";
|
||||
import { Landmark, LogIn } from "lucide-react";
|
||||
import { BadgeCheck, Eye, EyeOff, Landmark, Lock, LogIn, ShieldCheck, User } from "lucide-react";
|
||||
import { Spinner } from "../components/ui";
|
||||
import { useAuth } from "../auth";
|
||||
|
||||
/**
|
||||
* Sign-in screen: navy brand panel (desktop) + form on the app canvas.
|
||||
* Uses the app's "ledgr" tokens — periwinkle primary, navy, lavender canvas — so the
|
||||
* login feels like the first screen of the dashboard, not a bolt-on.
|
||||
*/
|
||||
export default function Login() {
|
||||
const { login } = useAuth();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPw, setShowPw] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
|
|
@ -24,41 +30,147 @@ export default function Login() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full bg-canvas flex items-center justify-center p-4">
|
||||
<div className="card w-full max-w-sm p-6 shadow-pop">
|
||||
<div className="flex items-center gap-2.5 mb-5">
|
||||
<span className="inline-flex items-center justify-center w-9 h-9 rounded-xl bg-primary text-white">
|
||||
<Landmark size={18} />
|
||||
<div className="min-h-full flex bg-canvas">
|
||||
{/* ---------------- brand panel (desktop) ---------------- */}
|
||||
<aside
|
||||
className="hidden lg:flex flex-col justify-between w-[44%] max-w-xl p-12 text-white relative overflow-hidden bg-navy"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
radial-gradient(52rem 30rem at -10% -20%, rgba(109,93,232,0.45), transparent 60%),
|
||||
radial-gradient(40rem 26rem at 110% 115%, rgba(201,195,245,0.22), transparent 60%),
|
||||
linear-gradient(rgba(255,255,255,0.045) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.045) 1px, transparent 1px)`,
|
||||
backgroundSize: "auto, auto, 100% 3.25rem, 3.25rem 100%",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="inline-flex items-center justify-center w-11 h-11 rounded-2xl bg-primary text-white shadow-pop">
|
||||
<Landmark size={22} />
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-sm font-semibold leading-tight text-ink">Amazon A/R Aging</div>
|
||||
<div className="text-[11px] text-muted">Month-End Closing · sign in</div>
|
||||
<div className="text-base font-semibold leading-tight">Amazon A/R Aging</div>
|
||||
<div className="text-xs text-lavender">Month-End Closing · Utopia Brands Finance</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} className="space-y-3">
|
||||
<div>
|
||||
<label className="label" htmlFor="login-user">Username</label>
|
||||
<input id="login-user" className="input" autoComplete="username" autoFocus
|
||||
value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="label" htmlFor="login-pw">Password</label>
|
||||
<input id="login-pw" className="input" type="password" autoComplete="current-password"
|
||||
value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-bad">{error}</p>}
|
||||
<button className="btn-primary w-full justify-center" type="submit"
|
||||
disabled={busy || !username.trim() || !password}>
|
||||
{busy ? <Spinner /> : <LogIn size={16} />} Sign in
|
||||
</button>
|
||||
</form>
|
||||
<div className="max-w-md">
|
||||
<h1 className="text-3xl xl:text-4xl font-semibold leading-tight [text-wrap:balance]">
|
||||
Every month closed, checked, and accounted for.
|
||||
</h1>
|
||||
<p className="mt-4 text-sm leading-relaxed text-lavender-soft/90">
|
||||
Upload the month's Amazon transaction files and get the full receivable
|
||||
position — settlements, aging, journal entry, and the audit trail behind
|
||||
every figure.
|
||||
</p>
|
||||
|
||||
<p className="text-[11px] text-muted mt-4">
|
||||
Accounts are created by the administrator on the server
|
||||
(<span className="num">manage.py add-user</span>) — there is no self-signup.
|
||||
<ul className="mt-8 space-y-3 text-sm">
|
||||
{[
|
||||
{ Icon: ShieldCheck, text: "Processed on the company server — financial data never leaves" },
|
||||
{ Icon: BadgeCheck, text: "Six month-end controls gate every published figure" },
|
||||
].map(({ Icon, text }) => (
|
||||
<li key={text} className="flex items-start gap-3">
|
||||
<span className="mt-0.5 inline-flex items-center justify-center w-6 h-6 rounded-lg bg-white/10 text-lavender shrink-0">
|
||||
<Icon size={14} />
|
||||
</span>
|
||||
<span className="text-lavender-soft/90 leading-snug">{text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-lavender/60">
|
||||
Amazon-only · 13 marketplaces · reconciles to the cent
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* ---------------- sign-in form ---------------- */}
|
||||
<main
|
||||
className="flex-1 flex items-center justify-center p-6 sm:p-10"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(38rem 24rem at 85% -10%, rgba(109,93,232,0.10), transparent 65%)",
|
||||
}}
|
||||
>
|
||||
<div className="w-full max-w-sm motion-safe:animate-[login-in_.45s_ease-out]">
|
||||
{/* compact brand header for mobile, where the panel is hidden */}
|
||||
<div className="lg:hidden flex items-center gap-2.5 mb-8">
|
||||
<span className="inline-flex items-center justify-center w-9 h-9 rounded-xl bg-primary text-white shadow-card">
|
||||
<Landmark size={18} />
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-sm font-semibold leading-tight text-ink">Amazon A/R Aging</div>
|
||||
<div className="text-[11px] text-muted">Month-End Closing</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-semibold text-ink">Welcome back</h2>
|
||||
<p className="mt-1 text-sm text-subink">Sign in to continue to this month's closing.</p>
|
||||
|
||||
<form onSubmit={submit} className="mt-8 space-y-4">
|
||||
<div>
|
||||
<label className="label" htmlFor="login-user">Username</label>
|
||||
<div className="relative">
|
||||
<User size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
|
||||
<input
|
||||
id="login-user"
|
||||
className="input pl-10 py-2.5"
|
||||
placeholder="you@utopiabrands.com"
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label" htmlFor="login-pw">Password</label>
|
||||
<div className="relative">
|
||||
<Lock size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-faint pointer-events-none" />
|
||||
<input
|
||||
id="login-pw"
|
||||
className="input pl-10 pr-11 py-2.5"
|
||||
type={showPw ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={showPw ? "Hide password" : "Show password"}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded-lg text-faint hover:text-subink hover:bg-neutralbg transition-colors"
|
||||
onClick={() => setShowPw((v) => !v)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPw ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-xl border border-bad/30 bg-badbg/60 px-3.5 py-2.5 text-sm text-bad">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btn-primary w-full justify-center py-2.5 text-[15px]"
|
||||
type="submit"
|
||||
disabled={busy || !username.trim() || !password}
|
||||
>
|
||||
{busy ? <Spinner /> : <LogIn size={16} />} Sign in
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-8 text-xs text-muted leading-relaxed">
|
||||
No account or forgot your password? Ask the administrator — accounts are
|
||||
created and reset on the server, never self-service.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* one-time entrance; browsers honouring reduced motion skip it via motion-safe */}
|
||||
<style>{`@keyframes login-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue