Редизайн доски и дизайн-система кнопок
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 50s

This commit is contained in:
poignatov
2026-01-22 19:21:23 +03:00
parent e7ef6caa41
commit e823312f0e
9 changed files with 339 additions and 88 deletions

184
IMPACT_ANALYSIS.md Normal file
View File

@@ -0,0 +1,184 @@
# Импакт-анализ: Редизайн экрана редактирования доски желаний
## Дата анализа
2025-01-21
## Созданные компоненты (дизайн-система)
### 1. `SubmitButton.jsx`
- **Путь**: `play-life-web/src/components/SubmitButton.jsx`
- **Назначение**: Переиспользуемый компонент кнопки сохранения с градиентным фоном
- **Пропсы**: `loading`, `disabled`, `children`, `onClick`, `type`
- **Стили**: Градиент от #6366f1 до #8b5cf6, hover эффект с тенью
- **Использование**: Заменяет все кнопки сохранения в формах
### 2. `DeleteButton.jsx`
- **Путь**: `play-life-web/src/components/DeleteButton.jsx`
- **Назначение**: Переиспользуемый компонент кнопки удаления с красным фоном и иконкой корзины
- **Пропсы**: `loading`, `disabled`, `onClick`, `title`
- **Стили**: Красный фон #ef4444, квадратная кнопка 44x44px
- **Использование**: Заменяет все кнопки удаления в формах
### 3. `Buttons.css`
- **Путь**: `play-life-web/src/components/Buttons.css`
- **Назначение**: Общие стили для кнопок дизайн-системы
- **Содержимое**:
- `.form-actions` - flex-контейнер для группировки кнопок
- `.submit-button` - стили для кнопки сохранения
- `.delete-button` - стили для кнопки удаления
## Измененные компоненты
### 1. `BoardForm.jsx`
**Путь**: `play-life-web/src/components/BoardForm.jsx`
**Изменения**:
- ✅ Заменена эмодзи копирования (📋) на SVG иконку в кнопке копирования ссылки
- ✅ Удалена кнопка "Отмена" из блока `form-actions`
- ✅ Кнопка удаления перемещена в блок `form-actions` справа от кнопки "Сохранить"
- ✅ Добавлено состояние `isDeleting` для отслеживания процесса удаления
- ✅ Удалена кнопка "Перегенерить ссылку"
- ✅ Удалена функция `handleRegenerateLink` (заменена на `generateInviteLink` для внутреннего использования)
- ✅ Интегрированы компоненты `SubmitButton` и `DeleteButton`
- ✅ Добавлен импорт `Buttons.css`
**Затронутые места в компоненте**:
- Строки 1-5: Добавлены импорты новых компонентов и стилей
- Строка 14: Добавлено состояние `isDeleting`
- Строки 89-105: Удалена функция `handleRegenerateLink`
- Строки 114-132: Обновлена функция `handleToggleInvite` (использует `generateInviteLink`)
- Строки 134-151: Обновлена функция `handleDelete` (добавлено состояние `isDeleting`)
- Строки 216-222: Заменена эмодзи на SVG иконку копирования
- Строки 224-229: Удалена кнопка "Перегенерить ссылку"
- Строки 247-258: Обновлен блок `form-actions` (удалена кнопка "Отмена", добавлены новые компоненты)
- Строки 261-265: Удален отдельный блок с кнопкой удаления
### 2. `BoardForm.css`
**Путь**: `play-life-web/src/components/BoardForm.css`
**Изменения**:
- ✅ Удалены стили `.regenerate-btn` (строки 128-143)
- ✅ Удалены стили `.delete-board-btn` (строки 152-169)
- ✅ Стили кнопок теперь импортируются из `Buttons.css`
**Затронутые места**:
- Удалено 42 строки неиспользуемых стилей
### 3. `TaskForm.jsx`
**Путь**: `play-life-web/src/components/TaskForm.jsx`
**Изменения**:
- ✅ Интегрированы компоненты `SubmitButton` и `DeleteButton`
- ✅ Добавлен импорт `Buttons.css` (через компоненты)
- ✅ Заменены нативные кнопки на компоненты дизайн-системы
**Затронутые места в компоненте**:
- Строки 1-4: Добавлены импорты новых компонентов
- Строки 1170-1195: Заменены кнопки на компоненты `SubmitButton` и `DeleteButton`
## Затронутые экраны
### 1. Экран редактирования доски желаний (`board-form`)
**Компонент**: `BoardForm`
**Навигация**: Открывается из экрана списка желаний (`wishlist`) при нажатии на кнопку редактирования доски
**Изменения в UI**:
- ✅ Кнопка копирования ссылки: эмодзи 📋 заменена на SVG иконку (два перекрывающихся прямоугольника)
- ✅ При успешном копировании показывается SVG иконка галочки вместо текста ✓
- ✅ Удалена кнопка "Отмена" - теперь закрытие происходит только через крестик в правом верхнем углу
- ✅ Кнопка "Удалить доску" перемещена в блок действий справа от кнопки "Сохранить"
- ✅ Кнопка удаления теперь имеет красный фон и иконку корзины (как в экране редактирования задачи)
- ✅ Удалена кнопка "Перегенерить ссылку" - ссылка теперь генерируется автоматически при включении доступа
- ✅ Кнопка "Сохранить" имеет градиентный фон и hover эффект (как в экране редактирования задачи)
**Функциональные изменения**:
- Ссылка для приглашения теперь генерируется автоматически при включении переключателя "Разрешить присоединение по ссылке"
- Кнопка удаления показывает состояние загрузки (три точки) во время удаления
- Кнопка сохранения показывает "Сохранение..." во время процесса сохранения
**Путь навигации**:
- `wishlist``board-form` (при нажатии на кнопку редактирования доски)
### 2. Экран редактирования задачи (`task-form`)
**Компонент**: `TaskForm`
**Навигация**: Открывается из списка задач (`tasks`) или из деталей желания (`wishlist-detail`)
**Изменения в UI**:
- ✅ Кнопки сохранения и удаления теперь используют компоненты дизайн-системы
- ✅ Визуально идентичны кнопкам на экране редактирования доски
**Функциональные изменения**:
- Нет функциональных изменений, только рефакторинг кода
**Путь навигации**:
- `tasks``task-form` (при создании/редактировании задачи)
- `wishlist-detail``task-form` (при создании задачи из желания)
## Потенциальные места для рефакторинга
Следующие компоненты используют похожие кнопки и могут быть обновлены для использования новых компонентов дизайн-системы:
### 1. `WishlistForm.jsx`
- **Текущее состояние**: Использует нативную кнопку с классом `submit-button`
- **Потенциал**: Можно заменить на `SubmitButton`
- **Расположение**: Строки 836-838, 1246-1248
### 2. `AddWords.jsx`
- **Текущее состояние**: Использует нативную кнопку с классом `submit-button`
- **Потенциал**: Можно заменить на `SubmitButton`
- **Расположение**: Строка 187
### 3. Другие формы
- Компоненты с кнопками удаления могут использовать `DeleteButton`
- Компоненты с кнопками сохранения могут использовать `SubmitButton`
## Файлы, созданные/измененные
### Созданные файлы:
1. `play-life-web/src/components/SubmitButton.jsx` (новый)
2. `play-life-web/src/components/DeleteButton.jsx` (новый)
3. `play-life-web/src/components/Buttons.css` (новый)
### Измененные файлы:
1. `play-life-web/src/components/BoardForm.jsx` (обновлен)
2. `play-life-web/src/components/BoardForm.css` (обновлен)
3. `play-life-web/src/components/TaskForm.jsx` (обновлен)
## Визуальные изменения
### До изменений:
- Эмодзи в кнопке копирования
- Кнопка "Отмена" в блоке действий
- Кнопка удаления отдельно внизу формы
- Кнопка "Перегенерить ссылку" под полем ссылки
- Разные стили кнопок в разных формах
### После изменений:
- SVG иконки в кнопке копирования
- Только кнопка "Сохранить" и "Удалить" в блоке действий
- Кнопка удаления справа от кнопки сохранения
- Автоматическая генерация ссылки
- Единый стиль кнопок во всех формах (дизайн-система)
## Технические детали
### Зависимости
- Новые компоненты не добавляют внешних зависимостей
- Используют только React и существующие стили
### Обратная совместимость
-Все изменения обратно совместимы
- ✅ Функциональность не нарушена
- ✅ API компонентов не изменился
### Производительность
- ✅ Нет влияния на производительность
- ✅ Компоненты легковесные
- ✅ Стили оптимизированы
## Рекомендации
1. **Рефакторинг других форм**: Рассмотреть возможность замены кнопок в `WishlistForm` и `AddWords` на компоненты дизайн-системы
2. **Расширение дизайн-системы**: Добавить другие типы кнопок (например, `CancelButton`, `IconButton`)
3. **Документация**: Создать документацию по использованию компонентов дизайн-системы
4. **Тестирование**: Протестировать все затронутые экраны после развертывания

