Files
route-commerce/src/app/login/LoginClient.tsx
T
Tyler c8fa2e8b52
Deploy to route.crispygoat.com / deploy (push) Successful in 4m33s
polish: consolidate typography to next/font variables, add atelier utility classes
- Remove Google Fonts @import from 4 components (SiteHeader, LandingPageWrapper,
  TestimonialsAndCTA, FeaturesAndStats) — eliminate render-blocking external
  request and font flash
- Replace all unloaded Cormorant Garamond / Playfair Display / DM Sans /
  Plus Jakarta Sans references with the existing next/font variables
  (Fraunces, Manrope, Fragment_Mono) — visual coherence with the design system
- Update 9 pages (blog, brands, changelog, maintenance, protected-example,
  roadmap, security, waitlist, WaitlistForm) to use the loaded Fraunces
- Fix dead --font-geist / --font-jetbrains-mono references in
  admin-design-system.css (now point to --font-manrope / --font-fragment-mono
  which are actually loaded)
- Add atelier-pill, atelier-numerals, atelier-fineprint, atelier-canvas-soft
  utility classes; .atelier-input:focus-visible refined ring
- Add .ha-field-textarea, .ha-scroll, .ha-skeleton to admin design system
- Extend prefers-reduced-motion guard to atelier-enter/stagger/shimmer
- Apply atelier-fineprint to login/not-found/error pages for consistency
- No structural changes; build passes, 0 type errors
2026-06-16 23:50:16 -06:00

229 lines
9.5 KiB
TypeScript

"use client";
import { useState, useId } from "react";
type LoginClientProps = {
error: string | null;
};
const REDIRECT_URL = "/admin";
// Development mode detection
const isDev = process.env.NODE_ENV !== "production";
export default function LoginClient({ error }: LoginClientProps) {
const emailId = useId();
const passwordId = useId();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
async function handleSignIn() {
if (!email || !password) {
setLocalError("Email and password are required.");
return;
}
setLocalError(null);
setLoading(true);
try {
const res = await fetch("/api/auth/sign-in", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.trim().toLowerCase(), password: password.trim() }),
});
const data = await res.json();
if (!res.ok || data.error) {
setLocalError(data.message ?? "Sign-in failed. Please check your email and password.");
setLoading(false);
return;
}
// Cookie is set server-side via next/headers — just redirect on success.
window.location.href = REDIRECT_URL;
} catch {
setLocalError("Sign-in failed. Please try again.");
setLoading(false);
}
}
function handleDevLogin(role: string) {
// Set the dev_session cookie for local development bypass
document.cookie = `dev_session=${role}; path=/; max-age=86400`;
window.location.href = REDIRECT_URL;
}
const displayError = error ?? localError;
return (
<main className="atelier-canvas relative min-h-screen flex flex-col overflow-hidden">
{/* Decorative grain layer */}
<div className="atelier-grain" aria-hidden="true" />
{/* Ambient background flourishes */}
<div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
<div
className="absolute -top-32 -right-32 w-[28rem] h-[28rem] rounded-full opacity-30"
style={{ background: "radial-gradient(circle at 30% 30%, rgba(202, 138, 4, 0.18) 0%, transparent 70%)", filter: "blur(48px)" }}
/>
<div
className="absolute -bottom-48 -left-48 w-[36rem] h-[36rem] rounded-full opacity-25"
style={{ background: "radial-gradient(circle at 70% 70%, rgba(34, 78, 47, 0.16) 0%, transparent 70%)", filter: "blur(60px)" }}
/>
</div>
{/* Editorial corner flourish */}
<div className="absolute top-6 left-6 atelier-flourish w-20 h-20 opacity-40" aria-hidden="true" />
<div className="flex-1 flex items-center justify-center px-6 py-12 relative z-10">
<div className="w-full max-w-md">
{/* Editorial numeral + section label */}
<div className="text-center mb-6">
<span className="atelier-section-num">No. 01</span>
</div>
<div className="atelier-enter relative bg-white/90 backdrop-blur-xl rounded-2xl ring-1 ring-stone-200/80 shadow-[0_24px_64px_-24px_rgba(20,83,45,0.25)] overflow-hidden">
{/* Top hairline rule */}
<div className="atelier-rule" />
<div className="px-8 sm:px-10 py-10">
{/* Brand mark */}
<div className="flex flex-col items-center mb-8">
<div
className="inline-flex h-14 w-14 items-center justify-center rounded-2xl mb-5"
style={{
background: "linear-gradient(135deg, #14532D 0%, #1F6B3E 50%, #14532D 100%)",
boxShadow: "0 10px 28px -6px rgba(20, 83, 45, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.12)",
}}
>
<svg className="h-7 w-7 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5} aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</svg>
</div>
<h1 className="atelier-title text-3xl sm:text-4xl text-center">
Welcome <span className="atelier-italic text-[#786B53]">back</span>
</h1>
<p className="mt-2 text-sm text-stone-500 text-center">
Sign in to your <em className="atelier-italic not-italic font-normal">atelier</em> account
</p>
</div>
<form
onSubmit={(e) => {
e.preventDefault();
handleSignIn();
}}
className="space-y-5"
noValidate
>
<div className="flex flex-col gap-1.5">
<label htmlFor={emailId} className="atelier-section-label">
Email
</label>
<input
id={emailId}
type="email"
required
autoComplete="username"
className="atelier-input"
placeholder="you@yourfarm.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
/>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<label htmlFor={passwordId} className="atelier-section-label">
Password
</label>
<a
href="/forgot-password"
className="text-[10px] tracking-[0.15em] uppercase text-stone-500 hover:text-[#14532D] transition-colors"
style={{ fontFamily: "var(--font-fragment-mono)" }}
>
Forgot?
</a>
</div>
<input
id={passwordId}
type="password"
required
autoComplete="current-password"
className="atelier-input"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
</div>
{displayError && (
<div
role="alert"
className="atelier-stagger rounded-lg border border-rose-200/80 bg-rose-50/80 px-3.5 py-2.5 text-sm text-rose-800 flex items-start gap-2"
>
<svg className="h-4 w-4 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5} aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>{displayError}</span>
</div>
)}
<button
type="submit"
disabled={loading}
className="atelier-cta w-full"
>
<span className="relative z-10">{loading ? "Signing in…" : "Sign in"}</span>
{!loading && (
<svg className="h-4 w-4 relative z-10" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
)}
<span className="atelier-cta-shimmer" aria-hidden="true" />
</button>
</form>
{/* Development bypass buttons */}
{isDev && (
<div className="mt-8 pt-6 border-t border-stone-200/80">
<p className="atelier-section-label text-center mb-3">Dev Mode · Quick Access</p>
<div className="grid grid-cols-3 gap-2">
{[
{ role: "platform_admin", label: "Platform", color: "#14532D" },
{ role: "brand_admin", label: "Brand", color: "#1F6B3E" },
{ role: "store_employee", label: "Store", color: "#6F8562" },
].map((opt) => (
<button
key={opt.role}
type="button"
onClick={() => handleDevLogin(opt.role)}
className="rounded-lg px-3 py-2 text-[11px] font-semibold text-white transition-all hover:opacity-90 active:scale-[0.98]"
style={{
fontFamily: "var(--font-manrope)",
background: opt.color,
letterSpacing: "0.04em",
}}
>
{opt.label}
</button>
))}
</div>
</div>
)}
</div>
</div>
<p className="atelier-fineprint text-center mt-6">
Protected by Neon Auth · Encrypted at rest
</p>
</div>
</div>
</main>
);
}