feat: добавлено автозаполнение полей wishlist из ссылки (v3.9.0)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
- Добавлен эндпоинт /api/wishlist/metadata для извлечения метаданных из URL - Реализовано извлечение Open Graph тегов (title, image, description) - Добавлена кнопка Pull для ручной загрузки информации из ссылки - Автоматическое заполнение полей: название, цена, картинка - Обновлена версия до 3.9.0
This commit is contained in:
318
play-life-web/src/components/Wishlist.jsx
Normal file
318
play-life-web/src/components/Wishlist.jsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from './auth/AuthContext'
|
||||
import LoadingError from './LoadingError'
|
||||
import './Wishlist.css'
|
||||
|
||||
const API_URL = '/api/wishlist'
|
||||
|
||||
function Wishlist({ onNavigate, refreshTrigger = 0 }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [items, setItems] = useState([])
|
||||
const [completed, setCompleted] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [completedExpanded, setCompletedExpanded] = useState(false)
|
||||
const [completedLoading, setCompletedLoading] = useState(false)
|
||||
const [selectedItem, setSelectedItem] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchWishlist()
|
||||
}, [])
|
||||
|
||||
// Обновляем данные при изменении refreshTrigger
|
||||
useEffect(() => {
|
||||
if (refreshTrigger > 0) {
|
||||
fetchWishlist()
|
||||
// Если завершённые развёрнуты, обновляем и их
|
||||
if (completedExpanded) {
|
||||
fetchWishlist(true)
|
||||
}
|
||||
}
|
||||
}, [refreshTrigger])
|
||||
|
||||
const fetchWishlist = async (includeCompleted = false) => {
|
||||
try {
|
||||
if (includeCompleted) {
|
||||
setCompletedLoading(true)
|
||||
} else {
|
||||
setLoading(true)
|
||||
}
|
||||
|
||||
const url = includeCompleted ? `${API_URL}?include_completed=true` : API_URL
|
||||
const response = await authFetch(url)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка при загрузке желаний')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
// Объединяем разблокированные и заблокированные в один список
|
||||
const allItems = [...(data.unlocked || []), ...(data.locked || [])]
|
||||
setItems(allItems)
|
||||
if (includeCompleted) {
|
||||
setCompleted(data.completed || [])
|
||||
}
|
||||
setError('')
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setItems([])
|
||||
if (includeCompleted) {
|
||||
setCompleted([])
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setCompletedLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleCompleted = () => {
|
||||
const newExpanded = !completedExpanded
|
||||
setCompletedExpanded(newExpanded)
|
||||
if (newExpanded && completed.length === 0) {
|
||||
fetchWishlist(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddClick = () => {
|
||||
onNavigate?.('wishlist-form', { wishlistId: undefined })
|
||||
}
|
||||
|
||||
const handleItemClick = (item) => {
|
||||
onNavigate?.('wishlist-detail', { wishlistId: item.id })
|
||||
}
|
||||
|
||||
const handleMenuClick = (item, e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedItem(item)
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
if (selectedItem) {
|
||||
onNavigate?.('wishlist-form', { wishlistId: selectedItem.id })
|
||||
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)
|
||||
await fetchWishlist(completedExpanded)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setSelectedItem(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (!selectedItem) return
|
||||
|
||||
try {
|
||||
const response = await authFetch(`${API_URL}/${selectedItem.id}/complete`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка при завершении')
|
||||
}
|
||||
|
||||
setSelectedItem(null)
|
||||
await fetchWishlist(completedExpanded)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setSelectedItem(null)
|
||||
}
|
||||
}
|
||||
|
||||
const formatPrice = (price) => {
|
||||
return new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
currency: 'RUB',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(price)
|
||||
}
|
||||
|
||||
const renderUnlockCondition = (item) => {
|
||||
if (item.unlocked || item.completed) return null
|
||||
if (!item.first_locked_condition) return null
|
||||
|
||||
const condition = item.first_locked_condition
|
||||
const moreCount = item.more_locked_conditions || 0
|
||||
|
||||
let conditionText = ''
|
||||
if (condition.type === 'task_completion') {
|
||||
conditionText = condition.task_name || 'Задача'
|
||||
} else {
|
||||
const points = condition.required_points || 0
|
||||
const project = condition.project_name || 'Проект'
|
||||
let period = ''
|
||||
if (condition.period_type) {
|
||||
const periodLabels = {
|
||||
week: 'за неделю',
|
||||
month: 'за месяц',
|
||||
year: 'за год',
|
||||
}
|
||||
period = ' ' + periodLabels[condition.period_type] || ''
|
||||
}
|
||||
conditionText = `${points} в ${project}${period}`
|
||||
}
|
||||
|
||||
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>
|
||||
{item.price && (
|
||||
<div className="card-price">{formatPrice(item.price)}</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>
|
||||
|
||||
{isFaded && !item.completed ? (
|
||||
renderUnlockCondition(item)
|
||||
) : (
|
||||
item.price && <div className="card-price">{formatPrice(item.price)}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="wishlist">
|
||||
<LoadingError onRetry={() => fetchWishlist()} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wishlist">
|
||||
{/* Кнопка добавления */}
|
||||
<button onClick={handleAddClick} className="add-wishlist-button">
|
||||
Добавить
|
||||
</button>
|
||||
|
||||
{/* Основной список (разблокированные и заблокированные вместе) */}
|
||||
{items.length > 0 && (
|
||||
<div className="wishlist-grid">
|
||||
{items.map(renderItem)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Завершённые */}
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Модальное окно для действий */}
|
||||
{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>
|
||||
{!selectedItem.completed && selectedItem.unlocked && (
|
||||
<button className="wishlist-modal-complete" onClick={handleComplete}>
|
||||
Завершить
|
||||
</button>
|
||||
)}
|
||||
<button className="wishlist-modal-delete" onClick={handleDelete}>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Wishlist
|
||||
|
||||
Reference in New Issue
Block a user