codex/broker-accounts-page #34

Merged
ksv741 merged 19 commits from codex/broker-accounts-page into main 2026-06-21 20:57:47 +03:00
5 changed files with 126 additions and 268 deletions
Showing only changes of commit 62a3389bfb - Show all commits

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; 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 { Routes, Route } from 'react-router-dom';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw'; import { http, HttpResponse } from 'msw';
@ -72,6 +72,8 @@ describe('LoginPage', () => {
await user.type(screen.getByPlaceholderText('••••••••'), 'password'); await user.type(screen.getByPlaceholderText('••••••••'), 'password');
await user.click(screen.getByRole('button', { name: 'Войти' })); await user.click(screen.getByRole('button', { name: 'Войти' }));
expect(await screen.findByText('Вход...')).toBeInTheDocument(); await waitFor(() => {
expect(screen.getByRole('button', { name: 'Войти' })).toBeDisabled();
});
}); });
}); });

View File

@ -1,5 +1,7 @@
import { useState, type FormEvent } from 'react'; import { useState, type FormEvent } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; 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'; import { useSession } from '@/entities/session';
export function LoginPage() { export function LoginPage() {
@ -28,84 +30,44 @@ export function LoginPage() {
} }
return ( return (
<div style={{ maxWidth: 400, margin: '60px auto' }}> <Box maxWidth={400} mx="auto" mt={7.5}>
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 700 }}>Вход</h1> <Heading level={1} size="title">
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> Вход
{error && <div style={{ color: 'var(--color-negative)', fontSize: 14 }}>{error}</div>} </Heading>
<div> <Box
<label component="form"
style={{ onSubmit={handleSubmit}
display: 'block', sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 3 }}
marginBottom: 4,
fontSize: 14,
color: 'var(--color-text-secondary)',
}}
> >
Email {error && <Text tone="negative">{error}</Text>}
</label> <TextField
<input label="Email"
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
style={inputStyle}
placeholder="email@example.com" placeholder="email@example.com"
/> />
</div> <TextField
<div> label="Пароль"
<label
style={{
display: 'block',
marginBottom: 4,
fontSize: 14,
color: 'var(--color-text-secondary)',
}}
>
Пароль
</label>
<input
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
style={inputStyle}
placeholder="••••••••" placeholder="••••••••"
/> />
</div> <Button type="submit" loading={loading}>
<button type="submit" disabled={loading} style={buttonStyle}> Войти
{loading ? 'Вход...' : 'Войти'} </Button>
</button> <Text tone="secondary" style={{ textAlign: 'center' }}>
<p style={{ textAlign: 'center', fontSize: 14, color: 'var(--color-text-secondary)' }}>
Нет аккаунта?{' '} Нет аккаунта?{' '}
<Link <Link
to={`/register${redirect !== '/' ? `?redirect=${encodeURIComponent(redirect)}` : ''}`} to={`/register${redirect !== '/' ? `?redirect=${encodeURIComponent(redirect)}` : ''}`}
style={{ color: 'var(--color-primary)' }}
> >
Зарегистрироваться Зарегистрироваться
</Link> </Link>
</p> </Text>
</form> </Box>
</div> </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',
};

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; 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 userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw'; import { http, HttpResponse } from 'msw';
import { server } from '@/shared/lib/test/server'; import { server } from '@/shared/lib/test/server';
@ -57,6 +57,8 @@ describe('ProfilePage', () => {
await user.type(input, 'New Name'); await user.type(input, 'New Name');
await user.click(screen.getByRole('button', { name: 'Сохранить' })); await user.click(screen.getByRole('button', { name: 'Сохранить' }));
expect(await screen.findByText('Сохранение...')).toBeInTheDocument(); await waitFor(() => {
expect(screen.getByRole('button', { name: 'Сохранить' })).toBeDisabled();
});
}); });
}); });

View File

