From d9278f6fb6e7ec2c1a8f3dca5a59c8b119f33ad1 Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Fri, 6 Mar 2026 10:55:08 +0000 Subject: [PATCH] Update admin gallery to fetch images directly from Cloudinary Integrates Cloudinary API to fetch image details for the admin gallery page, replacing previous data sources. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: eb361b34-3147-44e1-a7c3-11100d3e83b8 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 --- client/src/pages/admin-gallery.tsx | 170 +++++++++++++++++++---------- server/cloudinary.ts | 57 ++++++++++ server/gallery-focal-points.json | 8 ++ server/routes.ts | 23 ++++ 4 files changed, 203 insertions(+), 55 deletions(-) diff --git a/client/src/pages/admin-gallery.tsx b/client/src/pages/admin-gallery.tsx index 278846d..8785646 100644 --- a/client/src/pages/admin-gallery.tsx +++ b/client/src/pages/admin-gallery.tsx @@ -1,12 +1,32 @@ 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 } from "lucide-react"; +import { ArrowLeft, Crosshair, X, Check, RotateCcw, User, Search, Filter, Cloud, HardDrive, Image } from "lucide-react"; import { apiRequest } from "@/lib/queryClient"; -import type { GalleryImage, FocalPointMap } from "@/components/photo-gallery"; +import type { FocalPointMap } from "@/components/photo-gallery"; import { getObjectPosition } from "@/components/photo-gallery"; -type ArtistOverrides = Record; +interface CloudinaryImage { + publicId: string; + fileName: string; + artist: string; + artistOverridden: boolean; + width: number; + height: number; + format: string; + bytes: number; + createdAt: string; + thumb: string; + large: string; + mobile: string; + full: string; +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; + return (bytes / (1024 * 1024)).toFixed(1) + " MB"; +} function FocalPointEditor({ image, @@ -17,7 +37,7 @@ function FocalPointEditor({ onSaveArtist, onClose, }: { - image: GalleryImage; + image: CloudinaryImage; currentFocalPoint?: { x: number; y: number }; currentArtist: string; onSaveFocal: (x: number, y: number) => Promise; @@ -64,25 +84,28 @@ function FocalPointEditor({ return (
-
+
- - {image.fileName} - +
+ + {image.artist || image.fileName} + + {image.publicId} · {image.width}x{image.height} · {formatBytes(image.bytes)} +
-
+
setArtistName(e.target.value)} - placeholder="Interpret / Künstlername" + placeholder="Interpret / Künstlername eingeben..." className="flex-1 bg-white/10 border border-white/20 rounded px-3 py-2 text-white text-sm placeholder:text-white/30 focus:outline-none focus:border-yellow-400/60" data-testid="input-artist-name" /> @@ -179,27 +202,32 @@ function FocalPointEditor({ export default function AdminGalleryPage() { const queryClient = useQueryClient(); - const { data: images, isLoading } = useQuery({ - queryKey: ["/api/gallery"], + const { data: images, isLoading, error } = useQuery({ + queryKey: ["/api/admin/cloudinary-gallery"], }); const { data: focalPoints } = useQuery({ queryKey: ["/api/gallery/focal-points"], }); - const { data: artistOverrides } = useQuery({ - queryKey: ["/api/gallery/artists"], - }); - const [editingImage, setEditingImage] = useState(null); + const [editingImage, setEditingImage] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [filterMode, setFilterMode] = useState<"all" | "no-artist" | "no-focal" | "has-focal">("all"); + const [sortMode, setSortMode] = useState<"name" | "size" | "date">("name"); const fp = focalPoints || {}; - const ao = artistOverrides || {}; - const filteredImages = (images || []).filter((img) => { + const sortedImages = [...(images || [])].sort((a, b) => { + if (sortMode === "size") return b.bytes - a.bytes; + if (sortMode === "date") return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + return a.fileName.localeCompare(b.fileName); + }); + + const filteredImages = sortedImages.filter((img) => { if (searchQuery) { const q = searchQuery.toLowerCase(); - const matches = img.fileName.toLowerCase().includes(q) || (img.artist || "").toLowerCase().includes(q); + const matches = img.fileName.toLowerCase().includes(q) || + img.artist.toLowerCase().includes(q) || + img.publicId.toLowerCase().includes(q); if (!matches) return false; } if (filterMode === "no-artist") return !img.artist; @@ -225,30 +253,40 @@ export default function AdminGalleryPage() { await apiRequest("PUT", `/api/gallery/artists/${encodeURIComponent(editingImage.fileName)}`, { artist }); queryClient.invalidateQueries({ queryKey: ["/api/gallery/artists"] }); queryClient.invalidateQueries({ queryKey: ["/api/gallery"] }); + queryClient.invalidateQueries({ queryKey: ["/api/admin/cloudinary-gallery"] }); }; const totalImages = images?.length || 0; + const totalSize = (images || []).reduce((sum, img) => sum + img.bytes, 0); const withFocal = Object.keys(fp).length; const withoutArtist = (images || []).filter((img) => !img.artist).length; return (
-
-
-
- - - -
-

Galerie Admin

+
+
+
+
+ + + +
+
+ +

Cloudinary Galerie

+
+
-
- {totalImages} Bilder - {withFocal} mit Fokus - 0 ? "text-yellow-400" : ""}>{withoutArtist} ohne Name +
+ {totalImages} Bilder + {formatBytes(totalSize)} + {withFocal} mit Fokus + 0 ? "text-yellow-400" : ""}`}> + {withoutArtist} ohne Name +
@@ -261,12 +299,12 @@ export default function AdminGalleryPage() { type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - placeholder="Suche nach Dateiname oder Künstler..." - className="w-full pl-9 pr-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-yellow-400/50" + placeholder="Suche nach Name, Künstler, Public ID..." + className="w-full pl-9 pr-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-blue-400/50" data-testid="input-admin-search" />
-
+
{(["all", "no-artist", "no-focal", "has-focal"] as const).map((mode) => ( ))} +
+ {(["name", "size", "date"] as const).map((s) => ( + + ))}
- {isLoading ? ( -
- {Array.from({ length: 20 }).map((_, i) => ( -
- ))} + {error ? ( +
+ +

Cloudinary Verbindung fehlgeschlagen

+

{(error as Error).message}

+
+ ) : isLoading ? ( +
+
+

Lade Bilder von Cloudinary...

) : ( <> @@ -300,14 +358,14 @@ export default function AdminGalleryPage() {
{filteredImages.map((img) => (
-
+

{img.artist || Kein Name}

-

{img.fileName}

+

{img.publicId}

+

+ {img.width}x{img.height} · {img.format} · {formatBytes(img.bytes)} +

))} diff --git a/server/cloudinary.ts b/server/cloudinary.ts index 3ff4526..6ce83bb 100644 --- a/server/cloudinary.ts +++ b/server/cloudinary.ts @@ -116,6 +116,63 @@ export async function listCloudinaryGallery(): Promise { } } +export interface CloudinaryResourceInfo { + publicId: string; + fileName: string; + artist: string; + width: number; + height: number; + format: string; + bytes: number; + createdAt: string; + thumb: string; + large: string; + mobile: string; + full: string; +} + +export async function listCloudinaryGalleryDetailed(): Promise { + try { + let allResources: any[] = []; + let nextCursor: string | undefined; + + do { + const result = await cloudinary.api.resources({ + type: "upload", + prefix: GALLERY_FOLDER + "/", + max_results: 500, + resource_type: "image", + ...(nextCursor ? { next_cursor: nextCursor } : {}), + }); + allResources = allResources.concat(result.resources); + nextCursor = result.next_cursor; + } while (nextCursor); + + return allResources.map((r: any) => { + const urls = generateImageUrls(r.public_id); + const rawName = r.public_id.replace(`${GALLERY_FOLDER}/`, "").replace(/_/g, " "); + let originalFileName = rawName; + const ext = r.format || "jpg"; + originalFileName = rawName + "." + ext; + + return { + publicId: r.public_id, + fileName: originalFileName, + artist: extractArtistFromPublicId(r.public_id), + width: r.width, + height: r.height, + format: r.format, + bytes: r.bytes, + createdAt: r.created_at, + ...urls, + }; + }); + } catch (err: any) { + console.error("Cloudinary detailed list failed:", err.message); + return []; + } +} + export function extractArtistFromPublicId(publicId: string): string { const name = publicId.replace(`${GALLERY_FOLDER}/`, "").replace(/_/g, " "); if (/^DSC\d/i.test(name) || /^IMG[\s_]\d/i.test(name)) return ""; diff --git a/server/gallery-focal-points.json b/server/gallery-focal-points.json index 180cb17..905b3ef 100644 --- a/server/gallery-focal-points.json +++ b/server/gallery-focal-points.json @@ -2,5 +2,13 @@ "DSC07135.jpg": { "x": 50, "y": 30 + }, + "Arina 1.jpg": { + "x": 79, + "y": 56 + }, + "Anita Hofmann.jpg": { + "x": 42, + "y": 13 } } \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index 449ed92..8ef76f9 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -7,6 +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 multer from "multer"; import path from "path"; import fs from "fs"; @@ -411,6 +412,28 @@ export async function registerRoutes( } }); + app.get("/api/admin/cloudinary-gallery", async (_req, res) => { + try { + const images = await listCloudinaryGalleryDetailed(); + const aPath = path.join(process.cwd(), "server/gallery-artist-overrides.json"); + let artistOverrides: Record = {}; + if (fs.existsSync(aPath)) { + artistOverrides = JSON.parse(fs.readFileSync(aPath, "utf-8")); + } + const result = images.map((img) => { + const overriddenArtist = artistOverrides[img.fileName]; + return { + ...img, + artist: overriddenArtist || img.artist, + artistOverridden: !!overriddenArtist, + }; + }); + res.json(result); + } 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");