Редизайн доски и дизайн-система кнопок
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
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 { 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>
|
||||
|
||||
Reference in New Issue
Block a user