202 lines
7.1 KiB
TypeScript
202 lines
7.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, type FormEvent } from 'react'
|
|
import { Eye, EyeOff } from 'lucide-react'
|
|
import { useRouter } from 'next/navigation'
|
|
import Image from 'next/image'
|
|
import { Space_Grotesk } from 'next/font/google'
|
|
import { useAuth } from '@/contexts/auth-context'
|
|
import { getApiError } from '@/lib/api'
|
|
import { toast } from '@/components/ui/toast'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
const sg = Space_Grotesk({ subsets: ['latin'], weight: ['400', '500', '600', '700'] })
|
|
|
|
export default function LoginPage() {
|
|
const { login, user, loading } = useAuth()
|
|
const router = useRouter()
|
|
const [identifier, setIdentifier] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [showPassword, setShowPassword] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!loading && user) router.replace('/')
|
|
}, [user, loading, router])
|
|
|
|
async function handleSubmit(e: FormEvent) {
|
|
e.preventDefault()
|
|
setError(null)
|
|
setSubmitting(true)
|
|
try {
|
|
await login(identifier, password)
|
|
toast.add({ title: 'Signed in successfully', type: 'success' })
|
|
} catch (err) {
|
|
const message = getApiError(err)
|
|
setError(message)
|
|
toast.add({ title: 'Sign in failed', description: message, type: 'error' })
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={`${sg.className} min-h-svh flex flex-col md:flex-row`}>
|
|
|
|
{/* ── Left panel: image (hidden on mobile, 60% on desktop) ── */}
|
|
<div className="relative hidden md:flex md:w-[60%] shrink-0 flex-col">
|
|
<Image
|
|
src="/HexaDistribution.png"
|
|
alt="Distribution warehouse"
|
|
fill
|
|
sizes="60vw"
|
|
className="object-cover object-center"
|
|
priority
|
|
unoptimized
|
|
/>
|
|
|
|
{/* Logo overlay top-left */}
|
|
<div className="relative z-10 px-10 pt-9">
|
|
<div className="flex items-center gap-2.5">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src="https://hexdive.com/_next/image?url=%2F2.png&w=256&q=75"
|
|
alt="HexDive"
|
|
className="h-7 w-auto object-contain"
|
|
/>
|
|
<span className="font-bold text-[15px] tracking-wide text-black">
|
|
HexaDistro
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Gradient at bottom */}
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{ background: 'linear-gradient(to top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.2) 40%, transparent 70%)' }}
|
|
/>
|
|
|
|
{/* Text content pinned to bottom-left */}
|
|
<div className="relative z-10 mt-auto px-12 pb-12">
|
|
<h1
|
|
className="font-bold leading-tight mb-3"
|
|
style={{ fontSize: 'clamp(28px, 3vw, 46px)', color: '#ffffff' }}
|
|
>
|
|
Your operations.<br />
|
|
<span
|
|
className="inline-block px-2.5 py-1 mt-1 rounded-sm"
|
|
style={{ backgroundColor: '#1A1033', color: '#ffffff' }}
|
|
>
|
|
Fully in control.
|
|
</span>
|
|
</h1>
|
|
<p style={{ color: 'rgba(255,255,255,0.72)', fontSize: 15, lineHeight: 1.65, maxWidth: 380 }}>
|
|
Distribution ERP built for teams that deliver right, every time.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Right panel: login card ── */}
|
|
<div className="flex-1 bg-white flex flex-col items-center justify-center px-6 py-10 sm:px-10">
|
|
|
|
{/* Mobile-only logo */}
|
|
<div className="flex items-center gap-2.5 mb-8 md:hidden">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src="https://hexdive.com/_next/image?url=%2F2.png&w=256&q=75"
|
|
alt="HexDive"
|
|
className="h-7 w-auto object-contain"
|
|
/>
|
|
<span className="font-bold text-[14px] tracking-widest uppercase text-gray-900">
|
|
HexaDistro
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
className="w-full rounded-2xl bg-white px-6 py-8 sm:px-10 sm:py-11"
|
|
style={{
|
|
maxWidth: 440,
|
|
boxShadow: '0 8px 40px rgba(0,0,0,0.12)',
|
|
}}
|
|
>
|
|
{/* Logo — desktop only inside card */}
|
|
<div className="hidden md:flex items-center gap-2.5 mb-8">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src="https://hexdive.com/_next/image?url=%2F2.png&w=256&q=75"
|
|
alt="HexDive"
|
|
className="h-7 w-auto object-contain"
|
|
/>
|
|
<span className="font-bold text-[14px] tracking-widest uppercase text-gray-900">
|
|
HexaDistro
|
|
</span>
|
|
</div>
|
|
|
|
{/* Heading */}
|
|
<h1 className="text-3xl font-semibold text-gray-900 mb-1">Sign in</h1>
|
|
<p className="text-sm text-gray-500 mb-8">
|
|
Enter your credentials to access your account.
|
|
</p>
|
|
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="identifier" className="text-gray-700">
|
|
Username or Email
|
|
</Label>
|
|
<Input
|
|
id="identifier"
|
|
type="text"
|
|
autoComplete="username"
|
|
required
|
|
value={identifier}
|
|
onChange={e => setIdentifier(e.target.value)}
|
|
className="border-gray-300 bg-white text-gray-900 placeholder:text-gray-400"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="password" className="text-gray-700">
|
|
Password
|
|
</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="password"
|
|
type={showPassword ? 'text' : 'password'}
|
|
autoComplete="current-password"
|
|
required
|
|
value={password}
|
|
onChange={e => setPassword(e.target.value)}
|
|
className="border-gray-300 bg-white text-gray-900 placeholder:text-gray-400 pr-10"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(v => !v)}
|
|
className="absolute inset-y-0 right-2.5 flex items-center text-gray-400 hover:text-gray-600"
|
|
tabIndex={-1}
|
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
>
|
|
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="text-sm text-destructive">{error}</p>
|
|
)}
|
|
|
|
<Button type="submit" disabled={submitting} className="w-full mt-2">
|
|
{submitting ? 'Signing in…' : 'Sign In'}
|
|
</Button>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
)
|
|
}
|