View File

@@ -1 +1 @@
3.25.7
3.26.0

View File

@@ -1,6 +1,6 @@
{
"name": "play-life-web",
"version": "3.25.7",
"version": "3.26.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -125,46 +125,8 @@
background: #4f46e5;
}
.regenerate-btn {
width: 100%;
padding: 10px;
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 0.9rem;
color: #374151;
cursor: pointer;
transition: all 0.2s;
}
.regenerate-btn:hover {
background: #e5e7eb;
border-color: #9ca3af;
}
.invite-hint {
margin-top: 8px;
font-size: 0.85rem;
color: #6b7280;
}
/* Delete button */
.delete-board-btn {
display: block;
width: 100%;
margin-top: 1.5rem;
padding: 1rem;
background: transparent;
border: 1px solid #fecaca;
border-radius: 8px;
color: #ef4444;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.delete-board-btn:hover {
background: #fef2f2;
border-color: #ef4444;
}

View File

@@ -2,6 +2,9 @@ import React, { useState, useEffect } from 'react'
import { useAuth } from './auth/AuthContext'
import BoardMembers from './BoardMembers'
import Toast from './Toast'
import SubmitButton from './SubmitButton'
import DeleteButton from './DeleteButton'
import './Buttons.css'
import './BoardForm.css'
function BoardForm({ boardId, onNavigate, onSaved }) {
@@ -11,6 +14,7 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
const [inviteURL, setInviteURL] = useState('')
const [loading, setLoading] = useState(false)
const [loadingBoard, setLoadingBoard] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [copied, setCopied] = useState(false)
const [toastMessage, setToastMessage] = useState(null)
@@ -86,7 +90,8 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
}
}
const handleRegenerateLink = async () => {
// Функция для автоматической генерации ссылки при включении доступа
const generateInviteLink = async () => {
try {
const res = await authFetch(`/api/wishlist/boards/${boardId}/regenerate-invite`, {
method: 'POST'
@@ -95,12 +100,9 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
const data = await res.json()
setInviteURL(data.invite_url)
setInviteEnabled(true)
setToastMessage({ text: 'Ссылка обновлена', type: 'success' })
} else {
setToastMessage({ text: 'Ошибка обновления ссылки', type: 'error' })
}
} catch (err) {
setToastMessage({ text: 'Ошибка', type: 'error' })
console.error('Error generating invite link:', err)
}
}
@@ -116,7 +118,7 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
if (boardId && enabled && !inviteURL) {
// Автоматически генерируем ссылку при включении
await handleRegenerateLink()
await generateInviteLink()
} else if (boardId) {
// Просто обновляем статус
try {
@@ -134,6 +136,7 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
const handleDelete = async () => {
if (!window.confirm('Удалить доску? Все желания на ней будут удалены.')) return
setIsDeleting(true)
try {
const res = await authFetch(`/api/wishlist/boards/${boardId}`, {
method: 'DELETE'
@@ -144,9 +147,11 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
onNavigate('wishlist', { boardDeleted: true })
} else {
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
setIsDeleting(false)
}
} catch (err) {
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
setIsDeleting(false)
}
}
@@ -218,15 +223,18 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
onClick={handleCopyLink}
title="Копировать ссылку"
>
{copied ? '✓' : '📋'}
{copied ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 6L9 17l-5-5"></path>
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
)}
</button>
</div>
<button
className="regenerate-btn"
onClick={handleRegenerateLink}
>
🔄 Перегенерировать ссылку
</button>
<p className="invite-hint">
Пользователь, открывший ссылку, сможет присоединиться к доске
</p>
@@ -245,25 +253,24 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
)}
<div className="form-actions">
<button className="cancel-button" onClick={handleClose}>
Отмена
</button>
<button
className="submit-button"
<SubmitButton
onClick={handleSave}
disabled={loading || !name.trim()}
loading={loading}
disabled={!name.trim()}
>
{loading ? 'Сохранение...' : 'Сохранить'}
</button>
Сохранить
</SubmitButton>
{isEdit && (
<DeleteButton
onClick={handleDelete}
loading={isDeleting}
disabled={loading}
title="Удалить доску"
/>
)}
</div>
</div>
{isEdit && (
<button className="delete-board-btn" onClick={handleDelete}>
🗑 Удалить доску
</button>
)}
{toastMessage && (
<Toast
message={toastMessage.text}

View File

@@ -0,0 +1,56 @@
.form-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid #e5e7eb;
}
.submit-button {
background: linear-gradient(to right, #6366f1, #8b5cf6);
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.375rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
flex: 1;
}
.submit-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.submit-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.delete-button {
background: #ef4444;
color: white;
padding: 0.75rem;
border: none;
border-radius: 0.375rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
width: 44px;
}
.delete-button:hover:not(:disabled) {
background: #dc2626;
}
.delete-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}

View File

@@ -0,0 +1,29 @@
import React from 'react'
import './Buttons.css'
function DeleteButton({ loading, disabled, onClick, title = 'Удалить', ...props }) {
return (
<button
type="button"
className="delete-button"
onClick={onClick}
disabled={disabled || loading}
title={title}
{...props}
>
{loading ? (
<span>...</span>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
)}
</button>
)
}
export default DeleteButton

View File

@@ -0,0 +1,20 @@
import React from 'react'
import './Buttons.css'
function SubmitButton({ loading, disabled, children, onClick, type = 'button', ...props }) {
const displayText = loading ? 'Сохранение...' : (children || 'Сохранить')
return (
<button
type={type}
className="submit-button"
onClick={onClick}
disabled={disabled || loading}
{...props}
>
{displayText}
</button>
)
}
export default SubmitButton

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect, useRef } from 'react'
import { useAuth } from './auth/AuthContext'
import Toast from './Toast'
import SubmitButton from './SubmitButton'
import DeleteButton from './DeleteButton'
import './TaskForm.css'
const API_URL = '/api/tasks'
@@ -1168,29 +1170,20 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
)}
<div className="form-actions">
<button type="submit" disabled={loading || isDeleting} className="submit-button">
{loading ? 'Сохранение...' : 'Сохранить'}
</button>
<SubmitButton
type="submit"
loading={loading}
disabled={isDeleting}
>
Сохранить
</SubmitButton>
{taskId && (
<button
type="button"
onClick={handleDelete}
className="delete-button"
disabled={isDeleting || loading}
<DeleteButton
onClick={handleDelete}
loading={isDeleting}
disabled={loading}
title="Удалить задачу"
>
{isDeleting ? (
<span>...</span>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
)}
</button>
/>
)}
</div>
</form>