Files
play-life/play-life-web/src/components/ShoppingBoardForm.jsx
poignatov 60fca2d93c
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m22s
6.4.0: Экран товаров (Shopping List)
2026-03-08 16:11:08 +03:00

279 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 SubmitButton from './SubmitButton'
import DeleteButton from './DeleteButton'
import './Buttons.css'
import './BoardForm.css'
function ShoppingBoardForm({ 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 [isDeleting, setIsDeleting] = 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/shopping/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/shopping/boards/${boardId}`
: '/api/shopping/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('shopping', { boardId: data.id })
} else {
onNavigate('shopping', { boardId: boardId })
}
} else {
const err = await res.json()
setToastMessage({ text: err.error || 'Ошибка сохранения', type: 'error' })
}
} catch (err) {
setToastMessage({ text: 'Ошибка сохранения', type: 'error' })
} finally {
setLoading(false)
}
}
const generateInviteLink = async () => {
try {
const res = await authFetch(`/api/shopping/boards/${boardId}/regenerate-invite`, {
method: 'POST'
})
if (res.ok) {
const data = await res.json()
setInviteURL(data.invite_url)
setInviteEnabled(true)
}
} catch (err) {
console.error('Error generating invite link:', err)
}
}
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 generateInviteLink()
} else if (boardId) {
try {
await authFetch(`/api/shopping/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
setIsDeleting(true)
try {
const res = await authFetch(`/api/shopping/boards/${boardId}`, {
method: 'DELETE'
})
if (res.ok) {
onSaved?.()
onNavigate('shopping', { boardDeleted: true })
} else {
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
setIsDeleting(false)
}
} catch (err) {
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
setIsDeleting(false)
}
}
const handleClose = () => {
window.history.back()
}
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 ? (
<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>
<p className="invite-hint">
Пользователь, открывший ссылку, сможет присоединиться к доске
</p>
</div>
)}
</div>
<BoardMembers
boardId={boardId}
apiBase="/api/shopping"
onMemberRemoved={() => {
setToastMessage({ text: 'Участник удалён', type: 'success' })
}}
/>
</>
)}
<div className="form-actions">
<SubmitButton
onClick={handleSave}
loading={loading}
disabled={!name.trim()}
>
Сохранить
</SubmitButton>
{isEdit && (
<DeleteButton
onClick={handleDelete}
loading={isDeleting}
disabled={loading}
title="Удалить доску"
/>
)}
</div>
</div>
{toastMessage && (
<Toast
message={toastMessage.text}
type={toastMessage.type}
onClose={() => setToastMessage(null)}
/>
)}
</div>
)
}
export default ShoppingBoardForm