diff --git a/VERSION b/VERSION
index 2496b04..961b1c8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-6.24.0
+6.25.0
diff --git a/play-life-web/package.json b/play-life-web/package.json
index 9f26292..abca42c 100644
--- a/play-life-web/package.json
+++ b/play-life-web/package.json
@@ -1,6 +1,6 @@
{
"name": "play-life-web",
- "version": "6.24.0",
+ "version": "6.25.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/play-life-web/src/components/BoardForm.jsx b/play-life-web/src/components/BoardForm.jsx
index abd3b74..ada2343 100644
--- a/play-life-web/src/components/BoardForm.jsx
+++ b/play-life-web/src/components/BoardForm.jsx
@@ -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 (
-
-
-
-
{name}
-
-
-
- {isArchived ? (
-
- ) : (
-
- )}
-
-
-
-
- {toastMessage && (
-
setToastMessage(null)}
- />
- )}
-
- )
- }
-
return (
{isEdit && (
-
+
+ ⋮
+
)}
,
document.body
) : null}
+ {showActionMenu && createPortal(
+
+
e.stopPropagation()}>
+
+
{name}
+
+
+ {isArchived ? (
+
+ Разархивировать
+
+ ) : (
+
+ Архивировать
+
+ )}
+
+ Удалить
+
+
+
+
,
+ document.body
+ )}
)
}
diff --git a/play-life-web/src/components/ShoppingBoardForm.jsx b/play-life-web/src/components/ShoppingBoardForm.jsx
index 24211b3..3a285a6 100644
--- a/play-life-web/src/components/ShoppingBoardForm.jsx
+++ b/play-life-web/src/components/ShoppingBoardForm.jsx
@@ -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 (
-
-
- ✕
-
-
-
{name}
-
-
-
- {isArchived ? (
-
-
- Разархивировать
-
- ) : (
-
-
- Архивировать
-
- )}
-
-
- Покинуть доску
-
-
-
-
- {toastMessage && (
-
setToastMessage(null)}
- />
- )}
-
- )
- }
-
return (
@@ -352,27 +349,6 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
}}
/>
- {/* Архивирование */}
-
- {isArchived ? (
-
-
- Разархивировать
-
- ) : (
-
-
- Архивировать
-
- )}
-
>
)}
@@ -422,16 +398,61 @@ function ShoppingBoardForm({ boardId, onNavigate, onSaved, isActive }) {
{loading ? 'Сохранение...' : 'Сохранить'}
{isEdit && (
-
+
+ ⋮
+
)}
,
document.body
) : null}
+ {showActionMenu && createPortal(
+
+
e.stopPropagation()}>
+
+
{name}
+
+
+ {isArchived ? (
+
+ Разархивировать
+
+ ) : (
+
+ Архивировать
+
+ )}
+
+ Удалить
+
+
+
+
,
+ document.body
+ )}
)
}
diff --git a/play-life-web/src/components/ShoppingList.jsx b/play-life-web/src/components/ShoppingList.jsx
index f21ea82..4901cdc 100644
--- a/play-life-web/src/components/ShoppingList.jsx
+++ b/play-life-web/src/components/ShoppingList.jsx
@@ -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(
+
+
e.stopPropagation()}>
+
+
{boards.find(b => b.id === selectedBoardId)?.name}
+
+
+ {boards.find(b => b.id === selectedBoardId)?.is_archived ? (
+
+ Разархивировать
+
+ ) : (
+
+ Архивировать
+
+ )}
+
+ Выйти
+
+
+
+
,
+ document.body
+ )}
)
}
diff --git a/play-life-web/src/components/Wishlist.jsx b/play-life-web/src/components/Wishlist.jsx
index be65d0f..8618a22 100644
--- a/play-life-web/src/components/Wishlist.jsx
+++ b/play-life-web/src/components/Wishlist.jsx
@@ -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(
+
+
e.stopPropagation()}>
+
+
{boards.find(b => b.id === selectedBoardId)?.name}
+
+
+ {boards.find(b => b.id === selectedBoardId)?.is_archived ? (
+
+ Разархивировать
+
+ ) : (
+
+ Архивировать
+
+ )}
+
+ Выйти
+
+
+
+
,
+ document.body
+ )}
)
}