All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m22s
279 lines
8.9 KiB
JavaScript
279 lines
8.9 KiB
JavaScript
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
|