- Migrate login page to atelier design system (editorial modal style) - Polish root error.tsx, not-found.tsx, loading.tsx with atelier design - Add JSON-LD structured data: SoftwareApplication + LocalBusiness - Fix next.config.ts outputFileTracingRoot absolute path warning - Add force-dynamic to protected-example (uses cookies) - Add proper type for stops page (replace : any) - Fix unescaped quotes in StopTableClient - Fix no-explicit-any in db-schema route - Clean up console.logs in admin-permissions - Fix inline any in admin/stops page - Update OG image references to existing og-default.svg
This commit is contained in:
+118
-99
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useId } from "react";
|
||||
|
||||
type LoginClientProps = {
|
||||
error: string | null;
|
||||
@@ -12,6 +12,8 @@ const REDIRECT_URL = "/admin";
|
||||
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);
|
||||
@@ -54,156 +56,173 @@ export default function LoginClient({ error }: LoginClientProps) {
|
||||
window.location.href = REDIRECT_URL;
|
||||
}
|
||||
|
||||
return (
|
||||
<main
|
||||
className="min-h-screen flex flex-col relative overflow-hidden"
|
||||
style={{ backgroundColor: "#faf8f5", height: "100vh" }}
|
||||
>
|
||||
<style jsx global>{`
|
||||
@import url("https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap");
|
||||
html, body { overflow: hidden; }
|
||||
`}</style>
|
||||
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-96 h-96 rounded-full opacity-20" style={{ background: "radial-gradient(circle at 30% 30%, #c97a3e20 0%, transparent 70%)", filter: "blur(40px)" }} />
|
||||
<div className="absolute -bottom-48 -left-48 w-[600px] h-[600px] rounded-full opacity-15" style={{ background: "radial-gradient(circle at 70% 70%, #6b8f7130 0%, transparent 70%)", filter: "blur(60px)" }} />
|
||||
<div className="absolute top-1/3 left-1/4 w-72 h-72 rounded-full opacity-10" style={{ background: "radial-gradient(circle, #1a4d2e15 0%, transparent 70%)", filter: "blur(30px)" }} />
|
||||
<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>
|
||||
|
||||
<div className="flex-1 flex items-center justify-center px-6 py-12 relative z-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="relative bg-white/80 backdrop-blur-xl rounded-3xl shadow-xl ring-1 ring-black/5 overflow-hidden">
|
||||
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-[#6b8f71]/30 to-transparent" />
|
||||
{/* Editorial corner flourish */}
|
||||
<div className="absolute top-6 left-6 atelier-flourish w-20 h-20 opacity-40" aria-hidden="true" />
|
||||
|
||||
<div className="p-8 sm:p-10">
|
||||
<div className="text-center mb-8">
|
||||
<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-16 w-16 items-center justify-center rounded-2xl mb-5"
|
||||
className="inline-flex h-14 w-14 items-center justify-center rounded-2xl mb-5"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #1a4d2e 0%, #2d6a45 100%)",
|
||||
boxShadow: "0 12px 32px rgba(26, 77, 46, 0.25)",
|
||||
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-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<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="text-3xl font-semibold text-stone-900"
|
||||
style={{ fontFamily: "'Cormorant Garamond', Georgia, serif", letterSpacing: "-0.02em" }}
|
||||
>
|
||||
Welcome back
|
||||
<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"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif", color: "#7a7570" }}
|
||||
>
|
||||
Sign in to your account
|
||||
<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>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-700 mb-1.5" style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}>
|
||||
<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="w-full rounded-xl border border-stone-200/80 bg-white px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-[#6b8f71]/40 focus:border-[#6b8f71]"
|
||||
placeholder="you@example.com"
|
||||
className="atelier-input"
|
||||
placeholder="you@yourfarm.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={loading}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleSignIn(); }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-stone-700 mb-1.5" style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}>
|
||||
Password
|
||||
</label>
|
||||
<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="w-full rounded-xl border border-stone-200/80 bg-white px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-[#6b8f71]/40 focus:border-[#6b8f71]"
|
||||
className="atelier-input"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={loading}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleSignIn(); }}
|
||||
/>
|
||||
</div>
|
||||
{(error || localError) && (
|
||||
<p className="text-sm text-rose-700 bg-rose-50 border border-rose-200 rounded-lg px-3 py-2">
|
||||
{error ?? localError}
|
||||
</p>
|
||||
|
||||
{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="button"
|
||||
onClick={handleSignIn}
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full rounded-xl px-6 py-3.5 text-sm font-semibold text-white shadow-sm transition-all disabled:opacity-60 disabled:cursor-not-allowed active:scale-[0.98]"
|
||||
style={{
|
||||
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
|
||||
background: loading
|
||||
? "linear-gradient(135deg, #6b8f71 0%, #7ba085 100%)"
|
||||
: "linear-gradient(135deg, #1a4d2e 0%, #2d6a45 100%)",
|
||||
boxShadow: "0 8px 24px rgba(26, 77, 46, 0.20)",
|
||||
}}
|
||||
className="atelier-cta w-full"
|
||||
>
|
||||
{loading ? "Signing in…" : "Sign in with email"}
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Development bypass buttons */}
|
||||
{isDev && (
|
||||
<div className="mt-6 pt-6 border-t border-stone-200">
|
||||
<p className="text-xs text-stone-500 text-center mb-3" style={{ fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif" }}>
|
||||
Dev Mode — Quick Access
|
||||
</p>
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDevLogin("platform_admin")}
|
||||
className="rounded-lg px-3 py-2 text-xs font-medium text-white transition-all hover:opacity-90"
|
||||
style={{
|
||||
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
|
||||
background: "#1a4d2e",
|
||||
}}
|
||||
>
|
||||
Platform Admin
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDevLogin("brand_admin")}
|
||||
className="rounded-lg px-3 py-2 text-xs font-medium text-white transition-all hover:opacity-90"
|
||||
style={{
|
||||
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
|
||||
background: "#2d6a45",
|
||||
}}
|
||||
>
|
||||
Brand Admin
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDevLogin("store_employee")}
|
||||
className="rounded-lg px-3 py-2 text-xs font-medium text-white transition-all hover:opacity-90"
|
||||
style={{
|
||||
fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif",
|
||||
background: "#6b8f71",
|
||||
}}
|
||||
>
|
||||
Store Employee
|
||||
</button>
|
||||
{[
|
||||
{ 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="text-center text-[11px] text-stone-500 mt-6 tracking-wide" style={{ fontFamily: "var(--font-fragment-mono)" }}>
|
||||
Protected by Neon Auth · Encrypted at rest
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user