Redesign login UI, add theme toggle, refactor user update
- Redesigned login page with two-panel layout and branding - Added HexaDistro logo and theme toggle to sidebar/header - Refactored user update: roles via update endpoint, removed assignRole API/controller - Extended UpdateUserRequest for username, role, password changes - UserService.UpdateAsync syncs changes with AuthHex, with rollback - Stock balances client dedupes by itemId-locationId - Middleware matcher excludes static assets from auth checks - Improved localStorage clearing for user context - Added lucide-react for icons - Minor UI/code cleanups for consistency and UX
This commit is contained in:
@@ -1,25 +1,29 @@
|
||||
'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 { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
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 [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('/')
|
||||
}
|
||||
if (!loading && user) router.replace('/')
|
||||
}, [user, loading, router])
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
@@ -36,47 +40,158 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-svh items-center justify-center bg-background">
|
||||
<form onSubmit={handleSubmit} className="flex w-full max-w-sm flex-col gap-6 p-6">
|
||||
<div className="flex flex-col gap-1 text-center">
|
||||
<h1 className="text-2xl font-semibold text-foreground">Sign in</h1>
|
||||
<p className="text-sm text-muted-foreground">Distribution ERP</p>
|
||||
<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>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="identifier">Email</Label>
|
||||
<Input
|
||||
id="identifier"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
required
|
||||
value={identifier}
|
||||
onChange={(e) => setIdentifier(e.target.value)}
|
||||
/>
|
||||
</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%)' }}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" disabled={submitting} className="w-full">
|
||||
{submitting ? 'Signing in…' : 'Sign in'}
|
||||
</Button>
|
||||
{/* 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>
|
||||
</form>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user