🚀 MVP: FastAPI + React форма с SMS верификацией

 Инфраструктура: PostgreSQL, Redis, RabbitMQ, S3
 Backend: SMS сервис + API endpoints
 Frontend: React форма (3 шага) + SMS верификация
This commit is contained in:
AI Assistant
2025-10-24 16:19:58 +03:00
parent 8af23e90fa
commit 0f82eef08d
42 changed files with 2902 additions and 241 deletions

View File

@@ -0,0 +1,199 @@
import { useState } from 'react';
import { Form, Input, Button, message, Space } from 'antd';
import { PhoneOutlined, SafetyOutlined, FileProtectOutlined } from '@ant-design/icons';
interface Props {
formData: any;
updateFormData: (data: any) => void;
onNext: () => void;
isPhoneVerified: boolean;
setIsPhoneVerified: (verified: boolean) => void;
}
export default function Step1Phone({ formData, updateFormData, onNext, isPhoneVerified, setIsPhoneVerified }: Props) {
const [form] = Form.useForm();
const [codeSent, setCodeSent] = useState(false);
const [loading, setLoading] = useState(false);
const [verifyLoading, setVerifyLoading] = useState(false);
const sendCode = async () => {
try {
const phone = form.getFieldValue('phone');
if (!phone) {
message.error('Введите номер телефона');
return;
}
setLoading(true);
const response = await fetch('http://147.45.146.17:8100/api/v1/sms/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone }),
});
const result = await response.json();
if (response.ok) {
message.success('Код отправлен на ваш телефон');
setCodeSent(true);
if (result.debug_code) {
message.info(`DEBUG: Код ${result.debug_code}`);
}
} else {
message.error(result.detail || 'Ошибка отправки кода');
}
} catch (error) {
message.error('Ошибка соединения с сервером');
} finally {
setLoading(false);
}
};
const verifyCode = async () => {
try {
const phone = form.getFieldValue('phone');
const code = form.getFieldValue('smsCode');
if (!code) {
message.error('Введите код из SMS');
return;
}
setVerifyLoading(true);
const response = await fetch('http://147.45.146.17:8100/api/v1/sms/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, code }),
});
const result = await response.json();
if (response.ok) {
message.success('Телефон подтвержден!');
setIsPhoneVerified(true);
} else {
message.error(result.detail || 'Неверный код');
}
} catch (error) {
message.error('Ошибка соединения с сервером');
} finally {
setVerifyLoading(false);
}
};
const handleNext = async () => {
try {
const values = await form.validateFields();
updateFormData(values);
onNext();
} catch (error) {
message.error('Заполните все обязательные поля');
}
};
return (
<Form
form={form}
layout="vertical"
initialValues={formData}
style={{ marginTop: 24 }}
>
<Form.Item
label="Номер телефона"
name="phone"
rules={[
{ required: true, message: 'Введите номер телефона' },
{ pattern: /^\+7\d{10}$/, message: 'Формат: +79001234567' }
]}
>
<Input
prefix={<PhoneOutlined />}
placeholder="+79001234567"
disabled={isPhoneVerified}
maxLength={12}
/>
</Form.Item>
{!isPhoneVerified && (
<>
<Form.Item>
<Button
type="primary"
onClick={sendCode}
loading={loading}
disabled={codeSent}
>
{codeSent ? 'Код отправлен' : 'Отправить код'}
</Button>
</Form.Item>
{codeSent && (
<Form.Item
label="Код из SMS"
name="smsCode"
rules={[{ required: true, message: 'Введите код' }, { len: 6, message: '6 цифр' }]}
>
<Space.Compact style={{ width: '100%' }}>
<Input
prefix={<SafetyOutlined />}
placeholder="123456"
maxLength={6}
style={{ width: '70%' }}
/>
<Button
type="primary"
onClick={verifyCode}
loading={verifyLoading}
style={{ width: '30%' }}
>
Проверить
</Button>
</Space.Compact>
</Form.Item>
)}
</>
)}
{isPhoneVerified && (
<>
<Form.Item
label="Email (необязательно)"
name="email"
rules={[{ type: 'email', message: 'Неверный формат email' }]}
>
<Input placeholder="example@mail.ru" />
</Form.Item>
<Form.Item
label="ИНН (необязательно)"
name="inn"
>
<Input placeholder="1234567890" maxLength={12} />
</Form.Item>
<Form.Item
label="Номер полиса"
name="policyNumber"
rules={[{ required: true, message: 'Введите номер полиса' }]}
>
<Input prefix={<FileProtectOutlined />} placeholder="123456789" />
</Form.Item>
<Form.Item
label="Серия полиса (необязательно)"
name="policySeries"
>
<Input placeholder="AB" />
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handleNext} size="large" block>
Далее
</Button>
</Form.Item>
</>
)}
</Form>
);
}