Add functionality to delete images from the gallery and Cloudinary

Implement image deletion functionality, including a confirmation dialog and API endpoint for removing images from Cloudinary and updating the gallery map.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 37efc56d-aeff-47ff-8064-9aa64d8b204c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/ncMMRQ9
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic 2026-03-06 10:58:47 +00:00
parent d9278f6fb6
commit f52f8c7ba0
4 changed files with 124 additions and 4 deletions

View File

@ -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<void>;
onResetFocal: () => Promise<void>;
onSaveArtist: (artist: string) => Promise<void>;
onDelete: () => Promise<void>;
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<HTMLImageElement>(null);
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
@ -172,6 +176,16 @@ function FocalPointEditor({
)}
<div className="flex items-center gap-3 mt-2">
<button
onClick={() => setShowDeleteConfirm(true)}
disabled={saving || deleting}
className="flex items-center gap-1.5 px-3 py-2 rounded bg-red-500/20 hover:bg-red-500/40 text-red-400 hover:text-red-300 text-sm transition-colors disabled:opacity-50 border border-red-500/30"
data-testid="button-delete-image"
>
<Trash2 className="w-3.5 h-3.5" />
Löschen
</button>
<div className="flex-1" />
<button
onClick={handleResetFocal}
disabled={saving}
@ -195,6 +209,58 @@ function FocalPointEditor({
Speichern
</button>
</div>
{showDeleteConfirm && (
<div className="fixed inset-0 z-[60] bg-black/80 flex items-center justify-center" data-testid="delete-confirm-dialog">
<div className="bg-zinc-900 border border-red-500/30 rounded-xl p-6 max-w-sm mx-4 shadow-2xl">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-red-500/20 flex items-center justify-center">
<Trash2 className="w-5 h-5 text-red-400" />
</div>
<div>
<h3 className="text-white font-semibold text-sm">Bild löschen?</h3>
<p className="text-white/40 text-xs">Aus Cloudinary entfernen</p>
</div>
</div>
<p className="text-white/60 text-sm mb-1">
<span className="font-medium text-white">{image.artist || image.fileName}</span>
</p>
<p className="text-white/30 text-xs mb-4">{image.publicId}</p>
<p className="text-red-400/80 text-xs mb-5">
Achtung: Diese Aktion kann nicht rückgängig gemacht werden!
</p>
<div className="flex items-center gap-3 justify-end">
<button
onClick={() => setShowDeleteConfirm(false)}
disabled={deleting}
className="px-4 py-2 rounded bg-white/10 hover:bg-white/20 text-white/70 text-sm transition-colors"
data-testid="button-delete-cancel"
>
Abbrechen
</button>
<button
onClick={async () => {
setDeleting(true);
await onDelete();
setDeleting(false);
setShowDeleteConfirm(false);
onClose();
}}
disabled={deleting}
className="flex items-center gap-1.5 px-4 py-2 rounded bg-red-600 hover:bg-red-500 disabled:bg-red-600/40 text-white font-medium text-sm transition-colors"
data-testid="button-delete-confirm"
>
{deleting ? (
<div className="w-4 h-4 border-2 border-white/50 border-t-white rounded-full animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
Endgültig löschen
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
@ -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)}
/>
)}

View File

@ -92,6 +92,16 @@ export async function uploadToCloudinary(imageUrl: string, fileName: string): Pr
}
}
export async function deleteFromCloudinary(publicId: string): Promise<boolean> {
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<boolean> {
try {
await cloudinary.api.resource(publicId);

View File

@ -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
}
}

View File

@ -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");