118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
'use client';
|
||
|
||
import * as React from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
import { DoorOpen, KeyRound } from 'lucide-react';
|
||
import { Button } from '@/components/ui/button';
|
||
import { PinInput } from '@/components/ui/pin-input';
|
||
import { isPinInputComplete } from '@/lib/pin-input';
|
||
|
||
interface PinLockModalProps {
|
||
open: boolean;
|
||
isSubmitting: boolean;
|
||
error?: string | null;
|
||
onSubmit: (pin: string) => Promise<void>;
|
||
onLogout?: () => void;
|
||
}
|
||
|
||
export function PinLockModal({ open, isSubmitting, error, onSubmit, onLogout }: PinLockModalProps) {
|
||
const [pin, setPin] = React.useState('');
|
||
const [mounted, setMounted] = React.useState(false);
|
||
|
||
React.useEffect(() => {
|
||
setMounted(true);
|
||
}, []);
|
||
|
||
React.useEffect(() => {
|
||
if (open) {
|
||
setPin('');
|
||
}
|
||
}, [open]);
|
||
|
||
React.useEffect(() => {
|
||
if (!open || typeof document === 'undefined') return;
|
||
const previousOverflow = document.body.style.overflow;
|
||
document.body.style.overflow = 'hidden';
|
||
return () => {
|
||
document.body.style.overflow = previousOverflow;
|
||
};
|
||
}, [open]);
|
||
|
||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
await onSubmit(pin);
|
||
}
|
||
|
||
if (!open || !mounted) return null;
|
||
|
||
return createPortal(
|
||
<div className="fixed inset-0 z-[6000]">
|
||
<div className="absolute inset-0 bg-black/50 backdrop-blur-[1px]" aria-hidden />
|
||
|
||
<div
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="pin-lock-title"
|
||
className="fixed left-1/2 w-[min(92vw,520px)] -translate-x-1/2 rounded-[28px] bg-white p-6 shadow-2xl"
|
||
style={{ top: 'max(1.5rem, 10vh)' }}
|
||
>
|
||
{onLogout ? (
|
||
<button
|
||
type="button"
|
||
onClick={onLogout}
|
||
disabled={isSubmitting}
|
||
title="Выйти из аккаунта"
|
||
aria-label="Выйти из аккаунта"
|
||
className="absolute left-5 top-5 flex h-9 w-9 items-center justify-center rounded-full text-[#6b6f7b] transition hover:bg-[#f4f5f8] hover:text-[#111] disabled:opacity-50"
|
||
>
|
||
<DoorOpen className="h-4 w-4" />
|
||
</button>
|
||
) : null}
|
||
|
||
<div className="mb-5 flex flex-col gap-1">
|
||
<div className="mx-auto mb-2 flex h-14 w-14 items-center justify-center rounded-full bg-[#f4f5f8]">
|
||
<KeyRound className="h-7 w-7 text-[#111]" />
|
||
</div>
|
||
<h2 id="pin-lock-title" className="text-center text-xl font-semibold">
|
||
Введите PIN-код
|
||
</h2>
|
||
<p className="text-center text-sm text-[#6b6f7b]">
|
||
Сессия заблокирована из-за бездействия. Подтвердите PIN, чтобы продолжить работу.
|
||
</p>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<PinInput
|
||
autoFocus
|
||
className="h-[58px] text-center text-lg tracking-[0.4em]"
|
||
placeholder="PIN"
|
||
value={pin}
|
||
onChange={setPin}
|
||
required
|
||
minLength={4}
|
||
maxLength={6}
|
||
/>
|
||
{error ? <p className="text-center text-sm text-red-600">{error}</p> : null}
|
||
<Button type="submit" size="lg" className="w-full rounded-[18px]" disabled={isSubmitting || !isPinInputComplete(pin)}>
|
||
{isSubmitting ? 'Проверяем...' : 'Разблокировать'}
|
||
</Button>
|
||
{onLogout ? (
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="lg"
|
||
className="w-full rounded-[18px] text-[#6b6f7b]"
|
||
disabled={isSubmitting}
|
||
onClick={onLogout}
|
||
>
|
||
<DoorOpen className="mr-2 h-4 w-4" />
|
||
Выйти из аккаунта
|
||
</Button>
|
||
) : null}
|
||
</form>
|
||
</div>
|
||
</div>,
|
||
document.body
|
||
);
|
||
}
|