v3.9.5: Добавлена возможность копирования желаний, исправлена замена изображений
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 48s

This commit is contained in:
poignatov
2026-01-12 17:42:51 +03:00
parent 3cf3cd4edb
commit 705eb2400e
9 changed files with 509 additions and 162 deletions

View File

@@ -9,37 +9,52 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
const { authFetch } = useAuth()
const [items, setItems] = useState([])
const [completed, setCompleted] = useState([])
const [completedCount, setCompletedCount] = useState(0)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [completedExpanded, setCompletedExpanded] = useState(false)
const [completedLoading, setCompletedLoading] = useState(false)
const [selectedItem, setSelectedItem] = useState(null)
const [tasks, setTasks] = useState([])
const [projects, setProjects] = useState([])
useEffect(() => {
fetchWishlist()
loadTasksAndProjects()
}, [])
const loadTasksAndProjects = async () => {
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 : [])
}
} catch (err) {
console.error('Error loading tasks and projects:', err)
}
}
// Обновляем данные при изменении refreshTrigger
useEffect(() => {
if (refreshTrigger > 0) {
fetchWishlist()
// Если завершённые развёрнуты, обновляем и их
if (completedExpanded) {
fetchWishlist(true)
}
}
}, [refreshTrigger])
const fetchWishlist = async (includeCompleted = false) => {
const fetchWishlist = async () => {
try {
if (includeCompleted) {
setCompletedLoading(true)
} else {
setLoading(true)
}
setLoading(true)
const url = includeCompleted ? `${API_URL}?include_completed=true` : API_URL
const response = await authFetch(url)
const response = await authFetch(API_URL)
if (!response.ok) {
throw new Error('Ошибка при загрузке желаний')
@@ -49,18 +64,42 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
// Объединяем разблокированные и заблокированные в один список
const allItems = [...(data.unlocked || []), ...(data.locked || [])]
setItems(allItems)
if (includeCompleted) {
setCompleted(data.completed || [])
const count = data.completed_count || 0
setCompletedCount(count)
// Загружаем завершённые сразу, если они есть
if (count > 0) {
fetchCompleted()
} else {
setCompleted([])
}
setError('')
} catch (err) {
setError(err.message)
setItems([])
if (includeCompleted) {
setCompleted([])
}
setCompleted([])
setCompletedCount(0)
} finally {
setLoading(false)
}
}
const fetchCompleted = async () => {
try {
setCompletedLoading(true)
const response = await authFetch(`${API_URL}?include_completed=true`)
if (!response.ok) {
throw new Error('Ошибка при загрузке завершённых желаний')
}
const data = await response.json()
setCompleted(data.completed || [])
} catch (err) {
console.error('Error fetching completed items:', err)
setCompleted([])
} finally {
setCompletedLoading(false)
}
}
@@ -68,8 +107,8 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
const handleToggleCompleted = () => {
const newExpanded = !completedExpanded
setCompletedExpanded(newExpanded)
if (newExpanded && completed.length === 0) {
fetchWishlist(true)
if (newExpanded && completed.length === 0 && completedCount > 0) {
fetchCompleted()
}
}
@@ -133,6 +172,108 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
}
}
const handleCopy = async () => {
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, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(copyData),
})
if (!createResponse.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)
// Не прерываем процесс, просто логируем ошибку
}
}
setSelectedItem(null)
// Открываем экран редактирования нового желания
onNavigate?.('wishlist-form', { wishlistId: newItem.id })
} catch (err) {
setError(err.message)
setSelectedItem(null)
}
}
const formatPrice = (price) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
@@ -142,12 +283,38 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
}).format(price)
}
const renderUnlockCondition = (item) => {
if (item.unlocked || item.completed) return null
if (!item.first_locked_condition) return null
// Находит первое невыполненное условие
const findFirstUnmetCondition = (item) => {
if (!item.unlock_conditions || item.unlock_conditions.length === 0) {
return null
}
const condition = item.first_locked_condition
const moreCount = item.more_locked_conditions || 0
for (const condition of item.unlock_conditions) {
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
}
if (!isMet) {
return condition
}
}
return null
}
const renderUnlockCondition = (item) => {
if (item.completed) return null
const condition = findFirstUnmetCondition(item)
if (!condition) return null
let conditionText = ''
if (condition.type === 'task_completion') {
@@ -155,16 +322,14 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
} 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] || ''
let dateText = ''
if (condition.start_date) {
const date = new Date(condition.start_date + 'T00:00:00')
dateText = ` с ${date.toLocaleDateString('ru-RU')}`
} else {
dateText = ' за всё время'
}
conditionText = `${points} в ${project}${period}`
conditionText = `${points} в ${project}${dateText}`
}
return (
@@ -176,9 +341,6 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
</svg>
<span className="condition-text">{conditionText}</span>
</div>
{item.price && (
<div className="card-price">{formatPrice(item.price)}</div>
)}
</div>
</div>
)
@@ -217,11 +379,18 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
<div className="card-name">{item.name}</div>
{isFaded && !item.completed ? (
renderUnlockCondition(item)
) : (
item.price && <div className="card-price">{formatPrice(item.price)}</div>
)}
{(() => {
// Показываем первое невыполненное условие, если есть
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
})()}
</div>
)
}
@@ -261,28 +430,32 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
</div>
)}
{/* Завершённые */}
<div className="section-divider">
<button
className="completed-toggle"
onClick={handleToggleCompleted}
>
<span className="completed-toggle-icon">
{completedExpanded ? '▼' : '▶'}
</span>
<span>Завершённые</span>
</button>
</div>
{completedExpanded && (
{/* Завершённые - показываем только если есть завершённые желания */}
{completedCount > 0 && (
<>
{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>
<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>
)}
</>
)}
</>
)}
@@ -298,6 +471,9 @@ function Wishlist({ onNavigate, refreshTrigger = 0 }) {
<button className="wishlist-modal-edit" onClick={handleEdit}>
Редактировать
</button>
<button className="wishlist-modal-copy" onClick={handleCopy}>
Копировать
</button>
{!selectedItem.completed && selectedItem.unlocked && (
<button className="wishlist-modal-complete" onClick={handleComplete}>
Завершить