codex/broker-accounts-page #34
@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
@ -72,6 +72,8 @@ describe('LoginPage', () => {
|
||||
await user.type(screen.getByPlaceholderText('••••••••'), 'password');
|
||||
await user.click(screen.getByRole('button', { name: 'Войти' }));
|
||||
|
||||
expect(await screen.findByText('Вход...')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Войти' })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Box } from '@mui/material';
|
||||
import { Button, Heading, Text, TextField } from '@moex-vibe/design-system';
|
||||
import { useSession } from '@/entities/session';
|
||||
|
||||
export function LoginPage() {
|
||||
@ -28,84 +30,44 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 400, margin: '60px auto' }}>
|
||||
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 700 }}>Вход</h1>
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{error && <div style={{ color: 'var(--color-negative)', fontSize: 14 }}>{error}</div>}
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: 4,
|
||||
fontSize: 14,
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
style={inputStyle}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: 4,
|
||||
fontSize: 14,
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
Пароль
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
style={inputStyle}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" disabled={loading} style={buttonStyle}>
|
||||
{loading ? 'Вход...' : 'Войти'}
|
||||
</button>
|
||||
<p style={{ textAlign: 'center', fontSize: 14, color: 'var(--color-text-secondary)' }}>
|
||||
<Box maxWidth={400} mx="auto" mt={7.5}>
|
||||
<Heading level={1} size="title">
|
||||
Вход
|
||||
</Heading>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 3 }}
|
||||
>
|
||||
{error && <Text tone="negative">{error}</Text>}
|
||||
<TextField
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<TextField
|
||||
label="Пароль"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<Button type="submit" loading={loading}>
|
||||
Войти
|
||||
</Button>
|
||||
<Text tone="secondary" style={{ textAlign: 'center' }}>
|
||||
Нет аккаунта?{' '}
|
||||
<Link
|
||||
to={`/register${redirect !== '/' ? `?redirect=${encodeURIComponent(redirect)}` : ''}`}
|
||||
style={{ color: 'var(--color-primary)' }}
|
||||
>
|
||||
Зарегистрироваться
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
fontSize: 16,
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
const buttonStyle: React.CSSProperties = {
|
||||
padding: '12px 24px',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '@/shared/lib/test/server';
|
||||
@ -57,6 +57,8 @@ describe('ProfilePage', () => {
|
||||
await user.type(input, 'New Name');
|
||||
await user.click(screen.getByRole('button', { name: 'Сохранить' }));
|
||||
|
||||
expect(await screen.findByText('Сохранение...')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Сохранить' })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { Button, Heading, Surface, Text, TextField } from '@moex-vibe/design-system';
|
||||
import { useSession } from '@/entities/session';
|
||||
|
||||
export function ProfilePage() {
|
||||
@ -24,82 +26,37 @@ export function ProfilePage() {
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 500, margin: '40px auto' }}>
|
||||
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 700 }}>Профиль</h1>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--color-surface)',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
boxShadow: 'var(--shadow)',
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ fontSize: 14, color: 'var(--color-text-secondary)' }}>Почта</span>
|
||||
<p style={{ fontSize: 16, fontWeight: 500 }}>{user.email}</p>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ fontSize: 14, color: 'var(--color-text-secondary)' }}>Роль</span>
|
||||
<p style={{ fontSize: 16, fontWeight: 500 }}>{user.role}</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: 4,
|
||||
fontSize: 14,
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
Имя
|
||||
</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
fontSize: 16,
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Box maxWidth={500} mx="auto" mt={5}>
|
||||
<Heading level={1} size="title">
|
||||
Профиль
|
||||
</Heading>
|
||||
<Surface elevation="sm" padding="md">
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Text variant="label" tone="secondary">
|
||||
Почта
|
||||
</Text>
|
||||
<Text>{user.email}</Text>
|
||||
</Box>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Text variant="label" tone="secondary">
|
||||
Роль
|
||||
</Text>
|
||||
<Text>{user.role}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
|
||||
>
|
||||
<TextField label="Имя" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color:
|
||||
message === 'Профиль обновлён'
|
||||
? 'var(--color-positive)'
|
||||
: 'var(--color-negative)',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
<Text tone={message === 'Профиль обновлён' ? 'positive' : 'negative'}>{message}</Text>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{saving ? 'Сохранение...' : 'Сохранить'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" loading={saving} variant="primary">
|
||||
Сохранить
|
||||
</Button>
|
||||
</Box>
|
||||
</Surface>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Box } from '@mui/material';
|
||||
import { Button, Heading, Text, TextField } from '@moex-vibe/design-system';
|
||||
import { useSession } from '@/entities/session';
|
||||
|
||||
export function RegisterPage() {
|
||||
@ -36,124 +38,57 @@ export function RegisterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 400, margin: '60px auto' }}>
|
||||
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 700 }}>Регистрация</h1>
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{error && <div style={{ color: 'var(--color-negative)', fontSize: 14 }}>{error}</div>}
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: 4,
|
||||
fontSize: 14,
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
Имя (необязательно)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
style={inputStyle}
|
||||
placeholder="Иван Иванов"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: 4,
|
||||
fontSize: 14,
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
style={inputStyle}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: 4,
|
||||
fontSize: 14,
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
Пароль
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
style={inputStyle}
|
||||
placeholder="Минимум 6 символов"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: 'block',
|
||||
marginBottom: 4,
|
||||
fontSize: 14,
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
>
|
||||
Подтверждение пароля
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
style={inputStyle}
|
||||
placeholder="Повторите пароль"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" disabled={loading} style={buttonStyle}>
|
||||
{loading ? 'Регистрация...' : 'Зарегистрироваться'}
|
||||
</button>
|
||||
<p style={{ textAlign: 'center', fontSize: 14, color: 'var(--color-text-secondary)' }}>
|
||||
<Box maxWidth={400} mx="auto" mt={7.5}>
|
||||
<Heading level={1} size="title">
|
||||
Регистрация
|
||||
</Heading>
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 3 }}
|
||||
>
|
||||
{error && <Text tone="negative">{error}</Text>}
|
||||
<TextField
|
||||
label="Имя (необязательно)"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Иван Иванов"
|
||||
/>
|
||||
<TextField
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<TextField
|
||||
label="Пароль"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
placeholder="Минимум 6 символов"
|
||||
slotProps={{ htmlInput: { minLength: 6 } }}
|
||||
/>
|
||||
<TextField
|
||||
label="Подтверждение пароля"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
placeholder="Повторите пароль"
|
||||
/>
|
||||
<Button type="submit" loading={loading}>
|
||||
Зарегистрироваться
|
||||
</Button>
|
||||
<Text tone="secondary" style={{ textAlign: 'center' }}>
|
||||
Уже есть аккаунт?{' '}
|
||||
<Link
|
||||
to={`/login${redirect !== '/' ? `?redirect=${encodeURIComponent(redirect)}` : ''}`}
|
||||
style={{ color: 'var(--color-primary)' }}
|
||||
>
|
||||
<Link to={`/login${redirect !== '/' ? `?redirect=${encodeURIComponent(redirect)}` : ''}`}>
|
||||
Войти
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
fontSize: 16,
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
const buttonStyle: React.CSSProperties = {
|
||||
padding: '12px 24px',
|
||||
background: 'var(--color-primary)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user