170 lines
6.5 KiB
TypeScript
170 lines
6.5 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useState } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { useLoginForm } from '../lib/useLoginForm';
|
|
import { useLoginModal, useRegisterModal } from '@/shared/zustand/auth';
|
|
import { useTranslations } from 'next-intl';
|
|
import { formatPhone, normalizeDigits } from '@/shared/lib/formatPhone';
|
|
import { MotionWrapper } from '@/shared/ui';
|
|
import PhonePrefix from '@/shared/ui/phonePrefix';
|
|
|
|
export function LoginForm() {
|
|
const [isFocused, setIsFocused] = useState(false);
|
|
const t = useTranslations('Auth.Login');
|
|
const tCommon = useTranslations('Common');
|
|
|
|
const { phone, setPhone, submit, error, loading } = useLoginForm();
|
|
const toggleLoginModal = useLoginModal((state) => state.toggleLoginModal);
|
|
const toggleRegisterModal = useRegisterModal(
|
|
(state) => state.toggleRegisterModal,
|
|
);
|
|
|
|
const handlePhoneChange = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setPhone(normalizeDigits(e.target.value));
|
|
},
|
|
[setPhone],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
<div
|
|
className="fixed inset-0 z-20 bg-black/40 backdrop-blur-sm"
|
|
onClick={toggleLoginModal}
|
|
/>
|
|
|
|
{/* Modal */}
|
|
<div className="fixed inset-0 z-30 flex items-center justify-center pointer-events-none px-6">
|
|
<MotionWrapper>
|
|
<div className="pointer-events-auto w-full max-w-sm rounded border border-stone-200 bg-white px-8 pb-8 pt-10 shadow-sm">
|
|
{/* Close */}
|
|
<div className="flex justify-end -mt-2 mb-4">
|
|
<button
|
|
onClick={toggleLoginModal}
|
|
className="flex h-7 w-7 items-center justify-center rounded-sm text-stone-400 transition-colors hover:bg-stone-100 hover:text-stone-700"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<p className="mb-1 text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-stone-400">
|
|
{t('welcome')}
|
|
</p>
|
|
<h1 className="mb-1 font-serif text-3xl leading-tight text-stone-900">
|
|
{t('title')}
|
|
</h1>
|
|
<p className="mb-8 text-sm text-stone-400">{t('description')}</p>
|
|
|
|
<form onSubmit={submit} noValidate className="flex flex-col gap-5">
|
|
{/* Phone field */}
|
|
<div className="flex flex-col gap-1.5">
|
|
<label
|
|
htmlFor="phone"
|
|
className="text-[0.7rem] font-medium tracking-widest uppercase text-stone-500"
|
|
>
|
|
{t('phoneLabel')}
|
|
</label>
|
|
|
|
<div
|
|
className={`relative transition-transform duration-200 ${isFocused ? 'scale-[1.01]' : ''}`}
|
|
>
|
|
<PhonePrefix isFocused={isFocused} />
|
|
<input
|
|
id="phone"
|
|
type="tel"
|
|
placeholder={t('phonePlaceholder')}
|
|
value={formatPhone(phone)}
|
|
onChange={handlePhoneChange}
|
|
onFocus={() => setIsFocused(true)}
|
|
onBlur={() => setIsFocused(false)}
|
|
className={`
|
|
w-full rounded-sm border bg-stone-50 py-2.5 pl-30 pr-3.5
|
|
text-[0.95rem] font-medium text-stone-900 outline-none
|
|
placeholder:text-stone-300 transition-all duration-150
|
|
${
|
|
error
|
|
? 'border-red-400 ring-2 ring-red-200/40'
|
|
: isFocused
|
|
? 'border-stone-400 ring-2 ring-stone-300/30'
|
|
: 'border-stone-200 hover:border-stone-300'
|
|
}
|
|
`}
|
|
/>
|
|
</div>
|
|
|
|
{/* Digit counter / complete hint */}
|
|
<div className="flex items-center justify-between px-0.5">
|
|
<span className="text-[0.7rem] text-stone-400">
|
|
{phone.length > 0 &&
|
|
t('digitsEntered', { count: phone.length })}
|
|
</span>
|
|
{phone.length === 9 && (
|
|
<span className="text-[0.7rem] font-medium text-emerald-600">
|
|
{tCommon('complete')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="rounded-sm border border-red-200 bg-red-50 px-3.5 py-2.5 text-[0.8rem] text-red-500">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Submit */}
|
|
<button
|
|
type="submit"
|
|
disabled={loading || phone.length !== 9}
|
|
className="
|
|
mt-1 w-full rounded-sm bg-stone-900 py-3
|
|
text-[0.82rem] font-semibold uppercase tracking-widest text-stone-100
|
|
transition-all duration-150
|
|
hover:bg-stone-800 active:scale-[0.99]
|
|
disabled:cursor-not-allowed disabled:opacity-40
|
|
"
|
|
>
|
|
{loading ? (
|
|
<span className="flex items-center justify-center gap-2">
|
|
<span className="h-3.5 w-3.5 rounded-full border-2 border-stone-500 border-t-stone-100 animate-spin" />
|
|
{t('sending')}
|
|
</span>
|
|
) : (
|
|
t('sendCode')
|
|
)}
|
|
</button>
|
|
|
|
{/* Divider */}
|
|
<div className="relative flex items-center gap-3 py-1">
|
|
<span className="h-px flex-1 bg-stone-200" />
|
|
<span className="text-[0.65rem] font-medium uppercase tracking-widest text-stone-400">
|
|
{tCommon('or')}
|
|
</span>
|
|
<span className="h-px flex-1 bg-stone-200" />
|
|
</div>
|
|
|
|
{/* Register hint */}
|
|
<p className="text-center text-[0.78rem] text-stone-400 flex items-center justify-center gap-2">
|
|
{t('registerPrompt')}
|
|
<p
|
|
onClick={() => {
|
|
toggleLoginModal();
|
|
toggleRegisterModal();
|
|
}}
|
|
className="text-stone-800 hover:cursor-pointer underline underline-offset-2 hover:text-stone-600 transition-colors"
|
|
>
|
|
{t('registerLink')}
|
|
</p>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</MotionWrapper>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|