feat(frontend): migrate LoginPage, RegisterPage, ProfilePage to design system

- LoginPage: <input>/<label> → <TextField>, <button> → <Button>,
  <h1> → <Heading>, inline error → <Text tone="negative">
- RegisterPage: same pattern, 4 fields, minLength via slotProps.htmlInput
- ProfilePage: <div> inline styles → <Surface>, <input> → <TextField>,
  <button> → <Button>, email/role → <Text>+<Text variant="label">
- Tests: loading state checks updated from text change to
  toBeDisabled() (DS Button keeps text, shows spinner)
- 264 lines removed, 134 net reduction
This commit is contained in:
Sergey Krylov 2026-06-21 17:23:04 +03:00
parent a8741cfeab
commit 62a3389bfb
5 changed files with 126 additions and 268 deletions

View File

@ -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();
});
});
});

View File

@ -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',
};

View File

@ -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();
});
});
});

View File

@ -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>
);
}

View File

@ -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',
};