diff --git a/client/src/pages/admin-gallery.tsx b/client/src/pages/admin-gallery.tsx index 8785646..88772ca 100644 --- a/client/src/pages/admin-gallery.tsx +++ b/client/src/pages/admin-gallery.tsx @@ -1,7 +1,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useState, useRef } from "react"; import { Link } from "wouter"; -import { ArrowLeft, Crosshair, X, Check, RotateCcw, User, Search, Filter, Cloud, HardDrive, Image } from "lucide-react"; +import { ArrowLeft, Crosshair, X, Check, RotateCcw, User, Search, Filter, Cloud, HardDrive, Image, Trash2 } from "lucide-react"; import { apiRequest } from "@/lib/queryClient"; import type { FocalPointMap } from "@/components/photo-gallery"; import { getObjectPosition } from "@/components/photo-gallery"; @@ -35,6 +35,7 @@ function FocalPointEditor({ onSaveFocal, onResetFocal, onSaveArtist, + onDelete, onClose, }: { image: CloudinaryImage; @@ -43,11 +44,14 @@ function FocalPointEditor({ onSaveFocal: (x: number, y: number) => Promise; onResetFocal: () => Promise; onSaveArtist: (artist: string) => Promise; + onDelete: () => Promise; onClose: () => void; }) { const [point, setPoint] = useState<{ x: number; y: number } | null>(currentFocalPoint || null); const [artistName, setArtistName] = useState(currentArtist); const [saving, setSaving] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleting, setDeleting] = useState(false); const imgRef = useRef(null); const handleClick = (e: React.MouseEvent) => { @@ -172,6 +176,16 @@ function FocalPointEditor({ )}
+ +
+ + {showDeleteConfirm && ( +
+
+
+
+ +
+
+

Bild löschen?

+

Aus Cloudinary entfernen

+
+
+

+ {image.artist || image.fileName} +

+

{image.publicId}

+

+ Achtung: Diese Aktion kann nicht rückgängig gemacht werden! +

+
+ + +
+
+
+ )}
); @@ -256,6 +322,13 @@ export default function AdminGalleryPage() { queryClient.invalidateQueries({ queryKey: ["/api/admin/cloudinary-gallery"] }); }; + const handleDeleteImage = async () => { + if (!editingImage) return; + await apiRequest("POST", `/api/admin/cloudinary-gallery/delete`, { publicId: editingImage.publicId }); + queryClient.invalidateQueries({ queryKey: ["/api/admin/cloudinary-gallery"] }); + queryClient.invalidateQueries({ queryKey: ["/api/gallery"] }); + }; + const totalImages = images?.length || 0; const totalSize = (images || []).reduce((sum, img) => sum + img.bytes, 0); const withFocal = Object.keys(fp).length; @@ -419,6 +492,7 @@ export default function AdminGalleryPage() { onSaveFocal={handleSaveFocalPoint} onResetFocal={handleResetFocalPoint} onSaveArtist={handleSaveArtist} + onDelete={handleDeleteImage} onClose={() => setEditingImage(null)} /> )} diff --git a/server/cloudinary.ts b/server/cloudinary.ts index 6ce83bb..aadc16f 100644 --- a/server/cloudinary.ts +++ b/server/cloudinary.ts @@ -92,6 +92,16 @@ export async function uploadToCloudinary(imageUrl: string, fileName: string): Pr } } +export async function deleteFromCloudinary(publicId: string): Promise { + try { + const result = await cloudinary.uploader.destroy(publicId, { resource_type: "image" }); + return result.result === "ok"; + } catch (err: any) { + console.error(`Cloudinary delete failed for ${publicId}:`, err.message); + return false; + } +} + export async function checkCloudinaryImage(publicId: string): Promise { try { await cloudinary.api.resource(publicId); diff --git a/server/gallery-focal-points.json b/server/gallery-focal-points.json index 905b3ef..acf0281 100644 --- a/server/gallery-focal-points.json +++ b/server/gallery-focal-points.json @@ -4,11 +4,19 @@ "y": 30 }, "Arina 1.jpg": { - "x": 79, - "y": 56 + "x": 70, + "y": 49 }, "Anita Hofmann.jpg": { "x": 42, "y": 13 + }, + "Alpensterne 2 .jpg": { + "x": 46, + "y": 27 + }, + "Aufw rts 1 1 .jpg": { + "x": 18, + "y": 43 } } \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index 8ef76f9..c5ce620 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -7,7 +7,7 @@ import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point"; import { optimizeImage } from "./image-optimizer"; import { getAuthUrl, exchangeCodeForTokens, isConnected, fetchGalleryFromDropbox, getValidAccessToken, migrateGalleryToCloudinary } from "./dropbox"; -import { listCloudinaryGalleryDetailed } from "./cloudinary"; +import { listCloudinaryGalleryDetailed, deleteFromCloudinary } from "./cloudinary"; import multer from "multer"; import path from "path"; import fs from "fs"; @@ -434,6 +434,34 @@ export async function registerRoutes( } }); + app.post("/api/admin/cloudinary-gallery/delete", async (req, res) => { + try { + const { publicId } = req.body; + if (!publicId) { + return res.status(400).json({ message: "publicId required" }); + } + const success = await deleteFromCloudinary(publicId); + if (!success) { + return res.status(500).json({ message: "Löschen fehlgeschlagen" }); + } + + const mapPath = path.join(process.cwd(), "server/cloudinary-gallery-map.json"); + if (fs.existsSync(mapPath)) { + const map = JSON.parse(fs.readFileSync(mapPath, "utf-8")); + for (const [key, val] of Object.entries(map)) { + if (val === publicId) { + delete map[key]; + } + } + fs.writeFileSync(mapPath, JSON.stringify(map, null, 2)); + } + + res.json({ ok: true, deleted: publicId }); + } catch (err: any) { + res.status(500).json({ message: err.message }); + } + }); + app.get("/api/gallery/artists", (_req, res) => { try { const aPath = path.join(process.cwd(), "server/gallery-artist-overrides.json");