303 lines
9.7 KiB
React
303 lines
9.7 KiB
React
|
|
import React, { useState, useEffect, useCallback } from 'react'
|
||
|
|
import { useAuth } from './auth/AuthContext'
|
||
|
|
import LoadingError from './LoadingError'
|
||
|
|
import Toast from './Toast'
|
||
|
|
import './WishlistDetail.css'
|
||
|
|
|
||
|
|
const API_URL = '/api/wishlist'
|
||
|
|
|
||
|
|
function WishlistDetail({ wishlistId, onNavigate, onRefresh }) {
|
||
|
|
const { authFetch } = useAuth()
|
||
|
|
const [wishlistItem, setWishlistItem] = useState(null)
|
||
|
|
const [loading, setLoading] = useState(true)
|
||
|
|
const [loadingWishlist, setLoadingWishlist] = useState(true)
|
||
|
|
const [error, setError] = useState(null)
|
||
|
|
const [isCompleting, setIsCompleting] = useState(false)
|
||
|
|
const [isDeleting, setIsDeleting] = useState(false)
|
||
|
|
const [toastMessage, setToastMessage] = useState(null)
|
||
|
|
|
||
|
|
const fetchWishlistDetail = useCallback(async () => {
|
||
|
|
try {
|
||
|
|
setLoadingWishlist(true)
|
||
|
|
setLoading(true)
|
||
|
|
setError(null)
|
||
|
|
const response = await authFetch(`${API_URL}/${wishlistId}`)
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Ошибка загрузки желания')
|
||
|
|
}
|
||
|
|
const data = await response.json()
|
||
|
|
setWishlistItem(data)
|
||
|
|
} catch (err) {
|
||
|
|
setError(err.message)
|
||
|
|
console.error('Error fetching wishlist detail:', err)
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
setLoadingWishlist(false)
|
||
|
|
}
|
||
|
|
}, [wishlistId, authFetch])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (wishlistId) {
|
||
|
|
fetchWishlistDetail()
|
||
|
|
} else {
|
||
|
|
setWishlistItem(null)
|
||
|
|
setLoading(true)
|
||
|
|
setLoadingWishlist(true)
|
||
|
|
setError(null)
|
||
|
|
}
|
||
|
|
}, [wishlistId, fetchWishlistDetail])
|
||
|
|
|
||
|
|
const handleEdit = () => {
|
||
|
|
onNavigate?.('wishlist-form', { wishlistId: wishlistId })
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleComplete = async () => {
|
||
|
|
if (!wishlistItem || !wishlistItem.unlocked) return
|
||
|
|
|
||
|
|
setIsCompleting(true)
|
||
|
|
try {
|
||
|
|
const response = await authFetch(`${API_URL}/${wishlistId}/complete`, {
|
||
|
|
method: 'POST',
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Ошибка при завершении')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (onRefresh) {
|
||
|
|
onRefresh()
|
||
|
|
}
|
||
|
|
if (onNavigate) {
|
||
|
|
onNavigate('wishlist')
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Error completing wishlist:', err)
|
||
|
|
setToastMessage({ text: err.message || 'Ошибка при завершении', type: 'error' })
|
||
|
|
} finally {
|
||
|
|
setIsCompleting(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleUncomplete = async () => {
|
||
|
|
if (!wishlistItem || !wishlistItem.completed) return
|
||
|
|
|
||
|
|
setIsCompleting(true)
|
||
|
|
try {
|
||
|
|
const response = await authFetch(`${API_URL}/${wishlistId}/uncomplete`, {
|
||
|
|
method: 'POST',
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Ошибка при отмене завершения')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (onRefresh) {
|
||
|
|
onRefresh()
|
||
|
|
}
|
||
|
|
fetchWishlistDetail()
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Error uncompleting wishlist:', err)
|
||
|
|
setToastMessage({ text: err.message || 'Ошибка при отмене завершения', type: 'error' })
|
||
|
|
} finally {
|
||
|
|
setIsCompleting(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleDelete = async () => {
|
||
|
|
if (!wishlistItem) return
|
||
|
|
|
||
|
|
if (!window.confirm('Вы уверены, что хотите удалить это желание?')) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsDeleting(true)
|
||
|
|
try {
|
||
|
|
const response = await authFetch(`${API_URL}/${wishlistId}`, {
|
||
|
|
method: 'DELETE',
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Ошибка при удалении')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (onRefresh) {
|
||
|
|
onRefresh()
|
||
|
|
}
|
||
|
|
if (onNavigate) {
|
||
|
|
onNavigate('wishlist')
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Error deleting wishlist:', err)
|
||
|
|
setToastMessage({ text: err.message || 'Ошибка при удалении', type: 'error' })
|
||
|
|
} finally {
|
||
|
|
setIsDeleting(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const formatPrice = (price) => {
|
||
|
|
return new Intl.NumberFormat('ru-RU', {
|
||
|
|
style: 'currency',
|
||
|
|
currency: 'RUB',
|
||
|
|
minimumFractionDigits: 0,
|
||
|
|
maximumFractionDigits: 0,
|
||
|
|
}).format(price)
|
||
|
|
}
|
||
|
|
|
||
|
|
const renderUnlockConditions = () => {
|
||
|
|
if (!wishlistItem || !wishlistItem.unlock_conditions || wishlistItem.unlock_conditions.length === 0) {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="wishlist-detail-conditions">
|
||
|
|
<h3 className="wishlist-detail-section-title">Условия разблокировки:</h3>
|
||
|
|
{wishlistItem.unlock_conditions.map((condition, index) => {
|
||
|
|
let conditionText = ''
|
||
|
|
let progress = null
|
||
|
|
|
||
|
|
if (condition.type === 'task_completion') {
|
||
|
|
conditionText = condition.task_name || 'Задача'
|
||
|
|
const isCompleted = condition.task_completed === true
|
||
|
|
progress = {
|
||
|
|
type: 'task',
|
||
|
|
completed: isCompleted
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const requiredPoints = condition.required_points || 0
|
||
|
|
const currentPoints = condition.current_points || 0
|
||
|
|
const project = condition.project_name || 'Проект'
|
||
|
|
let period = ''
|
||
|
|
if (condition.period_type) {
|
||
|
|
const periodLabels = {
|
||
|
|
week: 'за неделю',
|
||
|
|
month: 'за месяц',
|
||
|
|
year: 'за год',
|
||
|
|
}
|
||
|
|
period = ' ' + periodLabels[condition.period_type] || ''
|
||
|
|
}
|
||
|
|
conditionText = `${requiredPoints} в ${project}${period}`
|
||
|
|
progress = {
|
||
|
|
type: 'points',
|
||
|
|
current: currentPoints,
|
||
|
|
required: requiredPoints,
|
||
|
|
percentage: requiredPoints > 0 ? Math.min(100, (currentPoints / requiredPoints) * 100) : 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const isMet = wishlistItem.unlocked || (progress?.type === 'task' && progress.completed) ||
|
||
|
|
(progress?.type === 'points' && progress.current >= progress.required)
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div key={index} className={`wishlist-detail-condition ${isMet ? 'met' : 'not-met'}`}>
|
||
|
|
<div className="condition-header">
|
||
|
|
<svg className="condition-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||
|
|
{isMet ? (
|
||
|
|
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||
|
|
) : (
|
||
|
|
<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>
|
||
|
|
{progress && progress.type === 'points' && !isMet && (
|
||
|
|
<div className="condition-progress">
|
||
|
|
<div className="progress-bar">
|
||
|
|
<div
|
||
|
|
className="progress-fill"
|
||
|
|
style={{ width: `${progress.percentage}%` }}
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
<div className="progress-text">
|
||
|
|
{Math.round(progress.current)} / {Math.round(progress.required)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (loadingWishlist) {
|
||
|
|
return (
|
||
|
|
<div className="wishlist-detail">
|
||
|
|
<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="wishlist-detail">
|
||
|
|
<button className="close-x-button" onClick={() => onNavigate?.('wishlist')}>
|
||
|
|
✕
|
||
|
|
</button>
|
||
|
|
<h2>{wishlistItem ? wishlistItem.name : 'Желание'}</h2>
|
||
|
|
|
||
|
|
<div className="wishlist-detail-content">
|
||
|
|
{error && (
|
||
|
|
<LoadingError onRetry={fetchWishlistDetail} />
|
||
|
|
)}
|
||
|
|
|
||
|
|
{!error && wishlistItem && (
|
||
|
|
<>
|
||
|
|
{/* Изображение */}
|
||
|
|
{wishlistItem.image_url && (
|
||
|
|
<div className="wishlist-detail-image">
|
||
|
|
<img src={wishlistItem.image_url} alt={wishlistItem.name} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Цена */}
|
||
|
|
{wishlistItem.price && (
|
||
|
|
<div className="wishlist-detail-price">
|
||
|
|
{formatPrice(wishlistItem.price)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Ссылка */}
|
||
|
|
{wishlistItem.link && (
|
||
|
|
<div className="wishlist-detail-link">
|
||
|
|
<a href={wishlistItem.link} target="_blank" rel="noopener noreferrer">
|
||
|
|
Открыть ссылку
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Условия разблокировки */}
|
||
|
|
{renderUnlockConditions()}
|
||
|
|
|
||
|
|
{/* Кнопка завершения */}
|
||
|
|
{wishlistItem.unlocked && !wishlistItem.completed && (
|
||
|
|
<div className="wishlist-detail-actions">
|
||
|
|
<button
|
||
|
|
onClick={handleComplete}
|
||
|
|
disabled={isCompleting}
|
||
|
|
className="wishlist-detail-complete-button"
|
||
|
|
>
|
||
|
|
{isCompleting ? 'Завершение...' : 'Завершить'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
{toastMessage && (
|
||
|
|
<Toast
|
||
|
|
message={toastMessage.text}
|
||
|
|
type={toastMessage.type}
|
||
|
|
onClose={() => setToastMessage(null)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default WishlistDetail
|
||
|
|
|