Files
microdao-daarion/apps/web/src/app/(auth)/login/page.tsx
Apple 6bd769ef40 feat(city-map): Add 2D City Map with coordinates and agent presence
- Add migration 013_city_map_coordinates.sql with map coordinates, zones, and agents table
- Add /city/map API endpoint in city-service
- Add /city/agents and /city/agents/online endpoints
- Extend presence aggregator to include agents[] in snapshot
- Add AgentsSource for fetching agent data from DB
- Create CityMap component with interactive room tiles
- Add useCityMap hook for fetching map data
- Update useGlobalPresence to include agents
- Add map/list view toggle on /city page
- Add agent badges to room cards and map tiles
2025-11-27 07:00:47 -08:00

206 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { Mail, Lock, Loader2, Sparkles, AlertCircle } from 'lucide-react'
import { useAuth } from '@/context/AuthContext'
import { login as authLogin } from '@/lib/auth'
import { cn } from '@/lib/utils'
export default function LoginPage() {
const router = useRouter()
const { refreshUser, isAuthenticated } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
// Redirect if already authenticated (client-side effect to avoid rendering push)
useEffect(() => {
if (isAuthenticated) {
router.push('/')
}
}, [isAuthenticated, router])
if (isAuthenticated) {
return null
}
const validateForm = (currentEmail: string, currentPassword: string): string | null => {
if (!currentEmail.trim()) {
return 'Введіть email адресу'
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(currentEmail)) {
return 'Введіть коректну email адресу'
}
if (!currentPassword) {
return 'Введіть пароль'
}
if (currentPassword.length < 8) {
return 'Пароль повинен містити мінімум 8 символів'
}
return null
}
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setError('')
const formData = new FormData(e.currentTarget)
const fallbackEmail = (formData.get('email')?.toString() ?? '').trim()
const fallbackPassword = formData.get('password')?.toString() ?? ''
const currentEmail = email.trim() || fallbackEmail
const currentPassword = password || fallbackPassword
if (!email && currentEmail) setEmail(currentEmail)
if (!password && currentPassword) setPassword(currentPassword)
const validationError = validateForm(currentEmail, currentPassword)
if (validationError) {
setError(validationError)
return
}
setLoading(true)
try {
await authLogin(currentEmail, currentPassword)
await refreshUser()
router.push('/')
} catch (err) {
setError(err instanceof Error ? err.message : 'Помилка входу. Перевірте дані та спробуйте ще раз.')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center px-4 py-12">
<div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<Link href="/" className="inline-flex items-center gap-2">
<Sparkles className="w-10 h-10 text-cyan-400" />
<span className="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-blue-500 bg-clip-text text-transparent">
DAARION
</span>
</Link>
<h1 className="mt-6 text-2xl font-bold text-white">Вхід в акаунт</h1>
<p className="mt-2 text-slate-400">
Увійдіть, щоб продовжити у DAARION.city
</p>
</div>
{/* Form */}
<div className="glass-panel p-8">
<form onSubmit={handleSubmit} noValidate className="space-y-6">
{/* Error */}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/20 rounded-xl text-red-400 text-sm">
<AlertCircle className="w-4 h-4 flex-shrink-0" />
{error}
</div>
)}
{/* Email */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-300 mb-2">
Email
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<input
id="email"
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
autoComplete="email"
className={cn(
'w-full pl-10 pr-4 py-3 bg-slate-800/50 border border-white/10 rounded-xl',
'text-white placeholder-slate-500',
'focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20',
'transition-all'
)}
/>
</div>
</div>
{/* Password */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-300 mb-2">
Пароль
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<input
id="password"
name="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
autoComplete="current-password"
className={cn(
'w-full pl-10 pr-4 py-3 bg-slate-800/50 border border-white/10 rounded-xl',
'text-white placeholder-slate-500',
'focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/20',
'transition-all'
)}
/>
</div>
</div>
{/* Submit */}
<button
type="submit"
disabled={loading}
className={cn(
'w-full py-3 rounded-xl font-medium transition-all',
'bg-gradient-to-r from-cyan-500 to-blue-600 text-white',
'hover:from-cyan-400 hover:to-blue-500',
'shadow-lg shadow-cyan-500/20 hover:shadow-cyan-500/40',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
{loading ? (
<Loader2 className="w-5 h-5 mx-auto animate-spin" />
) : (
'Увійти'
)}
</button>
</form>
{/* Divider */}
<div className="my-6 flex items-center gap-4">
<div className="flex-1 h-px bg-white/10" />
<span className="text-sm text-slate-500">або</span>
<div className="flex-1 h-px bg-white/10" />
</div>
{/* Register link */}
<p className="text-center text-slate-400">
Немає акаунту?{' '}
<Link href="/register" className="text-cyan-400 hover:text-cyan-300 font-medium">
Зареєструватися
</Link>
</p>
</div>
{/* Back to home */}
<p className="mt-6 text-center">
<Link href="/" className="text-slate-500 hover:text-slate-400 text-sm">
Повернутися на головну
</Link>
</p>
</div>
</div>
)
}