@ -1,4 +1,6 @@
import { useState, type FormEvent } from 'react'; 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'; import { useSession } from '@/entities/session';
export function ProfilePage() { export function ProfilePage() {
@ -24,82 +26,37 @@ export function ProfilePage() {
if (!user) return null; if (!user) return null;
return ( return (
<div style={{ maxWidth: 500, margin: '40px auto' }}> <Box maxWidth={500} mx="auto" mt={5}>
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 700 }}>Профиль</h1> <Heading level={1} size="title">
<div Профиль
style={{ </Heading>
background: 'var(--color-surface)', <Surface elevation="sm" padding="md">
borderRadius: 'var(--border-radius)', <Box sx={{ mb: 2 }}>
boxShadow: 'var(--shadow)', <Text variant="label" tone="secondary">
padding: 24, Почта
}} </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 }}
> >
<div style={{ marginBottom: 16 }}> <TextField label="Имя" value={name} onChange={(e) => setName(e.target.value)} />
<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>
{message && ( {message && (
<div <Text tone={message === 'Профиль обновлён' ? 'positive' : 'negative'}>{message}</Text>
style={{
fontSize: 14,
color:
message === 'Профиль обновлён'
? 'var(--color-positive)'
: 'var(--color-negative)',
}}
>
{message}
</div>
)} )}
<button <Button type="submit" loading={saving} variant="primary">
type="submit" Сохранить
disabled={saving} </Button>
style={{ </Box>
padding: '10px 20px', </Surface>
background: 'var(--color-primary)', </Box>
color: '#fff',
border: 'none',
borderRadius: 'var(--border-radius)',
fontSize: 14,
fontWeight: 600,
cursor: 'pointer',
alignSelf: 'flex-start',
}}
>
{saving ? 'Сохранение...' : 'Сохранить'}
</button>
</form>
</div>
</div>
); );
} }

View File

@ -1,5 +1,7 @@
import { useState, type FormEvent } from 'react'; import { useState, type FormEvent } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; 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'; import { useSession } from '@/entities/session';
export function RegisterPage() { export function RegisterPage() {
@ -36,124 +38,57 @@ export function RegisterPage() {
} }
return ( return (
<div style={{ maxWidth: 400, margin: '60px auto' }}> <Box maxWidth={400} mx="auto" mt={7.5}>
<h1 style={{ marginBottom: 24, fontSize: 24, fontWeight: 700 }}>Регистрация</h1> <Heading level={1} size="title">
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> Регистрация
{error && <div style={{ color: 'var(--color-negative)', fontSize: 14 }}>{error}</div>} </Heading>
<div> <Box
<label component="form"
style={{ onSubmit={handleSubmit}
display: 'block', sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 3 }}
marginBottom: 4,
fontSize: 14,
color: 'var(--color-text-secondary)',
}}
> >
Имя (необязательно) {error && <Text tone="negative">{error}</Text>}
</label> <TextField
<input label="Имя (необязательно)"
type="text"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
style={inputStyle}
placeholder="Иван Иванов" placeholder="Иван Иванов"
/> />
</div> <TextField
<div> label="Email"
<label
style={{
display: 'block',
marginBottom: 4,
fontSize: 14,
color: 'var(--color-text-secondary)',
}}
>
Email
</label>
<input
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
style={inputStyle}
placeholder="email@example.com" placeholder="email@example.com"
/> />
</div> <TextField
<div> label="Пароль"
<label
style={{
display: 'block',
marginBottom: 4,
fontSize: 14,
color: 'var(--color-text-secondary)',
}}
>
Пароль
</label>
<input
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
minLength={6}
style={inputStyle}
placeholder="Минимум 6 символов" placeholder="Минимум 6 символов"
slotProps={{ htmlInput: { minLength: 6 } }}
/> />
</div> <TextField
<div> label="Подтверждение пароля"
<label
style={{
display: 'block',
marginBottom: 4,
fontSize: 14,
color: 'var(--color-text-secondary)',
}}
>
Подтверждение пароля
</label>
<input
type="password" type="password"
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
required required
style={inputStyle}
placeholder="Повторите пароль" placeholder="Повторите пароль"
/> />
</div> <Button type="submit" loading={loading}>
<button type="submit" disabled={loading} style={buttonStyle}> Зарегистрироваться
{loading ? 'Регистрация...' : 'Зарегистрироваться'} </Button>
</button> <Text tone="secondary" style={{ textAlign: 'center' }}>
<p style={{ textAlign: 'center', fontSize: 14, color: 'var(--color-text-secondary)' }}>
Уже есть аккаунт?{' '} Уже есть аккаунт?{' '}
<Link <Link to={`/login${redirect !== '/' ? `?redirect=${encodeURIComponent(redirect)}` : ''}`}>
to={`/login${redirect !== '/' ? `?redirect=${encodeURIComponent(redirect)}` : ''}`}
style={{ color: 'var(--color-primary)' }}
>
Войти Войти
</Link> </Link>
</p> </Text>
</form> </Box>
</div> </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',
};