Редизайн доски и дизайн-система кнопок
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 50s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 50s
This commit is contained in:
184
IMPACT_ANALYSIS.md
Normal file
184
IMPACT_ANALYSIS.md
Normal 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. **Тестирование**: Протестировать все затронутые экраны после развертывания
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "play-life-web",
|
"name": "play-life-web",
|
||||||
"version": "3.25.7",
|
"version": "3.26.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -125,46 +125,8 @@
|
|||||||
background: #4f46e5;
|
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 {
|
.invite-hint {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #6b7280;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
import BoardMembers from './BoardMembers'
|
import BoardMembers from './BoardMembers'
|
||||||
import Toast from './Toast'
|
import Toast from './Toast'
|
||||||
|
import SubmitButton from './SubmitButton'
|
||||||
|
import DeleteButton from './DeleteButton'
|
||||||
|
import './Buttons.css'
|
||||||
import './BoardForm.css'
|
import './BoardForm.css'
|
||||||
|
|
||||||
function BoardForm({ boardId, onNavigate, onSaved }) {
|
function BoardForm({ boardId, onNavigate, onSaved }) {
|
||||||
@@ -11,6 +14,7 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
|
|||||||
const [inviteURL, setInviteURL] = useState('')
|
const [inviteURL, setInviteURL] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [loadingBoard, setLoadingBoard] = useState(false)
|
const [loadingBoard, setLoadingBoard] = useState(false)
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [toastMessage, setToastMessage] = useState(null)
|
const [toastMessage, setToastMessage] = useState(null)
|
||||||
|
|
||||||
@@ -86,7 +90,8 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRegenerateLink = async () => {
|
// Функция для автоматической генерации ссылки при включении доступа
|
||||||
|
const generateInviteLink = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/wishlist/boards/${boardId}/regenerate-invite`, {
|
const res = await authFetch(`/api/wishlist/boards/${boardId}/regenerate-invite`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
@@ -95,12 +100,9 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setInviteURL(data.invite_url)
|
setInviteURL(data.invite_url)
|
||||||
setInviteEnabled(true)
|
setInviteEnabled(true)
|
||||||
setToastMessage({ text: 'Ссылка обновлена', type: 'success' })
|
|
||||||
} else {
|
|
||||||
setToastMessage({ text: 'Ошибка обновления ссылки', type: 'error' })
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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) {
|
if (boardId && enabled && !inviteURL) {
|
||||||
// Автоматически генерируем ссылку при включении
|
// Автоматически генерируем ссылку при включении
|
||||||
await handleRegenerateLink()
|
await generateInviteLink()
|
||||||
} else if (boardId) {
|
} else if (boardId) {
|
||||||
// Просто обновляем статус
|
// Просто обновляем статус
|
||||||
try {
|
try {
|
||||||
@@ -134,6 +136,7 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!window.confirm('Удалить доску? Все желания на ней будут удалены.')) return
|
if (!window.confirm('Удалить доску? Все желания на ней будут удалены.')) return
|
||||||
|
|
||||||
|
setIsDeleting(true)
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/wishlist/boards/${boardId}`, {
|
const res = await authFetch(`/api/wishlist/boards/${boardId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
@@ -144,9 +147,11 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
|
|||||||
onNavigate('wishlist', { boardDeleted: true })
|
onNavigate('wishlist', { boardDeleted: true })
|
||||||
} else {
|
} else {
|
||||||
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
|
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
|
||||||
|
setIsDeleting(false)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
|
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
|
||||||
|
setIsDeleting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,15 +223,18 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
|
|||||||
onClick={handleCopyLink}
|
onClick={handleCopyLink}
|
||||||
title="Копировать ссылку"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
className="regenerate-btn"
|
|
||||||
onClick={handleRegenerateLink}
|
|
||||||
>
|
|
||||||
🔄 Перегенерировать ссылку
|
|
||||||
</button>
|
|
||||||
<p className="invite-hint">
|
<p className="invite-hint">
|
||||||
Пользователь, открывший ссылку, сможет присоединиться к доске
|
Пользователь, открывший ссылку, сможет присоединиться к доске
|
||||||
</p>
|
</p>
|
||||||
@@ -245,25 +253,24 @@ function BoardForm({ boardId, onNavigate, onSaved }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
<button className="cancel-button" onClick={handleClose}>
|
<SubmitButton
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="submit-button"
|
|
||||||
onClick={handleSave}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEdit && (
|
|
||||||
<button className="delete-board-btn" onClick={handleDelete}>
|
|
||||||
🗑 Удалить доску
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{toastMessage && (
|
{toastMessage && (
|
||||||
<Toast
|
<Toast
|
||||||
message={toastMessage.text}
|
message={toastMessage.text}
|
||||||
|
|||||||
56
play-life-web/src/components/Buttons.css
Normal file
56
play-life-web/src/components/Buttons.css
Normal 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;
|
||||||
|
}
|
||||||
29
play-life-web/src/components/DeleteButton.jsx
Normal file
29
play-life-web/src/components/DeleteButton.jsx
Normal 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
|
||||||
20
play-life-web/src/components/SubmitButton.jsx
Normal file
20
play-life-web/src/components/SubmitButton.jsx
Normal 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
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import { useAuth } from './auth/AuthContext'
|
import { useAuth } from './auth/AuthContext'
|
||||||
import Toast from './Toast'
|
import Toast from './Toast'
|
||||||
|
import SubmitButton from './SubmitButton'
|
||||||
|
import DeleteButton from './DeleteButton'
|
||||||
import './TaskForm.css'
|
import './TaskForm.css'
|
||||||
|
|
||||||
const API_URL = '/api/tasks'
|
const API_URL = '/api/tasks'
|
||||||
@@ -1168,29 +1170,20 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
<button type="submit" disabled={loading || isDeleting} className="submit-button">
|
<SubmitButton
|
||||||
{loading ? 'Сохранение...' : 'Сохранить'}
|
type="submit"
|
||||||
</button>
|
loading={loading}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</SubmitButton>
|
||||||
{taskId && (
|
{taskId && (
|
||||||
<button
|
<DeleteButton
|
||||||
type="button"
|
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="delete-button"
|
loading={isDeleting}
|
||||||
disabled={isDeleting || loading}
|
disabled={loading}
|
||||||
title="Удалить задачу"
|
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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user