Files
play-life/play-life-web/src/components/BoardForm.jsx
poignatov f9928c6470
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m0s
Доски желаний и политика награждения
2026-01-13 22:35:01 +03:00

280 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react'
import { useAuth } from './auth/AuthContext'
import BoardMembers from './BoardMembers'
import Toast from './Toast'
import './BoardForm.css'
function BoardForm({ boardId, onNavigate, onSaved }) {
const { authFetch } = useAuth()
const [name, setName] = useState('')
const [inviteEnabled, setInviteEnabled] = useState(false)
const [inviteURL, setInviteURL] = useState('')
const [loading, setLoading] = useState(false)
const [loadingBoard, setLoadingBoard] = useState(false)
const [copied, setCopied] = useState(false)
const [toastMessage, setToastMessage] = useState(null)
const isEdit = !!boardId
useEffect(() => {
if (boardId) {
fetchBoard()
}
}, [boardId])
const fetchBoard = async () => {
setLoadingBoard(true)
try {
const res = await authFetch(`/api/wishlist/boards/${boardId}`)
if (res.ok) {
const data = await res.json()
setName(data.name)
setInviteEnabled(data.invite_enabled)
setInviteURL(data.invite_url || '')
} else {
setToastMessage({ text: 'Ошибка загрузки доски', type: 'error' })
}
} catch (err) {
setToastMessage({ text: 'Ошибка загрузки', type: 'error' })
} finally {
setLoadingBoard(false)
}
}
const handleSave = async () => {
if (!name.trim()) {
setToastMessage({ text: 'Введите название доски', type: 'error' })
return
}
setLoading(true)
try {
const url = boardId
? `/api/wishlist/boards/${boardId}`
: '/api/wishlist/boards'
const res = await authFetch(url, {
method: boardId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name.trim(),
invite_enabled: inviteEnabled
})
})
if (res.ok) {
const data = await res.json()
if (data.invite_url) {
setInviteURL(data.invite_url)
}
onSaved?.()
if (!boardId) {
// При создании возвращаемся назад
onNavigate('wishlist', { boardId: data.id })
} else {
// При редактировании возвращаемся на доску
onNavigate('wishlist', { boardId: boardId })
}
} else {
const err = await res.json()
setToastMessage({ text: err.error || 'Ошибка сохранения', type: 'error' })
}
} catch (err) {
setToastMessage({ text: 'Ошибка сохранения', type: 'error' })
} finally {
setLoading(false)
}
}
const handleRegenerateLink = async () => {
try {
const res = await authFetch(`/api/wishlist/boards/${boardId}/regenerate-invite`, {
method: 'POST'
})
if (res.ok) {
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' })
}
}
const handleCopyLink = () => {
navigator.clipboard.writeText(inviteURL)
setCopied(true)
setToastMessage({ text: 'Ссылка скопирована', type: 'success' })
setTimeout(() => setCopied(false), 2000)
}
const handleToggleInvite = async (enabled) => {
setInviteEnabled(enabled)
if (boardId && enabled && !inviteURL) {
// Автоматически генерируем ссылку при включении
await handleRegenerateLink()
} else if (boardId) {
// Просто обновляем статус
try {
await authFetch(`/api/wishlist/boards/${boardId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ invite_enabled: enabled })
})
} catch (err) {
console.error('Error updating invite status:', err)
}
}
}
const handleDelete = async () => {
if (!window.confirm('Удалить доску? Все желания на ней будут удалены.')) return
try {
const res = await authFetch(`/api/wishlist/boards/${boardId}`, {
method: 'DELETE'
})
if (res.ok) {
onSaved?.()
// Передаём флаг, что доска удалена, чтобы Wishlist выбрал первую доступную
onNavigate('wishlist', { boardDeleted: true })
} else {
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
}
} catch (err) {
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
}
}
const handleClose = () => {
onNavigate('wishlist')
}
if (loadingBoard) {
return (
<div className="board-form">
<div className="fixed inset-0 bottom-20 flex justify-center items-center">
<div className="flex flex-col items-center">
<div className="w-12 h-12 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mb-4"></div>
<div className="text-gray-600 font-medium">Загрузка...</div>
</div>
</div>
</div>
)
}
return (
<div className="board-form">
<button className="close-x-button" onClick={handleClose}>
</button>
<h2>{isEdit ? 'Настройки доски' : 'Новая доска'}</h2>
<div className="form-card">
<div className="form-group">
<label htmlFor="board-name">Название</label>
<input
id="board-name"
type="text"
className="form-input"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Название доски"
/>
</div>
{isEdit && (
<>
{/* Настройки доступа */}
<div className="form-section">
<h3>Доступ по ссылке</h3>
<label className="toggle-field">
<input
type="checkbox"
checked={inviteEnabled}
onChange={e => handleToggleInvite(e.target.checked)}
/>
<span className="toggle-slider"></span>
<span className="toggle-label">Разрешить присоединение по ссылке</span>
</label>
{inviteEnabled && inviteURL && (
<div className="invite-link-section">
<div className="invite-url-row">
<input
type="text"
className="invite-url-input"
value={inviteURL}
readOnly
/>
<button
className="copy-btn"
onClick={handleCopyLink}
title="Копировать ссылку"
>
{copied ? '✓' : '📋'}
</button>
</div>
<button
className="regenerate-btn"
onClick={handleRegenerateLink}
>
🔄 Перегенерировать ссылку
</button>
<p className="invite-hint">
Пользователь, открывший ссылку, сможет присоединиться к доске
</p>
</div>
)}
</div>
{/* Список участников */}
<BoardMembers
boardId={boardId}
onMemberRemoved={() => {
setToastMessage({ text: 'Участник удалён', type: 'success' })
}}
/>
</>
)}
<div className="form-actions">
<button className="cancel-button" onClick={handleClose}>
Отмена
</button>
<button
className="submit-button"
onClick={handleSave}
disabled={loading || !name.trim()}
>
{loading ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
</div>
{isEdit && (
<button className="delete-board-btn" onClick={handleDelete}>
🗑 Удалить доску
</button>
)}
{toastMessage && (
<Toast
message={toastMessage.text}
type={toastMessage.type}
onClose={() => setToastMessage(null)}
/>
)}
</div>
)
}
export default BoardForm