codex/broker-accounts-page #34
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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',
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
|
||||||
};
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user