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, {error && <Text tone="negative">{error}</Text>}
color: 'var(--color-text-secondary)', <TextField
}} label="Email"
> type="email"
Email value={email}
</label> onChange={(e) => setEmail(e.target.value)}
<input required
type="email" placeholder="email@example.com"
value={email} />
onChange={(e) => setEmail(e.target.value)} <TextField
required label="Пароль"
style={inputStyle} type="password"
placeholder="email@example.com" value={password}
/> onChange={(e) => setPassword(e.target.value)}
</div> required
<div> placeholder="••••••••"
<label />
style={{ <Button type="submit" loading={loading}>
display: 'block', Войти
marginBottom: 4, </Button>
fontSize: 14, <Text tone="secondary" style={{ textAlign: 'center' }}>
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)' }}>
Нет аккаунта?{' '} Нет аккаунта?{' '}
<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>
<div style={{ marginBottom: 16 }}> </Box>
<span style={{ fontSize: 14, color: 'var(--color-text-secondary)' }}>Почта</span> <Box sx={{ mb: 2 }}>
<p style={{ fontSize: 16, fontWeight: 500 }}>{user.email}</p> <Text variant="label" tone="secondary">
</div> Роль
<div style={{ marginBottom: 16 }}> </Text>
<span style={{ fontSize: 14, color: 'var(--color-text-secondary)' }}>Роль</span> <Text>{user.role}</Text>
<p style={{ fontSize: 16, fontWeight: 500 }}>{user.role}</p> </Box>
</div> <Box
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> component="form"
<div> onSubmit={handleSubmit}
<label sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}
style={{ >
display: 'block', <TextField label="Имя" value={name} onChange={(e) => setName(e.target.value)} />
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, {error && <Text tone="negative">{error}</Text>}
color: 'var(--color-text-secondary)', <TextField
}} label="Имя (необязательно)"
> value={name}
Имя (необязательно) onChange={(e) => setName(e.target.value)}
</label> placeholder="Иван Иванов"
<input />
type="text" <TextField
value={name} label="Email"
onChange={(e) => setName(e.target.value)} type="email"
style={inputStyle} value={email}
placeholder="Иван Иванов" onChange={(e) => setEmail(e.target.value)}
/> required
</div> placeholder="email@example.com"
<div> />
<label <TextField
style={{ label="Пароль"
display: 'block', type="password"
marginBottom: 4, value={password}
fontSize: 14, onChange={(e) => setPassword(e.target.value)}
color: 'var(--color-text-secondary)', required
}} placeholder="Минимум 6 символов"
> slotProps={{ htmlInput: { minLength: 6 } }}
Email />
</label> <TextField
<input label="Подтверждение пароля"
type="email" type="password"
value={email} value={confirmPassword}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
required required
style={inputStyle} placeholder="Повторите пароль"
placeholder="email@example.com" />
/> <Button type="submit" loading={loading}>
</div> Зарегистрироваться
<div> </Button>
<label <Text tone="secondary" style={{ textAlign: 'center' }}>
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)' }}>
Уже есть аккаунт?{' '} Уже есть аккаунт?{' '}
<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',
};