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:
parent
d9278f6fb6
commit
f52f8c7ba0
@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user