diff --git a/VERSION b/VERSION index 9d0fec8..2c54817 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.20.5 +4.20.6 diff --git a/play-life-backend/main.go b/play-life-backend/main.go index 381c53f..49bb2e4 100644 --- a/play-life-backend/main.go +++ b/play-life-backend/main.go @@ -3982,6 +3982,7 @@ func main() { protected.HandleFunc("/api/wishlist/{id}", app.updateWishlistHandler).Methods("PUT", "OPTIONS") protected.HandleFunc("/api/wishlist/{id}", app.deleteWishlistHandler).Methods("DELETE", "OPTIONS") protected.HandleFunc("/api/wishlist/{id}/image", app.uploadWishlistImageHandler).Methods("POST", "OPTIONS") + protected.HandleFunc("/api/wishlist/{id}/image", app.deleteWishlistImageHandler).Methods("DELETE", "OPTIONS") protected.HandleFunc("/api/wishlist/{id}/complete", app.completeWishlistHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/api/wishlist/{id}/uncomplete", app.uncompleteWishlistHandler).Methods("POST", "OPTIONS") protected.HandleFunc("/api/wishlist/{id}/copy", app.copyWishlistHandler).Methods("POST", "OPTIONS") @@ -11866,6 +11867,88 @@ func (a *App) uploadWishlistImageHandler(w http.ResponseWriter, r *http.Request) }) } +// deleteWishlistImageHandler удаляет картинку желания +func (a *App) deleteWishlistImageHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "OPTIONS" { + setCORSHeaders(w) + w.WriteHeader(http.StatusOK) + return + } + setCORSHeaders(w) + + userID, ok := getUserIDFromContext(r) + if !ok { + sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + wishlistID, err := strconv.Atoi(vars["id"]) + if err != nil { + sendErrorWithCORS(w, "Invalid wishlist ID", http.StatusBadRequest) + return + } + + // Проверяем доступ к желанию + hasAccess, _, _, err := a.checkWishlistAccess(wishlistID, userID) + if err == sql.ErrNoRows { + sendErrorWithCORS(w, "Wishlist item not found", http.StatusNotFound) + return + } + if err != nil { + log.Printf("Error checking wishlist access: %v", err) + sendErrorWithCORS(w, fmt.Sprintf("Error checking wishlist access: %v", err), http.StatusInternalServerError) + return + } + if !hasAccess { + sendErrorWithCORS(w, "Access denied", http.StatusForbidden) + return + } + + // Получаем текущий путь к изображению из БД + var currentImagePath sql.NullString + err = a.DB.QueryRow(` + SELECT image_path + FROM wishlist_items + WHERE id = $1 + `, wishlistID).Scan(¤tImagePath) + + if err != nil { + log.Printf("Error getting image path: %v", err) + sendErrorWithCORS(w, "Error getting image path", http.StatusInternalServerError) + return + } + + // Удаляем файл, если он существует + if currentImagePath.Valid && currentImagePath.String != "" { + filePath := filepath.Join("/app", currentImagePath.String) + err = os.Remove(filePath) + if err != nil && !os.IsNotExist(err) { + log.Printf("Error deleting image file: %v", err) + // Продолжаем выполнение даже если файл не найден + } + } + + // Обновляем БД, устанавливая image_path в NULL + _, err = a.DB.Exec(` + UPDATE wishlist_items + SET image_path = NULL, updated_at = NOW() + WHERE id = $1 + `, wishlistID) + + if err != nil { + log.Printf("Error updating database: %v", err) + sendErrorWithCORS(w, "Error updating database", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "Image deleted successfully", + }) +} + // completeWishlistHandler помечает желание как завершённое func (a *App) completeWishlistHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "OPTIONS" { diff --git a/play-life-web/package.json b/play-life-web/package.json index 9c3b543..fb97c0f 100644 --- a/play-life-web/package.json +++ b/play-life-web/package.json @@ -1,6 +1,6 @@ { "name": "play-life-web", - "version": "4.20.5", + "version": "4.20.6", "type": "module", "scripts": { "dev": "vite", diff --git a/play-life-web/src/components/WishlistForm.jsx b/play-life-web/src/components/WishlistForm.jsx index 4e6bd2d..57b2e86 100644 --- a/play-life-web/src/components/WishlistForm.jsx +++ b/play-life-web/src/components/WishlistForm.jsx @@ -16,6 +16,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b const [link, setLink] = useState('') const [imageUrl, setImageUrl] = useState(null) const [imageFile, setImageFile] = useState(null) + const [imageRemoved, setImageRemoved] = useState(false) // Флаг удаления фото const [showCropper, setShowCropper] = useState(false) const [crop, setCrop] = useState({ x: 0, y: 0 }) const [zoom, setZoom] = useState(1) @@ -144,6 +145,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b setPrice(state.price || '') setLink(state.link || '') setImageUrl(state.imageUrl || null) + setImageRemoved(false) // Сбрасываем флаг удаления при восстановлении // Восстанавливаем условия и автоматически добавляем новую задачу const restoredConditions = state.unlockConditions || [] @@ -220,6 +222,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b setLink('') setImageUrl(null) setImageFile(null) + setImageRemoved(false) setUnlockConditions([]) setError('') setShowCropper(false) @@ -244,6 +247,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b setLink(data.link || '') setImageUrl(data.image_url || null) setImageFile(null) // Сбрасываем imageFile при загрузке существующего желания + setImageRemoved(false) // Сбрасываем флаг удаления при загрузке setSelectedProjectId(data.project_id ? String(data.project_id) : '') if (data.unlock_conditions) { setUnlockConditions(data.unlock_conditions.map((cond, idx) => ({ @@ -271,6 +275,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b setLink(data.link || '') setImageUrl(data.image_url || null) setImageFile(null) + setImageRemoved(false) // Сбрасываем флаг удаления при загрузке setSelectedProjectId(data.project_id ? String(data.project_id) : '') } } catch (err) { @@ -286,6 +291,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b setLink('') setImageUrl(null) setImageFile(null) + setImageRemoved(false) setUnlockConditions([]) setSelectedProjectId('') setError('') @@ -353,6 +359,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b reader.onload = () => { setImageUrl(reader.result) setImageFile(blob) + setImageRemoved(false) // Сбрасываем флаг удаления при загрузке из метаданных setShowCropper(true) } reader.readAsDataURL(blob) @@ -413,6 +420,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b reader.onload = () => { setImageFile(file) setImageUrl(reader.result) + setImageRemoved(false) // Сбрасываем флаг удаления при выборе нового фото setShowCropper(true) } reader.readAsDataURL(file) @@ -465,6 +473,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b reader.onload = () => { setImageUrl(reader.result) setImageFile(croppedImage) + setImageRemoved(false) // Сбрасываем флаг удаления при сохранении обрезки setShowCropper(false) } reader.readAsDataURL(croppedImage) @@ -609,8 +618,19 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b const savedItem = await response.json() const itemId = savedItem.id || wishlistId - // Загружаем картинку если есть - if (imageFile && itemId) { + // Удаляем картинку если она была удалена пользователем + if (imageRemoved && itemId) { + const deleteResponse = await authFetch(`${API_URL}/${itemId}/image`, { + method: 'DELETE', + }) + + if (!deleteResponse.ok) { + setToastMessage({ text: 'Желание сохранено, но ошибка при удалении картинки', type: 'warning' }) + } + } + + // Загружаем картинку если есть новое фото + if (imageFile && itemId && !imageRemoved) { const formData = new FormData() formData.append('image', imageFile) @@ -744,6 +764,11 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId, b onClick={() => { setImageUrl(null) setImageFile(null) + setImageRemoved(true) // Устанавливаем флаг удаления + // Сбрасываем file input, чтобы можно было выбрать новое фото + if (fileInputRef.current) { + fileInputRef.current.value = '' + } }} className="remove-image-button" >