Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ logs/
*.pem
*.key
secrets/
*-credentials.json
*-service-account.json
auron-ecommerce-*.json

# ============================================================
# TEST
Expand Down
138 changes: 138 additions & 0 deletions frontend/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { Metadata } from 'next'

export const metadata: Metadata = {
title: { absolute: 'Auron' },
}

const DARK = 'oklch(0.085 0.01 50)'
const DARK_SURFACE = 'oklch(0.11 0.01 55)'
const GOLD = 'oklch(0.75 0.12 85)'
const TEXT = 'oklch(0.95 0.01 70)'
const MUTED = 'oklch(0.42 0.02 70)'

export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<div
style={{ minHeight: '100svh', display: 'flex', background: DARK_SURFACE }}
>
{/* ── Left panel — brand identity ─────────────────────── */}
<div
className="hidden lg:flex lg:w-[46%] xl:w-[42%] relative overflow-hidden flex-col justify-between"
style={{
background: DARK,
padding: '3rem',
backgroundImage: `
linear-gradient(oklch(0.75 0.12 85 / 0.05) 1px, transparent 1px),
linear-gradient(90deg, oklch(0.75 0.12 85 / 0.05) 1px, transparent 1px)
`,
backgroundSize: '56px 56px',
}}
>
{/* Top — logo mark */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: GOLD,
boxShadow: `0 0 12px ${GOLD}`,
}}
/>
<span
style={{
fontFamily: 'var(--font-cormorant)',
fontSize: '0.95rem',
letterSpacing: '0.35em',
textTransform: 'uppercase',
color: TEXT,
fontWeight: 500,
}}
>
Auron
</span>
</div>

{/* Centre — large brand display */}
<div style={{ position: 'relative' }}>
<h1
style={{
fontFamily: 'var(--font-cormorant)',
fontSize: 'clamp(5rem, 10vw, 8.5rem)',
fontWeight: 300,
lineHeight: 0.88,
letterSpacing: '-0.02em',
color: TEXT,
margin: 0,
}}
>
Where
<br />
<span style={{ color: GOLD }}>excel</span>
<br />
lence
<br />
meets
<br />
<em style={{ fontStyle: 'italic', color: MUTED }}>elegance</em>
</h1>

{/* Decorative vertical line */}
<div
style={{
position: 'absolute',
left: '-2rem',
top: 0,
bottom: 0,
width: '1px',
background: `linear-gradient(to bottom, transparent, ${GOLD}, transparent)`,
}}
/>
</div>

{/* Bottom — tagline + accent */}
<div>
<div style={{ width: '2.5rem', height: '1px', background: GOLD, marginBottom: '1rem' }} />
<p
style={{
fontSize: '0.65rem',
letterSpacing: '0.3em',
textTransform: 'uppercase',
color: MUTED,
}}
>
Premium E-Commerce
</p>
</div>

{/* Corner accent — subtle glow */}
<div
style={{
position: 'absolute',
bottom: '-6rem',
right: '-6rem',
width: '20rem',
height: '20rem',
borderRadius: '50%',
background: `radial-gradient(circle, oklch(0.75 0.12 85 / 0.06) 0%, transparent 70%)`,
pointerEvents: 'none',
}}
/>
</div>

{/* ── Right panel — form container ────────────────────── */}
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem',
background: DARK_SURFACE,
}}
>
{children}
</div>
</div>
)
}
243 changes: 243 additions & 0 deletions frontend/app/(auth)/login/login-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { loginSchema, type LoginFormValues } from '@/lib/validations/auth'
import { Form, FormField, FormItem, FormControl, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
import Link from 'next/link'

const GOLD = 'oklch(0.75 0.12 85)'
const TEXT = 'oklch(0.95 0.01 70)'
const MUTED = 'oklch(0.42 0.02 70)'
const LABEL = 'oklch(0.52 0.02 70)'

const fadeUp = (delay = 0): React.CSSProperties => ({
animation: 'auron-fade-up 0.55s ease-out both',
animationDelay: `${delay}ms`,
})

export function LoginForm() {
const router = useRouter()
const searchParams = useSearchParams()
const callbackUrl = searchParams.get('callbackUrl') ?? '/'
const [isPending, setIsPending] = useState(false)
const [error, setError] = useState<string | null>(null)

const form = useForm<LoginFormValues>({
resolver: zodResolver(loginSchema),
defaultValues: { email: '', password: '' },
})

async function onSubmit(values: LoginFormValues) {
setIsPending(true)
setError(null)
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
setError(
data.message ?? data.error ??
(res.status === 401 ? 'Invalid email or password' : 'Login failed')
)
return
}
router.push(callbackUrl)
router.refresh()
} catch {
setError('Something went wrong. Please try again.')
} finally {
setIsPending(false)
}
}

