2026-01-13 20:55:44 +03:00
|
|
|
|
import React, { useState, useEffect, useRef } from 'react'
|
2026-01-11 21:12:26 +03:00
|
|
|
|
import { useAuth } from './auth/AuthContext'
|
2026-01-13 22:35:01 +03:00
|
|
|
|
import BoardSelector from './BoardSelector'
|
2026-01-11 21:12:26 +03:00
|
|
|
|
import LoadingError from './LoadingError'
|
|
|
|
|
|
import './Wishlist.css'
|
|
|
|
|
|
|
|
|
|
|
|
const API_URL = '/api/wishlist'
|
2026-01-13 22:35:01 +03:00
|
|
|
|
const BOARDS_CACHE_KEY = 'wishlist_boards_cache'
|
|
|
|
|
|
const ITEMS_CACHE_KEY = 'wishlist_items_cache'
|
|
|
|
|
|
const SELECTED_BOARD_KEY = 'wishlist_selected_board_id'
|
2026-01-11 21:12:26 +03:00
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoardId = null, boardDeleted = false }) {
|
2026-01-11 21:12:26 +03:00
|
|
|
|
const { authFetch } = useAuth()
|
2026-01-13 22:35:01 +03:00
|
|
|
|
const [boards, setBoards] = useState([])
|
|
|
|
|
|
|
|
|
|
|
|
// Восстанавливаем выбранную доску из localStorage или используем initialBoardId
|
|
|
|
|
|
const getInitialBoardId = () => {
|
|
|
|
|
|
if (initialBoardId) return initialBoardId
|
|
|
|
|
|
return getSavedBoardId()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Получает сохранённую доску из localStorage
|
|
|
|
|
|
const getSavedBoardId = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const saved = localStorage.getItem(SELECTED_BOARD_KEY)
|
|
|
|
|
|
if (saved) {
|
|
|
|
|
|
const boardId = parseInt(saved, 10)
|
|
|
|
|
|
if (!isNaN(boardId)) return boardId
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error loading selected board from cache:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const [selectedBoardId, setSelectedBoardIdState] = useState(getInitialBoardId)
|
2026-01-11 21:12:26 +03:00
|
|
|
|
const [items, setItems] = useState([])
|
|
|
|
|
|
const [completed, setCompleted] = useState([])
|
2026-01-12 17:42:51 +03:00
|
|
|
|
const [completedCount, setCompletedCount] = useState(0)
|
2026-01-11 21:12:26 +03:00
|
|
|
|
const [loading, setLoading] = useState(true)
|
2026-01-13 22:35:01 +03:00
|
|
|
|
const [boardsLoading, setBoardsLoading] = useState(true)
|
2026-01-11 21:12:26 +03:00
|
|
|
|
const [error, setError] = useState('')
|
|
|
|
|
|
const [completedExpanded, setCompletedExpanded] = useState(false)
|
|
|
|
|
|
const [completedLoading, setCompletedLoading] = useState(false)
|
|
|
|
|
|
const [selectedItem, setSelectedItem] = useState(null)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
const fetchingRef = useRef(false)
|
|
|
|
|
|
const fetchingCompletedRef = useRef(false)
|
|
|
|
|
|
const initialFetchDoneRef = useRef(false)
|
|
|
|
|
|
const prevIsActiveRef = useRef(isActive)
|
2026-01-11 21:12:26 +03:00
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Обёртка для setSelectedBoardId с сохранением в localStorage
|
|
|
|
|
|
const setSelectedBoardId = (boardId) => {
|
|
|
|
|
|
setSelectedBoardIdState(boardId)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
try {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
if (boardId) {
|
|
|
|
|
|
localStorage.setItem(SELECTED_BOARD_KEY, String(boardId))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
localStorage.removeItem(SELECTED_BOARD_KEY)
|
|
|
|
|
|
}
|
2026-01-13 20:55:44 +03:00
|
|
|
|
} catch (err) {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
console.error('Error saving selected board to cache:', err)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-11 21:12:26 +03:00
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Загрузка досок из кэша
|
|
|
|
|
|
const loadBoardsFromCache = () => {
|
2026-01-12 17:42:51 +03:00
|
|
|
|
try {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
const cached = localStorage.getItem(BOARDS_CACHE_KEY)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
if (cached) {
|
|
|
|
|
|
const data = JSON.parse(cached)
|
2026-01-13 22:35:01 +03:00
|
|
|
|
setBoards(data.boards || [])
|
|
|
|
|
|
// Проверяем, что сохранённая доска существует в списке
|
|
|
|
|
|
if (selectedBoardId) {
|
|
|
|
|
|
const boardExists = data.boards?.some(b => b.id === selectedBoardId)
|
|
|
|
|
|
if (!boardExists && data.boards?.length > 0) {
|
|
|
|
|
|
setSelectedBoardId(data.boards[0].id)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (data.boards?.length > 0) {
|
|
|
|
|
|
// Пытаемся восстановить из localStorage
|
|
|
|
|
|
const savedBoardId = getSavedBoardId()
|
|
|
|
|
|
if (savedBoardId && data.boards.some(b => b.id === savedBoardId)) {
|
|
|
|
|
|
setSelectedBoardId(savedBoardId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedBoardId(data.boards[0].id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-13 20:55:44 +03:00
|
|
|
|
return true
|
2026-01-12 17:42:51 +03:00
|
|
|
|
}
|
2026-01-13 20:55:44 +03:00
|
|
|
|
} catch (err) {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
console.error('Error loading boards from cache:', err)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
2026-01-12 17:42:51 +03:00
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Сохранение досок в кэш
|
|
|
|
|
|
const saveBoardsToCache = (boardsData) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
localStorage.setItem(BOARDS_CACHE_KEY, JSON.stringify({
|
|
|
|
|
|
boards: boardsData,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}))
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error saving boards to cache:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Загрузка желаний из кэша (по board_id)
|
|
|
|
|
|
const loadItemsFromCache = (boardId) => {
|
2026-01-13 20:55:44 +03:00
|
|
|
|
try {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
const cached = localStorage.getItem(`${ITEMS_CACHE_KEY}_${boardId}`)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
if (cached) {
|
|
|
|
|
|
const data = JSON.parse(cached)
|
2026-01-13 22:35:01 +03:00
|
|
|
|
setItems(data.items || [])
|
|
|
|
|
|
setCompletedCount(data.completedCount || 0)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
return true
|
2026-01-12 17:42:51 +03:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
console.error('Error loading items from cache:', err)
|
2026-01-12 17:42:51 +03:00
|
|
|
|
}
|
2026-01-13 20:55:44 +03:00
|
|
|
|
return false
|
2026-01-12 17:42:51 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Сохранение желаний в кэш
|
|
|
|
|
|
const saveItemsToCache = (boardId, itemsData, count) => {
|
2026-01-13 20:55:44 +03:00
|
|
|
|
try {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
localStorage.setItem(`${ITEMS_CACHE_KEY}_${boardId}`, JSON.stringify({
|
2026-01-13 20:55:44 +03:00
|
|
|
|
items: itemsData,
|
|
|
|
|
|
completedCount: count,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
}))
|
|
|
|
|
|
} catch (err) {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
console.error('Error saving items to cache:', err)
|
2026-01-11 21:12:26 +03:00
|
|
|
|
}
|
2026-01-13 20:55:44 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Загрузка списка досок
|
|
|
|
|
|
const fetchBoards = async () => {
|
2026-01-13 20:55:44 +03:00
|
|
|
|
try {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
const response = await authFetch(`${API_URL}/boards`)
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
setBoards(data || [])
|
|
|
|
|
|
saveBoardsToCache(data || [])
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем, что выбранная доска существует в списке
|
|
|
|
|
|
if (selectedBoardId) {
|
|
|
|
|
|
const boardExists = data?.some(b => b.id === selectedBoardId)
|
|
|
|
|
|
if (!boardExists && data?.length > 0) {
|
|
|
|
|
|
// Сохранённая доска не существует, выбираем первую
|
|
|
|
|
|
setSelectedBoardId(data[0].id)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (data?.length > 0) {
|
|
|
|
|
|
// Пытаемся восстановить из localStorage
|
|
|
|
|
|
const savedBoardId = getSavedBoardId()
|
|
|
|
|
|
if (savedBoardId && data.some(b => b.id === savedBoardId)) {
|
|
|
|
|
|
setSelectedBoardId(savedBoardId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedBoardId(data[0].id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-13 20:55:44 +03:00
|
|
|
|
} catch (err) {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
console.error('Error fetching boards:', err)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setBoardsLoading(false)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-11 21:12:26 +03:00
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Загрузка желаний выбранной доски
|
|
|
|
|
|
const fetchItems = async () => {
|
|
|
|
|
|
if (!selectedBoardId || fetchingRef.current) return
|
2026-01-13 20:55:44 +03:00
|
|
|
|
fetchingRef.current = true
|
|
|
|
|
|
|
2026-01-11 21:12:26 +03:00
|
|
|
|
try {
|
2026-01-13 20:55:44 +03:00
|
|
|
|
const hasDataInState = items.length > 0 || completedCount > 0
|
2026-01-13 22:35:01 +03:00
|
|
|
|
if (!hasDataInState) {
|
|
|
|
|
|
const cacheLoaded = loadItemsFromCache(selectedBoardId)
|
|
|
|
|
|
if (!cacheLoaded) {
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
}
|
2026-01-13 20:55:44 +03:00
|
|
|
|
}
|
2026-01-11 21:12:26 +03:00
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
const response = await authFetch(`${API_URL}/boards/${selectedBoardId}/items`)
|
2026-01-11 21:12:26 +03:00
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('Ошибка при загрузке желаний')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
const allItems = [...(data.unlocked || []), ...(data.locked || [])]
|
2026-01-12 17:42:51 +03:00
|
|
|
|
const count = data.completed_count || 0
|
|
|
|
|
|
|
2026-01-13 20:55:44 +03:00
|
|
|
|
setItems(allItems)
|
|
|
|
|
|
setCompletedCount(count)
|
2026-01-13 22:35:01 +03:00
|
|
|
|
saveItemsToCache(selectedBoardId, allItems, count)
|
2026-01-11 21:12:26 +03:00
|
|
|
|
setError('')
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(err.message)
|
2026-01-13 22:35:01 +03:00
|
|
|
|
if (!loadItemsFromCache(selectedBoardId)) {
|
2026-01-13 20:55:44 +03:00
|
|
|
|
setItems([])
|
|
|
|
|
|
setCompletedCount(0)
|
|
|
|
|
|
}
|
2026-01-11 21:12:26 +03:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
fetchingRef.current = false
|
2026-01-12 17:42:51 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Загрузка завершённых для текущей доски
|
2026-01-12 17:42:51 +03:00
|
|
|
|
const fetchCompleted = async () => {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
if (fetchingCompletedRef.current || !selectedBoardId) return
|
2026-01-13 20:55:44 +03:00
|
|
|
|
fetchingCompletedRef.current = true
|
|
|
|
|
|
|
2026-01-12 17:42:51 +03:00
|
|
|
|
try {
|
|
|
|
|
|
setCompletedLoading(true)
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Используем новый API для получения завершённых на доске
|
|
|
|
|
|
const response = await authFetch(`${API_URL}/boards/${selectedBoardId}/completed`)
|
2026-01-12 17:42:51 +03:00
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('Ошибка при загрузке завершённых желаний')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json()
|
2026-01-13 20:55:44 +03:00
|
|
|
|
const completedData = Array.isArray(data) ? data : []
|
|
|
|
|
|
setCompleted(completedData)
|
2026-01-12 17:42:51 +03:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error fetching completed items:', err)
|
|
|
|
|
|
setCompleted([])
|
|
|
|
|
|
} finally {
|
2026-01-11 21:12:26 +03:00
|
|
|
|
setCompletedLoading(false)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
fetchingCompletedRef.current = false
|
2026-01-11 21:12:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 20:55:44 +03:00
|
|
|
|
// Первая инициализация
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!initialFetchDoneRef.current) {
|
|
|
|
|
|
initialFetchDoneRef.current = true
|
|
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Загружаем доски из кэша
|
|
|
|
|
|
const boardsCacheLoaded = loadBoardsFromCache()
|
|
|
|
|
|
if (boardsCacheLoaded) {
|
|
|
|
|
|
setBoardsLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Загружаем доски с сервера
|
|
|
|
|
|
fetchBoards()
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
// Загружаем желания при смене доски
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (selectedBoardId) {
|
|
|
|
|
|
// Сбрасываем состояние
|
|
|
|
|
|
setItems([])
|
|
|
|
|
|
setCompletedCount(0)
|
|
|
|
|
|
setCompleted([])
|
|
|
|
|
|
setCompletedExpanded(false)
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
|
|
|
|
|
|
// Пробуем загрузить из кэша
|
|
|
|
|
|
const cacheLoaded = loadItemsFromCache(selectedBoardId)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
if (cacheLoaded) {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Загружаем свежие данные
|
2026-01-13 22:35:01 +03:00
|
|
|
|
fetchItems()
|
2026-01-13 20:55:44 +03:00
|
|
|
|
}
|
2026-01-13 22:35:01 +03:00
|
|
|
|
}, [selectedBoardId])
|
2026-01-13 20:55:44 +03:00
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Обновление при активации таба
|
2026-01-13 20:55:44 +03:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const wasActive = prevIsActiveRef.current
|
|
|
|
|
|
prevIsActiveRef.current = isActive
|
|
|
|
|
|
|
|
|
|
|
|
if (!initialFetchDoneRef.current) return
|
|
|
|
|
|
|
|
|
|
|
|
if (isActive && !wasActive) {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
fetchBoards()
|
|
|
|
|
|
if (selectedBoardId) {
|
|
|
|
|
|
fetchItems()
|
2026-01-13 20:55:44 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isActive])
|
|
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Обновление при refreshTrigger
|
2026-01-13 20:55:44 +03:00
|
|
|
|
useEffect(() => {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
if (refreshTrigger > 0 && selectedBoardId) {
|
|
|
|
|
|
// Очищаем кэш для текущей доски, чтобы загрузить свежие данные
|
|
|
|
|
|
try {
|
|
|
|
|
|
localStorage.removeItem(`${ITEMS_CACHE_KEY}_${selectedBoardId}`)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error clearing cache:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
fetchBoards()
|
|
|
|
|
|
fetchItems()
|
2026-01-13 20:55:44 +03:00
|
|
|
|
if (completedExpanded && completedCount > 0) {
|
|
|
|
|
|
fetchCompleted()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-13 22:35:01 +03:00
|
|
|
|
}, [refreshTrigger, selectedBoardId])
|
|
|
|
|
|
|
|
|
|
|
|
// Обновление при initialBoardId (когда создана новая доска или переход по ссылке)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (initialBoardId && initialBoardId !== selectedBoardId) {
|
|
|
|
|
|
// Сбрасываем флаг загрузки, чтобы не блокировать новую загрузку
|
|
|
|
|
|
fetchingRef.current = false
|
|
|
|
|
|
|
|
|
|
|
|
// Обновляем список досок (чтобы новая доска появилась)
|
|
|
|
|
|
fetchBoards().then(() => {
|
|
|
|
|
|
// Переключаемся на новую доску после обновления списка
|
|
|
|
|
|
// Это вызовет useEffect для selectedBoardId, который загрузит данные
|
|
|
|
|
|
setSelectedBoardId(initialBoardId)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [initialBoardId])
|
|
|
|
|
|
|
|
|
|
|
|
// Обработка удаления доски - выбираем первую доступную
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (boardDeleted && boards.length > 0) {
|
|
|
|
|
|
// Очищаем текущие данные
|
|
|
|
|
|
setItems([])
|
|
|
|
|
|
setCompletedCount(0)
|
|
|
|
|
|
setCompleted([])
|
|
|
|
|
|
setCompletedExpanded(false)
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
|
|
|
|
|
|
// Обновляем список досок и выбираем первую
|
|
|
|
|
|
fetchBoards().then(() => {
|
|
|
|
|
|
// fetchBoards обновит boards, но мы уже в этом useEffect
|
|
|
|
|
|
// selectedBoardId обновится автоматически в useEffect ниже
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [boardDeleted])
|
|
|
|
|
|
|
|
|
|
|
|
// Если текущая доска больше не существует в списке - выбираем первую
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (boards.length > 0 && selectedBoardId) {
|
|
|
|
|
|
const boardExists = boards.some(b => b.id === selectedBoardId)
|
|
|
|
|
|
if (!boardExists) {
|
|
|
|
|
|
setSelectedBoardId(boards[0].id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [boards, selectedBoardId])
|
|
|
|
|
|
|
|
|
|
|
|
const handleBoardChange = (boardId) => {
|
|
|
|
|
|
setSelectedBoardId(boardId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleBoardEdit = () => {
|
|
|
|
|
|
const board = boards.find(b => b.id === selectedBoardId)
|
|
|
|
|
|
if (board?.is_owner) {
|
|
|
|
|
|
onNavigate?.('board-form', { boardId: selectedBoardId })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Показать подтверждение выхода
|
|
|
|
|
|
handleLeaveBoard()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleLeaveBoard = async () => {
|
|
|
|
|
|
if (!window.confirm('Отвязаться от этой доски? Вы больше не будете видеть её желания.')) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await authFetch(`${API_URL}/boards/${selectedBoardId}/leave`, {
|
|
|
|
|
|
method: 'POST'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
// Убираем доску из списка
|
|
|
|
|
|
const newBoards = boards.filter(b => b.id !== selectedBoardId)
|
|
|
|
|
|
setBoards(newBoards)
|
|
|
|
|
|
saveBoardsToCache(newBoards)
|
|
|
|
|
|
|
|
|
|
|
|
// Выбираем первую доску
|
|
|
|
|
|
if (newBoards.length > 0) {
|
|
|
|
|
|
setSelectedBoardId(newBoards[0].id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedBoardId(null)
|
|
|
|
|
|
setItems([])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error leaving board:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleAddBoard = () => {
|
|
|
|
|
|
onNavigate?.('board-form', { boardId: null })
|
|
|
|
|
|
}
|
2026-01-13 20:55:44 +03:00
|
|
|
|
|
2026-01-11 21:12:26 +03:00
|
|
|
|
const handleToggleCompleted = () => {
|
|
|
|
|
|
const newExpanded = !completedExpanded
|
|
|
|
|
|
setCompletedExpanded(newExpanded)
|
2026-01-13 20:55:44 +03:00
|
|
|
|
|
|
|
|
|
|
if (newExpanded && completedCount > 0) {
|
2026-01-12 17:42:51 +03:00
|
|
|
|
fetchCompleted()
|
2026-01-11 21:12:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleAddClick = () => {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
onNavigate?.('wishlist-form', { wishlistId: undefined, boardId: selectedBoardId })
|
2026-01-11 21:12:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleItemClick = (item) => {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
onNavigate?.('wishlist-detail', { wishlistId: item.id, boardId: selectedBoardId })
|
2026-01-11 21:12:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleMenuClick = (item, e) => {
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
setSelectedItem(item)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleEdit = () => {
|
|
|
|
|
|
if (selectedItem) {
|
2026-01-13 22:35:01 +03:00
|
|
|
|
onNavigate?.('wishlist-form', { wishlistId: selectedItem.id, boardId: selectedBoardId })
|
2026-01-11 21:12:26 +03:00
|
|
|
|
setSelectedItem(null)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = async () => {
|
|
|
|
|
|
if (!selectedItem) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await authFetch(`${API_URL}/${selectedItem.id}`, {
|
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('Ошибка при удалении')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSelectedItem(null)
|
2026-01-13 22:35:01 +03:00
|
|
|
|
await fetchItems()
|
2026-01-13 20:55:44 +03:00
|
|
|
|
if (completedExpanded) {
|
|
|
|
|
|
await fetchCompleted()
|
|
|
|
|
|
}
|
2026-01-11 21:12:26 +03:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(err.message)
|
|
|
|
|
|
setSelectedItem(null)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 17:42:51 +03:00
|
|
|
|
const handleCopy = async () => {
|
|
|
|
|
|
if (!selectedItem) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-13 20:55:44 +03:00
|
|
|
|
const response = await authFetch(`${API_URL}/${selectedItem.id}/copy`, {
|
2026-01-12 17:42:51 +03:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-13 20:55:44 +03:00
|
|
|
|
if (!response.ok) {
|
2026-01-19 13:07:17 +03:00
|
|
|
|
const errorText = await response.text().catch(() => '')
|
|
|
|
|
|
throw new Error(errorText || 'Ошибка при копировании')
|
2026-01-12 17:42:51 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 20:55:44 +03:00
|
|
|
|
const newItem = await response.json()
|
2026-01-12 17:42:51 +03:00
|
|
|
|
|
|
|
|
|
|
setSelectedItem(null)
|
2026-01-19 13:07:17 +03:00
|
|
|
|
|
|
|
|
|
|
// Очищаем кэш для текущей доски, чтобы новое желание появилось в списке
|
|
|
|
|
|
if (selectedBoardId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
localStorage.removeItem(`${ITEMS_CACHE_KEY}_${selectedBoardId}`)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error clearing cache:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Обновляем список
|
|
|
|
|
|
await fetchItems()
|
|
|
|
|
|
|
|
|
|
|
|
// Открываем форму редактирования для нового желания
|
2026-01-13 22:35:01 +03:00
|
|
|
|
onNavigate?.('wishlist-form', { wishlistId: newItem.id, boardId: selectedBoardId })
|
2026-01-12 17:42:51 +03:00
|
|
|
|
} catch (err) {
|
2026-01-19 13:07:17 +03:00
|
|
|
|
console.error('Error copying wishlist item:', err)
|
|
|
|
|
|
setError(err.message || 'Ошибка при копировании')
|
2026-01-12 17:42:51 +03:00
|
|
|
|
setSelectedItem(null)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 21:12:26 +03:00
|
|
|
|
const formatPrice = (price) => {
|
|
|
|
|
|
return new Intl.NumberFormat('ru-RU', {
|
|
|
|
|
|
style: 'currency',
|
|
|
|
|
|
currency: 'RUB',
|
|
|
|
|
|
minimumFractionDigits: 0,
|
|
|
|
|
|
maximumFractionDigits: 0,
|
|
|
|
|
|
}).format(price)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 17:42:51 +03:00
|
|
|
|
const findFirstUnmetCondition = (item) => {
|
|
|
|
|
|
if (!item.unlock_conditions || item.unlock_conditions.length === 0) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const condition of item.unlock_conditions) {
|
|
|
|
|
|
let isMet = false
|
|
|
|
|
|
|
|
|
|
|
|
if (condition.type === 'task_completion') {
|
|
|
|
|
|
isMet = condition.task_completed === true
|
|
|
|
|
|
} else if (condition.type === 'project_points') {
|
|
|
|
|
|
const currentPoints = condition.current_points || 0
|
|
|
|
|
|
const requiredPoints = condition.required_points || 0
|
|
|
|
|
|
isMet = currentPoints >= requiredPoints
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isMet) {
|
|
|
|
|
|
return condition
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 21:12:26 +03:00
|
|
|
|
const renderUnlockCondition = (item) => {
|
2026-01-12 17:42:51 +03:00
|
|
|
|
if (item.completed) return null
|
2026-01-11 21:12:26 +03:00
|
|
|
|
|
2026-01-12 17:42:51 +03:00
|
|
|
|
const condition = findFirstUnmetCondition(item)
|
|
|
|
|
|
if (!condition) return null
|
2026-01-11 21:12:26 +03:00
|
|
|
|
|
|
|
|
|
|
let conditionText = ''
|
|
|
|
|
|
if (condition.type === 'task_completion') {
|
|
|
|
|
|
conditionText = condition.task_name || 'Задача'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const points = condition.required_points || 0
|
|
|
|
|
|
const project = condition.project_name || 'Проект'
|
2026-01-12 17:42:51 +03:00
|
|
|
|
let dateText = ''
|
|
|
|
|
|
if (condition.start_date) {
|
|
|
|
|
|
const date = new Date(condition.start_date + 'T00:00:00')
|
|
|
|
|
|
dateText = ` с ${date.toLocaleDateString('ru-RU')}`
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dateText = ' за всё время'
|
2026-01-11 21:12:26 +03:00
|
|
|
|
}
|
2026-01-12 17:42:51 +03:00
|
|
|
|
conditionText = `${points} в ${project}${dateText}`
|
2026-01-11 21:12:26 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="unlock-condition-wrapper">
|
|
|
|
|
|
<div className="unlock-condition-line">
|
|
|
|
|
|
<div className="unlock-condition">
|
|
|
|
|
|
<svg className="lock-icon" width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
|
|
|
|
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span className="condition-text">{conditionText}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renderItem = (item) => {
|
|
|
|
|
|
const isFaded = (!item.unlocked && !item.completed) || item.completed
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={item.id}
|
|
|
|
|
|
className={`wishlist-card ${isFaded ? 'faded' : ''}`}
|
|
|
|
|
|
onClick={() => handleItemClick(item)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="card-menu-button"
|
|
|
|
|
|
onClick={(e) => handleMenuClick(item, e)}
|
|
|
|
|
|
title="Меню"
|
|
|
|
|
|
>
|
|
|
|
|
|
⋮
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="card-image">
|
|
|
|
|
|
{item.image_url ? (
|
|
|
|
|
|
<img src={item.image_url} alt={item.name} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="placeholder">
|
|
|
|
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
|
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
|
|
|
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
|
|
|
|
<polyline points="21 15 16 10 5 21"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="card-name">{item.name}</div>
|
|
|
|
|
|
|
2026-01-12 17:42:51 +03:00
|
|
|
|
{(() => {
|
|
|
|
|
|
const unmetCondition = findFirstUnmetCondition(item)
|
|
|
|
|
|
if (unmetCondition && !item.completed) {
|
|
|
|
|
|
return renderUnlockCondition(item)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (item.price) {
|
|
|
|
|
|
return <div className="card-price">{formatPrice(item.price)}</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
})()}
|
2026-01-11 21:12:26 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
// Показываем loading только если и доски и желания грузятся
|
|
|
|
|
|
if (boardsLoading && loading) {
|
2026-01-11 21:12:26 +03:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="wishlist">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-13 22:35:01 +03:00
|
|
|
|
if (error && items.length === 0) {
|
2026-01-11 21:12:26 +03:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="wishlist">
|
2026-01-13 22:35:01 +03:00
|
|
|
|
<BoardSelector
|
|
|
|
|
|
boards={boards}
|
|
|
|
|
|
selectedBoardId={selectedBoardId}
|
|
|
|
|
|
onBoardChange={handleBoardChange}
|
|
|
|
|
|
onBoardEdit={handleBoardEdit}
|
|
|
|
|
|
onAddBoard={handleAddBoard}
|
|
|
|
|
|
loading={boardsLoading}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<LoadingError onRetry={() => fetchItems()} />
|
2026-01-11 21:12:26 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="wishlist">
|
2026-01-13 22:35:01 +03:00
|
|
|
|
{/* Селектор доски */}
|
|
|
|
|
|
<BoardSelector
|
|
|
|
|
|
boards={boards}
|
|
|
|
|
|
selectedBoardId={selectedBoardId}
|
|
|
|
|
|
onBoardChange={handleBoardChange}
|
|
|
|
|
|
onBoardEdit={handleBoardEdit}
|
|
|
|
|
|
onAddBoard={handleAddBoard}
|
|
|
|
|
|
loading={boardsLoading}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Основной список */}
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<div className="wishlist-loading">
|
|
|
|
|
|
<div className="w-8 h-8 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
2026-01-11 21:12:26 +03:00
|
|
|
|
<>
|
2026-01-13 22:35:01 +03:00
|
|
|
|
<div className="wishlist-grid">
|
|
|
|
|
|
{items.map(renderItem)}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleAddClick}
|
|
|
|
|
|
className="add-wishlist-button"
|
2026-01-12 17:42:51 +03:00
|
|
|
|
>
|
2026-01-13 22:35:01 +03:00
|
|
|
|
<div className="add-wishlist-icon">+</div>
|
2026-01-12 17:42:51 +03:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-01-13 22:35:01 +03:00
|
|
|
|
|
|
|
|
|
|
{/* Завершённые */}
|
|
|
|
|
|
{completedCount > 0 && (
|
2026-01-12 17:42:51 +03:00
|
|
|
|
<>
|
2026-01-13 22:35:01 +03:00
|
|
|
|
<div className="section-divider">
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="completed-toggle"
|
|
|
|
|
|
onClick={handleToggleCompleted}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="completed-toggle-icon">
|
|
|
|
|
|
{completedExpanded ? '▼' : '▶'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span>Завершённые</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{completedExpanded && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{completedLoading ? (
|
|
|
|
|
|
<div className="loading-completed">
|
|
|
|
|
|
<div className="w-8 h-8 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="wishlist-grid">
|
|
|
|
|
|
{completed.map(renderItem)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
2026-01-12 17:42:51 +03:00
|
|
|
|
)}
|
|
|
|
|
|
</>
|
2026-01-11 21:12:26 +03:00
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Модальное окно для действий */}
|
|
|
|
|
|
{selectedItem && (
|
|
|
|
|
|
<div className="wishlist-modal-overlay" onClick={() => setSelectedItem(null)}>
|
|
|
|
|
|
<div className="wishlist-modal" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
|
<div className="wishlist-modal-header">
|
|
|
|
|
|
<h3>{selectedItem.name}</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="wishlist-modal-actions">
|
|
|
|
|
|
<button className="wishlist-modal-edit" onClick={handleEdit}>
|
|
|
|
|
|
Редактировать
|
|
|
|
|
|
</button>
|
2026-01-12 17:42:51 +03:00
|
|
|
|
<button className="wishlist-modal-copy" onClick={handleCopy}>
|
|
|
|
|
|
Копировать
|
|
|
|
|
|
</button>
|
2026-01-11 21:12:26 +03:00
|
|
|
|
<button className="wishlist-modal-delete" onClick={handleDelete}>
|
|
|
|
|
|
Удалить
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default Wishlist
|