Редизайн доски и дизайн-система кнопок
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

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>