return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
style={{
width: '100%',
maxWidth: '22rem',
display: 'flex',
flexDirection: 'column',
gap: '1.75rem',
// Override shadcn token values for gold focus
'--ring': GOLD,
'--input': 'oklch(0.75 0.12 85 / 0.18)',
'--border': 'oklch(0.75 0.12 85 / 0.22)',
'--foreground': TEXT,
'--muted-foreground': MUTED,
} as React.CSSProperties}
>
{/* Header */}
<div style={fadeUp(0)}>
<p
style={{
fontSize: '0.65rem',
letterSpacing: '0.25em',
textTransform: 'uppercase',
color: LABEL,
marginBottom: '0.6rem',
}}
>
Welcome back
</p>
<h2
style={{
fontFamily: 'var(--font-cormorant)',
fontSize: '2.4rem',
fontWeight: 300,
lineHeight: 1.1,
color: TEXT,
margin: 0,
}}
>
Sign in to Auron
</h2>
</div>

{/* Email */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem style={fadeUp(80)}>
<label
style={{
display: 'block',
fontSize: '0.65rem',
letterSpacing: '0.2em',
textTransform: 'uppercase',
color: LABEL,
marginBottom: '0.5rem',
}}
>
Email address
</label>
<FormControl>
<Input
type="email"
placeholder="you@example.com"
autoComplete="email"
className="bg-transparent"
style={{ color: TEXT }}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

{/* Password */}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem style={fadeUp(160)}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '0.5rem',
}}
>
<label
style={{
fontSize: '0.65rem',
letterSpacing: '0.2em',
textTransform: 'uppercase',
color: LABEL,
}}
>
Password
</label>
<span
style={{
fontSize: '0.7rem',
color: GOLD,
cursor: 'pointer',
letterSpacing: '0.05em',
}}
>
Forgot?
</span>
</div>
<FormControl>
<Input
type="password"
placeholder="••••••••"
autoComplete="current-password"
className="bg-transparent"
style={{ color: TEXT }}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

{/* Error message */}
{error && (
<p
style={{
fontSize: '0.78rem',
color: 'oklch(0.68 0.2 25)',
animation: 'auron-fade-up 0.3s ease-out both',
margin: '-0.5rem 0',
}}
>
{error}
</p>
)}

{/* Submit */}
<div style={fadeUp(240)}>
<button
type="submit"
disabled={isPending}
style={{
width: '100%',
height: '42px',
background: isPending ? 'oklch(0.65 0.09 85)' : GOLD,
color: 'oklch(0.09 0.01 55)',
fontSize: '0.7rem',
fontWeight: 600,
letterSpacing: '0.2em',
textTransform: 'uppercase',
border: 'none',
borderRadius: '3px',
cursor: isPending ? 'not-allowed' : 'pointer',
transition: 'background 0.2s, opacity 0.2s',
opacity: isPending ? 0.7 : 1,
}}
>
{isPending ? 'Signing in…' : 'Sign In'}
</button>
</div>

{/* Register link */}
<p
style={{
textAlign: 'center',
fontSize: '0.73rem',
color: LABEL,
...fadeUp(320),
}}
>
Don&apos;t have an account?{' '}
<Link href="/register" style={{ color: GOLD, textDecoration: 'none' }}>
Create one
</Link>
</p>
</form>
</Form>
)
}
Loading
Loading