Оптимизация wishlist: раздельные запросы и копирование
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m14s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m14s
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import LoadingError from './LoadingError'
|
||||
import './Wishlist.css'
|
||||
|
||||
const API_URL = '/api/wishlist'
|
||||
const CACHE_KEY = 'wishlist_cache'
|
||||
const CACHE_COMPLETED_KEY = 'wishlist_completed_cache'
|
||||
|
||||
function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [items, setItems] = useState([])
|
||||
const [completed, setCompleted] = useState([])
|
||||
@@ -15,44 +17,84 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
const [completedExpanded, setCompletedExpanded] = useState(false)
|
||||
const [completedLoading, setCompletedLoading] = useState(false)
|
||||
const [selectedItem, setSelectedItem] = useState(null)
|
||||
const [tasks, setTasks] = useState([])
|
||||
const [projects, setProjects] = useState([])
|
||||
const fetchingRef = useRef(false)
|
||||
const fetchingCompletedRef = useRef(false)
|
||||
const initialFetchDoneRef = useRef(false)
|
||||
const prevIsActiveRef = useRef(isActive)
|
||||
|
||||
useEffect(() => {
|
||||
fetchWishlist()
|
||||
loadTasksAndProjects()
|
||||
}, [])
|
||||
|
||||
const loadTasksAndProjects = async () => {
|
||||
// Проверка наличия кэша
|
||||
const hasCache = () => {
|
||||
try {
|
||||
// Загружаем задачи
|
||||
const tasksResponse = await authFetch('/api/tasks')
|
||||
if (tasksResponse.ok) {
|
||||
const tasksData = await tasksResponse.json()
|
||||
setTasks(Array.isArray(tasksData) ? tasksData : [])
|
||||
}
|
||||
|
||||
// Загружаем проекты
|
||||
const projectsResponse = await authFetch('/projects')
|
||||
if (projectsResponse.ok) {
|
||||
const projectsData = await projectsResponse.json()
|
||||
setProjects(Array.isArray(projectsData) ? projectsData : [])
|
||||
}
|
||||
return localStorage.getItem(CACHE_KEY) !== null
|
||||
} catch (err) {
|
||||
console.error('Error loading tasks and projects:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем данные при изменении refreshTrigger
|
||||
useEffect(() => {
|
||||
if (refreshTrigger > 0) {
|
||||
fetchWishlist()
|
||||
}
|
||||
}, [refreshTrigger])
|
||||
|
||||
const fetchWishlist = async () => {
|
||||
// Загрузка основных данных из кэша
|
||||
const loadFromCache = () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const cached = localStorage.getItem(CACHE_KEY)
|
||||
if (cached) {
|
||||
const data = JSON.parse(cached)
|
||||
setItems(data.items || [])
|
||||
setCompletedCount(data.completedCount || 0)
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading from cache:', err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Загрузка завершённых из кэша
|
||||
const loadCompletedFromCache = () => {
|
||||
try {
|
||||
const cached = localStorage.getItem(CACHE_COMPLETED_KEY)
|
||||
if (cached) {
|
||||
const data = JSON.parse(cached)
|
||||
setCompleted(data || [])
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading completed from cache:', err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Сохранение основных данных в кэш
|
||||
const saveToCache = (itemsData, count) => {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify({
|
||||
items: itemsData,
|
||||
completedCount: count,
|
||||
timestamp: Date.now()
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('Error saving to cache:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Сохранение завершённых в кэш
|
||||
const saveCompletedToCache = (completedData) => {
|
||||
try {
|
||||
localStorage.setItem(CACHE_COMPLETED_KEY, JSON.stringify(completedData))
|
||||
} catch (err) {
|
||||
console.error('Error saving completed to cache:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка основного списка
|
||||
const fetchWishlist = async () => {
|
||||
if (fetchingRef.current) return
|
||||
fetchingRef.current = true
|
||||
|
||||
try {
|
||||
const hasDataInState = items.length > 0 || completedCount > 0
|
||||
const cacheExists = hasCache()
|
||||
if (!hasDataInState && !cacheExists) {
|
||||
setLoading(true)
|
||||
}
|
||||
|
||||
const response = await authFetch(API_URL)
|
||||
|
||||
@@ -61,53 +103,125 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
// Объединяем разблокированные и заблокированные в один список
|
||||
const allItems = [...(data.unlocked || []), ...(data.locked || [])]
|
||||
setItems(allItems)
|
||||
const count = data.completed_count || 0
|
||||
|
||||
setItems(allItems)
|
||||
setCompletedCount(count)
|
||||
|
||||
// Загружаем завершённые сразу, если они есть
|
||||
if (count > 0) {
|
||||
fetchCompleted()
|
||||
} else {
|
||||
setCompleted([])
|
||||
}
|
||||
|
||||
saveToCache(allItems, count)
|
||||
setError('')
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setItems([])
|
||||
setCompleted([])
|
||||
setCompletedCount(0)
|
||||
if (!hasCache()) {
|
||||
setItems([])
|
||||
setCompletedCount(0)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
fetchingRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка завершённых
|
||||
const fetchCompleted = async () => {
|
||||
if (fetchingCompletedRef.current) return
|
||||
fetchingCompletedRef.current = true
|
||||
|
||||
try {
|
||||
setCompletedLoading(true)
|
||||
const response = await authFetch(`${API_URL}?include_completed=true`)
|
||||
const response = await authFetch(`${API_URL}/completed`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка при загрузке завершённых желаний')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setCompleted(data.completed || [])
|
||||
const completedData = Array.isArray(data) ? data : []
|
||||
setCompleted(completedData)
|
||||
saveCompletedToCache(completedData)
|
||||
} catch (err) {
|
||||
console.error('Error fetching completed items:', err)
|
||||
setCompleted([])
|
||||
} finally {
|
||||
setCompletedLoading(false)
|
||||
fetchingCompletedRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
// Первая инициализация
|
||||
useEffect(() => {
|
||||
if (!initialFetchDoneRef.current) {
|
||||
initialFetchDoneRef.current = true
|
||||
|
||||
// Загружаем из кэша
|
||||
const cacheLoaded = loadFromCache()
|
||||
if (cacheLoaded) {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// Загружаем свежие данные
|
||||
fetchWishlist()
|
||||
|
||||
// Если список завершённых раскрыт - загружаем их тоже
|
||||
if (completedExpanded) {
|
||||
loadCompletedFromCache()
|
||||
fetchCompleted()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Обработка активации/деактивации таба
|
||||
useEffect(() => {
|
||||
const wasActive = prevIsActiveRef.current
|
||||
prevIsActiveRef.current = isActive
|
||||
|
||||
// Пропускаем первую инициализацию (она обрабатывается отдельно)
|
||||
if (!initialFetchDoneRef.current) return
|
||||
|
||||
// Когда таб становится видимым
|
||||
if (isActive && !wasActive) {
|
||||
// Показываем кэш, если есть данные
|
||||
const hasDataInState = items.length > 0 || completedCount > 0
|
||||
if (!hasDataInState) {
|
||||
const cacheLoaded = loadFromCache()
|
||||
if (cacheLoaded) {
|
||||
setLoading(false)
|
||||
} else {
|
||||
setLoading(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Всегда загружаем свежие данные основного списка
|
||||
fetchWishlist()
|
||||
|
||||
// Если список завершённых раскрыт - загружаем их тоже
|
||||
if (completedExpanded && completedCount > 0) {
|
||||
fetchCompleted()
|
||||
}
|
||||
}
|
||||
}, [isActive])
|
||||
|
||||
// Обновляем данные при изменении refreshTrigger
|
||||
useEffect(() => {
|
||||
if (refreshTrigger > 0) {
|
||||
fetchWishlist()
|
||||
if (completedExpanded && completedCount > 0) {
|
||||
fetchCompleted()
|
||||
}
|
||||
}
|
||||
}, [refreshTrigger])
|
||||
|
||||
const handleToggleCompleted = () => {
|
||||
const newExpanded = !completedExpanded
|
||||
setCompletedExpanded(newExpanded)
|
||||
if (newExpanded && completed.length === 0 && completedCount > 0) {
|
||||
|
||||
// При раскрытии загружаем завершённые
|
||||
if (newExpanded && completedCount > 0) {
|
||||
// Показываем из кэша если есть
|
||||
if (completed.length === 0) {
|
||||
loadCompletedFromCache()
|
||||
}
|
||||
// Загружаем свежие данные
|
||||
fetchCompleted()
|
||||
}
|
||||
}
|
||||
@@ -145,7 +259,10 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
}
|
||||
|
||||
setSelectedItem(null)
|
||||
await fetchWishlist(completedExpanded)
|
||||
await fetchWishlist()
|
||||
if (completedExpanded) {
|
||||
await fetchCompleted()
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setSelectedItem(null)
|
||||
@@ -165,7 +282,10 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
}
|
||||
|
||||
setSelectedItem(null)
|
||||
await fetchWishlist(completedExpanded)
|
||||
await fetchWishlist()
|
||||
if (completedExpanded) {
|
||||
await fetchCompleted()
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setSelectedItem(null)
|
||||
@@ -176,97 +296,17 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
if (!selectedItem) return
|
||||
|
||||
try {
|
||||
// Загружаем полные данные желания
|
||||
const response = await authFetch(`${API_URL}/${selectedItem.id}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка при загрузке желания')
|
||||
}
|
||||
|
||||
const itemData = await response.json()
|
||||
|
||||
// Преобразуем условия из формата Display в формат Request
|
||||
const unlockConditions = (itemData.unlock_conditions || []).map((cond) => {
|
||||
const condition = {
|
||||
type: cond.type,
|
||||
display_order: cond.display_order,
|
||||
}
|
||||
|
||||
if (cond.type === 'task_completion' && cond.task_name) {
|
||||
// Находим task_id по имени задачи
|
||||
const task = tasks.find(t => t.name === cond.task_name)
|
||||
if (task) {
|
||||
condition.task_id = task.id
|
||||
}
|
||||
} else if (cond.type === 'project_points' && cond.project_name) {
|
||||
// Находим project_id по имени проекта
|
||||
const project = projects.find(p => p.project_name === cond.project_name)
|
||||
if (project) {
|
||||
condition.project_id = project.project_id
|
||||
}
|
||||
if (cond.required_points !== undefined && cond.required_points !== null) {
|
||||
condition.required_points = cond.required_points
|
||||
}
|
||||
if (cond.start_date) {
|
||||
condition.start_date = cond.start_date
|
||||
}
|
||||
}
|
||||
|
||||
return condition
|
||||
})
|
||||
|
||||
// Создаем копию желания
|
||||
const copyData = {
|
||||
name: `${itemData.name} (копия)`,
|
||||
price: itemData.price || null,
|
||||
link: itemData.link || null,
|
||||
unlock_conditions: unlockConditions,
|
||||
}
|
||||
|
||||
const createResponse = await authFetch(API_URL, {
|
||||
const response = await authFetch(`${API_URL}/${selectedItem.id}/copy`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(copyData),
|
||||
})
|
||||
|
||||
if (!createResponse.ok) {
|
||||
throw new Error('Ошибка при создании копии')
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка при копировании')
|
||||
}
|
||||
|
||||
const newItem = await createResponse.json()
|
||||
|
||||
// Копируем изображение, если оно есть
|
||||
if (itemData.image_url) {
|
||||
try {
|
||||
// Загружаем изображение по URL (используем authFetch для авторизованных запросов)
|
||||
const imageResponse = await authFetch(itemData.image_url)
|
||||
if (imageResponse.ok) {
|
||||
const blob = await imageResponse.blob()
|
||||
// Проверяем, что это изображение и размер не превышает 5MB
|
||||
if (blob.type.startsWith('image/') && blob.size <= 5 * 1024 * 1024) {
|
||||
// Загружаем изображение для нового желания
|
||||
const formData = new FormData()
|
||||
formData.append('image', blob, 'image.jpg')
|
||||
|
||||
const uploadResponse = await authFetch(`${API_URL}/${newItem.id}/image`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
console.error('Ошибка при копировании изображения')
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (imgErr) {
|
||||
console.error('Ошибка при копировании изображения:', imgErr)
|
||||
// Не прерываем процесс, просто логируем ошибку
|
||||
}
|
||||
}
|
||||
const newItem = await response.json()
|
||||
|
||||
setSelectedItem(null)
|
||||
// Открываем экран редактирования нового желания
|
||||
onNavigate?.('wishlist-form', { wishlistId: newItem.id })
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
@@ -283,7 +323,6 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
}).format(price)
|
||||
}
|
||||
|
||||
// Находит первое невыполненное условие
|
||||
const findFirstUnmetCondition = (item) => {
|
||||
if (!item.unlock_conditions || item.unlock_conditions.length === 0) {
|
||||
return null
|
||||
@@ -293,10 +332,8 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
let isMet = false
|
||||
|
||||
if (condition.type === 'task_completion') {
|
||||
// Условие выполнено, если task_completed === true
|
||||
isMet = condition.task_completed === true
|
||||
} else if (condition.type === 'project_points') {
|
||||
// Условие выполнено, если current_points >= required_points
|
||||
const currentPoints = condition.current_points || 0
|
||||
const requiredPoints = condition.required_points || 0
|
||||
isMet = currentPoints >= requiredPoints
|
||||
@@ -380,12 +417,10 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
<div className="card-name">{item.name}</div>
|
||||
|
||||
{(() => {
|
||||
// Показываем первое невыполненное условие, если есть
|
||||
const unmetCondition = findFirstUnmetCondition(item)
|
||||
if (unmetCondition && !item.completed) {
|
||||
return renderUnlockCondition(item)
|
||||
}
|
||||
// Если все условия выполнены или условий нет - показываем цену
|
||||
if (item.price) {
|
||||
return <div className="card-price">{formatPrice(item.price)}</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user