6.25.0: Меню действий для досок вместо кнопок
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m11s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m11s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import BoardMembers from './BoardMembers'
|
||||
import Toast from './Toast'
|
||||
import DeleteButton from './DeleteButton'
|
||||
import './Buttons.css'
|
||||
import './BoardForm.css'
|
||||
import './Wishlist.css'
|
||||
|
||||
function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
const { authFetch } = useAuth()
|
||||
@@ -19,6 +19,9 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
const [toastMessage, setToastMessage] = useState(null)
|
||||
const [isOwner, setIsOwner] = useState(true)
|
||||
const [isArchived, setIsArchived] = useState(false)
|
||||
const [showActionMenu, setShowActionMenu] = useState(false)
|
||||
const actionMenuHistoryRef = useRef(false)
|
||||
const savedHistoryStateRef = useRef(null)
|
||||
|
||||
const isEdit = !!boardId
|
||||
|
||||
@@ -137,6 +140,17 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Навигация после действия из action menu: убрать обе записи (action menu + board-form)
|
||||
const navigateBackFromActionMenu = () => {
|
||||
setShowActionMenu(false)
|
||||
if (actionMenuHistoryRef.current) {
|
||||
actionMenuHistoryRef.current = false
|
||||
window.history.go(-2)
|
||||
} else {
|
||||
window.history.back()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!window.confirm('Удалить доску? Все желания на ней будут удалены.')) return
|
||||
|
||||
@@ -147,8 +161,7 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
})
|
||||
if (res.ok) {
|
||||
onSaved?.()
|
||||
// Передаём флаг, что доска удалена, чтобы Wishlist выбрал первую доступную
|
||||
onNavigate('wishlist', { boardDeleted: true })
|
||||
navigateBackFromActionMenu()
|
||||
} else {
|
||||
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
|
||||
setIsDeleting(false)
|
||||
@@ -168,7 +181,7 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
})
|
||||
if (res.ok) {
|
||||
onSaved?.()
|
||||
onNavigate('wishlist', { boardDeleted: true })
|
||||
onNavigate('wishlist', { boardDeleted: true }, { replace: true })
|
||||
} else {
|
||||
setToastMessage({ text: 'Ошибка выхода', type: 'error' })
|
||||
}
|
||||
@@ -186,7 +199,7 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
})
|
||||
if (res.ok) {
|
||||
onSaved?.()
|
||||
onNavigate('wishlist', { boardDeleted: true })
|
||||
navigateBackFromActionMenu()
|
||||
} else {
|
||||
setToastMessage({ text: 'Ошибка архивации', type: 'error' })
|
||||
}
|
||||
@@ -201,9 +214,8 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
method: 'POST'
|
||||
})
|
||||
if (res.ok) {
|
||||
setIsArchived(false)
|
||||
onSaved?.()
|
||||
setToastMessage({ text: 'Доска разархивирована', type: 'success' })
|
||||
navigateBackFromActionMenu()
|
||||
} else {
|
||||
setToastMessage({ text: 'Ошибка разархивации', type: 'error' })
|
||||
}
|
||||
@@ -212,6 +224,42 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
}
|
||||
}
|
||||
|
||||
const openActionMenu = () => {
|
||||
setShowActionMenu(true)
|
||||
savedHistoryStateRef.current = window.history.state
|
||||
window.history.pushState({ actionMenu: true }, '')
|
||||
actionMenuHistoryRef.current = true
|
||||
}
|
||||
|
||||
const closeActionMenu = () => {
|
||||
setShowActionMenu(false)
|
||||
if (actionMenuHistoryRef.current) {
|
||||
actionMenuHistoryRef.current = false
|
||||
window.history.back()
|
||||
}
|
||||
}
|
||||
|
||||
// Закрыть меню без popstate — заменяем запись в истории на сохранённое состояние
|
||||
const dismissActionMenu = () => {
|
||||
setShowActionMenu(false)
|
||||
if (actionMenuHistoryRef.current) {
|
||||
actionMenuHistoryRef.current = false
|
||||
window.history.replaceState(savedHistoryStateRef.current, '', window.location.href)
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка popstate для закрытия action menu кнопкой назад
|
||||
useEffect(() => {
|
||||
const handlePopState = () => {
|
||||
if (showActionMenu) {
|
||||
actionMenuHistoryRef.current = false
|
||||
setShowActionMenu(false)
|
||||
}
|
||||
}
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
return () => window.removeEventListener('popstate', handlePopState)
|
||||
}, [showActionMenu])
|
||||
|
||||
const handleClose = () => {
|
||||
window.history.back()
|
||||
}
|
||||
@@ -229,58 +277,6 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
)
|
||||
}
|
||||
|
||||
// Для не-владельца показываем упрощённую форму
|
||||
if (isEdit && !isOwner) {
|
||||
return (
|
||||
<div className="board-form">
|
||||
<button className="close-x-button" onClick={handleClose}>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h2>{name}</h2>
|
||||
|
||||
<div className="form-card">
|
||||
<div className="board-actions-list">
|
||||
{isArchived ? (
|
||||
<button className="board-action-button" onClick={handleUnarchive}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="1 4 1 10 7 10"></polyline>
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
|
||||
</svg>
|
||||
<span>Разархивировать</span>
|
||||
</button>
|
||||
) : (
|
||||
<button className="board-action-button" onClick={handleArchive}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="21 8 21 21 3 21 3 8"></polyline>
|
||||
<rect x="1" y="3" width="22" height="5"></rect>
|
||||
<line x1="10" y1="12" x2="14" y2="12"></line>
|
||||
</svg>
|
||||
<span>Архивировать</span>
|
||||
</button>
|
||||
)}
|
||||
<button className="board-action-button board-action-danger" onClick={handleLeave}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
<span>Покинуть доску</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{toastMessage && (
|
||||
<Toast
|
||||
message={toastMessage.text}
|
||||
type={toastMessage.type}
|
||||
onClose={() => setToastMessage(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="board-form">
|
||||
<button className="close-x-button" onClick={handleClose}>
|
||||
@@ -359,27 +355,6 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Архивирование */}
|
||||
<div className="form-section">
|
||||
{isArchived ? (
|
||||
<button className="board-action-button" onClick={handleUnarchive} style={{ marginTop: '0.5rem' }}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="1 4 1 10 7 10"></polyline>
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
|
||||
</svg>
|
||||
<span>Разархивировать</span>
|
||||
</button>
|
||||
) : (
|
||||
<button className="board-action-button" onClick={handleArchive} style={{ marginTop: '0.5rem' }}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="21 8 21 21 3 21 3 8"></polyline>
|
||||
<rect x="1" y="3" width="22" height="5"></rect>
|
||||
<line x1="10" y1="12" x2="14" y2="12"></line>
|
||||
</svg>
|
||||
<span>Архивировать</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -429,16 +404,61 @@ function BoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
{loading ? 'Сохранение...' : 'Сохранить'}
|
||||
</button>
|
||||
{isEdit && (
|
||||
<DeleteButton
|
||||
onClick={handleDelete}
|
||||
loading={isDeleting}
|
||||
disabled={loading}
|
||||
title="Удалить доску"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openActionMenu}
|
||||
disabled={loading || isDeleting}
|
||||
style={{
|
||||
width: '52px',
|
||||
height: '52px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'transparent',
|
||||
color: '#059669',
|
||||
border: '2px solid #059669',
|
||||
borderRadius: '0.5rem',
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 700,
|
||||
cursor: (loading || isDeleting) ? 'not-allowed' : 'pointer',
|
||||
lineHeight: 1,
|
||||
flexShrink: 0,
|
||||
padding: 0,
|
||||
boxSizing: 'border-box',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
title="Действия"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
) : null}
|
||||
{showActionMenu && createPortal(
|
||||
<div className="wishlist-modal-overlay" style={{ zIndex: 2000 }} onClick={closeActionMenu}>
|
||||
<div className="wishlist-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="wishlist-modal-header">
|
||||
<h3>{name}</h3>
|
||||
</div>
|
||||
<div className="wishlist-modal-actions">
|
||||
{isArchived ? (
|
||||
<button className="wishlist-modal-copy" onClick={handleUnarchive}>
|
||||
Разархивировать
|
||||
</button>
|
||||
) : (
|
||||
<button className="wishlist-modal-copy" onClick={handleArchive}>
|
||||
Архивировать
|
||||
</button>
|
||||
)}
|
||||
<button className="wishlist-modal-delete" onClick={handleDelete}>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import BoardMembers from './BoardMembers'
|
||||
import Toast from './Toast'
|
||||
import DeleteButton from './DeleteButton'
|
||||
import './Buttons.css'
|
||||
import './BoardForm.css'
|
||||
import './Wishlist.css'
|
||||
|
||||
function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
const { authFetch } = useAuth()
|
||||
@@ -19,6 +19,9 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
const [toastMessage, setToastMessage] = useState(null)
|
||||
const [isOwner, setIsOwner] = useState(true)
|
||||
const [isArchived, setIsArchived] = useState(false)
|
||||
const [showActionMenu, setShowActionMenu] = useState(false)
|
||||
const actionMenuHistoryRef = useRef(false)
|
||||
const savedHistoryStateRef = useRef(null)
|
||||
|
||||
const isEdit = !!boardId
|
||||
|
||||
@@ -132,6 +135,17 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Навигация после действия из action menu: убрать обе записи (action menu + board-form)
|
||||
const navigateBackFromActionMenu = () => {
|
||||
setShowActionMenu(false)
|
||||
if (actionMenuHistoryRef.current) {
|
||||
actionMenuHistoryRef.current = false
|
||||
window.history.go(-2)
|
||||
} else {
|
||||
window.history.back()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!window.confirm('Удалить доску? Все товары на ней будут удалены.')) return
|
||||
|
||||
@@ -142,7 +156,7 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
})
|
||||
if (res.ok) {
|
||||
onSaved?.()
|
||||
onNavigate('shopping', { boardDeleted: true })
|
||||
navigateBackFromActionMenu()
|
||||
} else {
|
||||
setToastMessage({ text: 'Ошибка удаления', type: 'error' })
|
||||
setIsDeleting(false)
|
||||
@@ -162,7 +176,7 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
})
|
||||
if (res.ok) {
|
||||
onSaved?.()
|
||||
onNavigate('shopping', { boardDeleted: true })
|
||||
onNavigate('shopping', { boardDeleted: true }, { replace: true })
|
||||
} else {
|
||||
setToastMessage({ text: 'Ошибка выхода', type: 'error' })
|
||||
}
|
||||
@@ -180,7 +194,7 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
})
|
||||
if (res.ok) {
|
||||
onSaved?.()
|
||||
onNavigate('shopping', { boardDeleted: true })
|
||||
navigateBackFromActionMenu()
|
||||
} else {
|
||||
setToastMessage({ text: 'Ошибка архивации', type: 'error' })
|
||||
}
|
||||
@@ -195,9 +209,8 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
method: 'POST'
|
||||
})
|
||||
if (res.ok) {
|
||||
setIsArchived(false)
|
||||
onSaved?.()
|
||||
setToastMessage({ text: 'Доска разархивирована', type: 'success' })
|
||||
navigateBackFromActionMenu()
|
||||
} else {
|
||||
setToastMessage({ text: 'Ошибка разархивации', type: 'error' })
|
||||
}
|
||||
@@ -206,6 +219,42 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
}
|
||||
}
|
||||
|
||||
const openActionMenu = () => {
|
||||
setShowActionMenu(true)
|
||||
savedHistoryStateRef.current = window.history.state
|
||||
window.history.pushState({ actionMenu: true }, '')
|
||||
actionMenuHistoryRef.current = true
|
||||
}
|
||||
|
||||
const closeActionMenu = () => {
|
||||
setShowActionMenu(false)
|
||||
if (actionMenuHistoryRef.current) {
|
||||
actionMenuHistoryRef.current = false
|
||||
window.history.back()
|
||||
}
|
||||
}
|
||||
|
||||
// Закрыть меню без popstate — заменяем запись в истории на сохранённое состояние
|
||||
const dismissActionMenu = () => {
|
||||
setShowActionMenu(false)
|
||||
if (actionMenuHistoryRef.current) {
|
||||
actionMenuHistoryRef.current = false
|
||||
window.history.replaceState(savedHistoryStateRef.current, '', window.location.href)
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка popstate для закрытия action menu кнопкой назад
|
||||
useEffect(() => {
|
||||
const handlePopState = () => {
|
||||
if (showActionMenu) {
|
||||
actionMenuHistoryRef.current = false
|
||||
setShowActionMenu(false)
|
||||
}
|
||||
}
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
return () => window.removeEventListener('popstate', handlePopState)
|
||||
}, [showActionMenu])
|
||||
|
||||
const handleClose = () => {
|
||||
window.history.back()
|
||||
}
|
||||
@@ -223,58 +272,6 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
)
|
||||
}
|
||||
|
||||
// Для не-владельца показываем упрощённую форму
|
||||
if (isEdit && !isOwner) {
|
||||
return (
|
||||
<div className="board-form">
|
||||
<button className="close-x-button" onClick={handleClose}>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<h2>{name}</h2>
|
||||
|
||||
<div className="form-card">
|
||||
<div className="board-actions-list">
|
||||
{isArchived ? (
|
||||
<button className="board-action-button" onClick={handleUnarchive}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="1 4 1 10 7 10"></polyline>
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
|
||||
</svg>
|
||||
<span>Разархивировать</span>
|
||||
</button>
|
||||
) : (
|
||||
<button className="board-action-button" onClick={handleArchive}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="21 8 21 21 3 21 3 8"></polyline>
|
||||
<rect x="1" y="3" width="22" height="5"></rect>
|
||||
<line x1="10" y1="12" x2="14" y2="12"></line>
|
||||
</svg>
|
||||
<span>Архивировать</span>
|
||||
</button>
|
||||
)}
|
||||
<button className="board-action-button board-action-danger" onClick={handleLeave}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
<span>Покинуть доску</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{toastMessage && (
|
||||
<Toast
|
||||
message={toastMessage.text}
|
||||
type={toastMessage.type}
|
||||
onClose={() => setToastMessage(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="board-form">
|
||||
<button className="close-x-button" onClick={handleClose}>
|
||||
@@ -352,27 +349,6 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Архивирование */}
|
||||
<div className="form-section">
|
||||
{isArchived ? (
|
||||
<button className="board-action-button" onClick={handleUnarchive} style={{ marginTop: '0.5rem' }}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="1 4 1 10 7 10"></polyline>
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
|
||||
</svg>
|
||||
<span>Разархивировать</span>
|
||||
</button>
|
||||
) : (
|
||||
<button className="board-action-button" onClick={handleArchive} style={{ marginTop: '0.5rem' }}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="21 8 21 21 3 21 3 8"></polyline>
|
||||
<rect x="1" y="3" width="22" height="5"></rect>
|
||||
<line x1="10" y1="12" x2="14" y2="12"></line>
|
||||
</svg>
|
||||
<span>Архивировать</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -422,16 +398,61 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
|
||||
{loading ? 'Сохранение...' : 'Сохранить'}
|
||||
</button>
|
||||
{isEdit && (
|
||||
<DeleteButton
|
||||
onClick={handleDelete}
|
||||
loading={isDeleting}
|
||||
disabled={loading}
|
||||
title="Удалить доску"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openActionMenu}
|
||||
disabled={loading || isDeleting}
|
||||
style={{
|
||||
width: '52px',
|
||||
height: '52px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'transparent',
|
||||
color: '#059669',
|
||||
border: '2px solid #059669',
|
||||
borderRadius: '0.5rem',
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 700,
|
||||
cursor: (loading || isDeleting) ? 'not-allowed' : 'pointer',
|
||||
lineHeight: 1,
|
||||
flexShrink: 0,
|
||||
padding: 0,
|
||||
boxSizing: 'border-box',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
title="Действия"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
) : null}
|
||||
{showActionMenu && createPortal(
|
||||
<div className="wishlist-modal-overlay" style={{ zIndex: 2000 }} onClick={closeActionMenu}>
|
||||
<div className="wishlist-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="wishlist-modal-header">
|
||||
<h3>{name}</h3>
|
||||
</div>
|
||||
<div className="wishlist-modal-actions">
|
||||
{isArchived ? (
|
||||
<button className="wishlist-modal-copy" onClick={handleUnarchive}>
|
||||
Разархивировать
|
||||
</button>
|
||||
) : (
|
||||
<button className="wishlist-modal-copy" onClick={handleArchive}>
|
||||
Архивировать
|
||||
</button>
|
||||
)}
|
||||
<button className="wishlist-modal-delete" onClick={handleDelete}>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'react-day-picker/style.css'
|
||||
import './TaskList.css'
|
||||
import './TaskDetail.css'
|
||||
import './ShoppingList.css'
|
||||
import './Wishlist.css'
|
||||
|
||||
const BOARDS_CACHE_KEY = 'shopping_boards_cache'
|
||||
const ITEMS_CACHE_KEY = 'shopping_items_cache'
|
||||
@@ -133,6 +134,7 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
const [postponeRemaining, setPostponeRemaining] = useState('')
|
||||
const [isPostponing, setIsPostponing] = useState(false)
|
||||
const [toast, setToast] = useState(null)
|
||||
const [showBoardActionMenu, setShowBoardActionMenu] = useState(false)
|
||||
const initialFetchDoneRef = useRef(false)
|
||||
const prevIsActiveRef = useRef(isActive)
|
||||
const itemsAbortRef = useRef(null)
|
||||
@@ -388,15 +390,77 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
|
||||
const handleBoardEdit = (boardId) => {
|
||||
const id = boardId || selectedBoardId
|
||||
if (id) {
|
||||
onNavigate('shopping-board-form', { boardId: id })
|
||||
if (!id) return
|
||||
const board = boards.find(b => b.id === id)
|
||||
if (board && !board.is_owner) {
|
||||
openBoardActionMenu()
|
||||
return
|
||||
}
|
||||
onNavigate('shopping-board-form', { boardId: id })
|
||||
}
|
||||
|
||||
const handleAddBoard = () => {
|
||||
onNavigate('shopping-board-form')
|
||||
}
|
||||
|
||||
const openBoardActionMenu = () => {
|
||||
setShowBoardActionMenu(true)
|
||||
}
|
||||
|
||||
const closeBoardActionMenu = () => {
|
||||
setShowBoardActionMenu(false)
|
||||
}
|
||||
|
||||
const handleBoardArchive = async () => {
|
||||
const board = boards.find(b => b.id === selectedBoardId)
|
||||
if (!board) return
|
||||
|
||||
if (board.is_archived) {
|
||||
setShowBoardActionMenu(false)
|
||||
try {
|
||||
const res = await authFetch(`/api/shopping/boards/${selectedBoardId}/unarchive`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchBoards()
|
||||
fetchItems(selectedBoardId)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error unarchiving board:', err)
|
||||
}
|
||||
} else {
|
||||
if (!window.confirm('Архивировать доску? Она переместится в архив.')) return
|
||||
|
||||
setShowBoardActionMenu(false)
|
||||
try {
|
||||
const res = await authFetch(`/api/shopping/boards/${selectedBoardId}/archive`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchBoards()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error archiving board:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleBoardLeave = async () => {
|
||||
if (!window.confirm('Покинуть доску? Вы больше не будете видеть её товары.')) return
|
||||
|
||||
setShowBoardActionMenu(false)
|
||||
try {
|
||||
const res = await authFetch(`/api/shopping/boards/${selectedBoardId}/leave`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchBoards()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error leaving board:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (selectedBoardId) fetchItems(selectedBoardId)
|
||||
}
|
||||
@@ -941,6 +1005,31 @@ function ShoppingList({ onNavigate, refreshTrigger = 0, isActive = false, initia
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showBoardActionMenu && createPortal(
|
||||
<div className="wishlist-modal-overlay" style={{ zIndex: 2000 }} onClick={closeBoardActionMenu}>
|
||||
<div className="wishlist-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="wishlist-modal-header">
|
||||
<h3>{boards.find(b => b.id === selectedBoardId)?.name}</h3>
|
||||
</div>
|
||||
<div className="wishlist-modal-actions">
|
||||
{boards.find(b => b.id === selectedBoardId)?.is_archived ? (
|
||||
<button className="wishlist-modal-copy" onClick={handleBoardArchive}>
|
||||
Разархивировать
|
||||
</button>
|
||||
) : (
|
||||
<button className="wishlist-modal-copy" onClick={handleBoardArchive}>
|
||||
Архивировать
|
||||
</button>
|
||||
)}
|
||||
<button className="wishlist-modal-delete" onClick={handleBoardLeave}>
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import BoardSelector from './BoardSelector'
|
||||
import LoadingError from './LoadingError'
|
||||
@@ -47,6 +48,7 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
||||
const [selectedItem, setSelectedItem] = useState(null)
|
||||
const [selectedWishlistForDetail, setSelectedWishlistForDetail] = useState(null)
|
||||
const [currentWeekData, setCurrentWeekData] = useState(null)
|
||||
const [showBoardActionMenu, setShowBoardActionMenu] = useState(false)
|
||||
const fetchingRef = useRef(false)
|
||||
const fetchingCompletedRef = useRef(false)
|
||||
const initialFetchDoneRef = useRef(false)
|
||||
@@ -393,13 +395,77 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
||||
}
|
||||
|
||||
const handleBoardEdit = (boardId) => {
|
||||
onNavigate?.('board-form', { boardId: boardId || selectedBoardId })
|
||||
const id = boardId || selectedBoardId
|
||||
const board = boards.find(b => b.id === id)
|
||||
if (board && !board.is_owner) {
|
||||
openBoardActionMenu()
|
||||
return
|
||||
}
|
||||
onNavigate?.('board-form', { boardId: id })
|
||||
}
|
||||
|
||||
const handleAddBoard = () => {
|
||||
onNavigate?.('board-form', { boardId: null })
|
||||
}
|
||||
|
||||
const openBoardActionMenu = () => {
|
||||
setShowBoardActionMenu(true)
|
||||
}
|
||||
|
||||
const closeBoardActionMenu = () => {
|
||||
setShowBoardActionMenu(false)
|
||||
}
|
||||
|
||||
const handleBoardArchive = async () => {
|
||||
const board = boards.find(b => b.id === selectedBoardId)
|
||||
if (!board) return
|
||||
|
||||
if (board.is_archived) {
|
||||
setShowBoardActionMenu(false)
|
||||
try {
|
||||
const res = await authFetch(`/api/wishlist/boards/${selectedBoardId}/unarchive`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchBoards()
|
||||
fetchItems(selectedBoardId)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error unarchiving board:', err)
|
||||
}
|
||||
} else {
|
||||
if (!window.confirm('Архивировать доску? Она переместится в архив.')) return
|
||||
|
||||
setShowBoardActionMenu(false)
|
||||
try {
|
||||
const res = await authFetch(`/api/wishlist/boards/${selectedBoardId}/archive`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchBoards()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error archiving board:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleBoardLeave = async () => {
|
||||
if (!window.confirm('Покинуть доску? Вы больше не будете видеть её желания.')) return
|
||||
|
||||
setShowBoardActionMenu(false)
|
||||
try {
|
||||
const res = await authFetch(`/api/wishlist/boards/${selectedBoardId}/leave`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchBoards()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error leaving board:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleCompleted = () => {
|
||||
const newExpanded = !completedExpanded
|
||||
setCompletedExpanded(newExpanded)
|
||||
@@ -789,6 +855,31 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
|
||||
onClose={handleCloseDetail}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showBoardActionMenu && createPortal(
|
||||
<div className="wishlist-modal-overlay" style={{ zIndex: 2000 }} onClick={closeBoardActionMenu}>
|
||||
<div className="wishlist-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="wishlist-modal-header">
|
||||
<h3>{boards.find(b => b.id === selectedBoardId)?.name}</h3>
|
||||
</div>
|
||||
<div className="wishlist-modal-actions">
|
||||
{boards.find(b => b.id === selectedBoardId)?.is_archived ? (
|
||||
<button className="wishlist-modal-copy" onClick={handleBoardArchive}>
|
||||
Разархивировать
|
||||
</button>
|
||||
) : (
|
||||
<button className="wishlist-modal-copy" onClick={handleBoardArchive}>
|
||||
Архивировать
|
||||
</button>
|
||||
)}
|
||||
<button className="wishlist-modal-delete" onClick={handleBoardLeave}>
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user