Add an admin page for managing gallery images and artist names
Introduce a new admin route at `/admin/gallery` enabling users to set focal points and artist names for gallery images, with corresponding API endpoints and data management. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 70b6b9df-7da9-46f3-afd7-dbcd9aac1855 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:
@@ -2,34 +2,24 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { ChevronLeft, ChevronRight, X, Images, Maximize2 } from "lucide-react";
|
||||
|
||||
interface GalleryImage {
|
||||
export interface GalleryImage {
|
||||
folder: string;
|
||||
fileName: string;
|
||||
thumb: string;
|
||||
large: string;
|
||||
mobile?: string;
|
||||
full?: string;
|
||||
artist?: string;
|
||||
}
|
||||
|
||||
export type FocalPointMap = Record<string, { x: number; y: number }>;
|
||||
|
||||
const PAGE_SIZE = 24;
|
||||
|
||||
function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||
useEffect(() => {
|
||||
const handler = () => setIsMobile(window.innerWidth < 768);
|
||||
window.addEventListener("resize", handler);
|
||||
return () => window.removeEventListener("resize", handler);
|
||||
}, []);
|
||||
return isMobile;
|
||||
}
|
||||
|
||||
function cloudinaryTransform(src: string, transform: string) {
|
||||
if (!src.includes("res.cloudinary.com")) return src;
|
||||
return src.replace(/\/upload\/[^/]+\//, `/upload/${transform}/`);
|
||||
}
|
||||
|
||||
function thumbUrl(src: string) {
|
||||
return src;
|
||||
export function getObjectPosition(fileName: string, focalPoints: FocalPointMap, fallback = "center 15%"): string {
|
||||
const fp = focalPoints[fileName];
|
||||
if (fp) return `${fp.x}% ${fp.y}%`;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function LazyImage({ src, alt, className, onClick, objectPosition }: { src: string; alt: string; className?: string; onClick?: () => void; objectPosition?: string }) {
|
||||
@@ -72,10 +62,12 @@ function Lightbox({
|
||||
images,
|
||||
startIndex,
|
||||
onClose,
|
||||
focalPoints,
|
||||
}: {
|
||||
images: GalleryImage[];
|
||||
startIndex: number;
|
||||
onClose: () => void;
|
||||
focalPoints: FocalPointMap;
|
||||
}) {
|
||||
const [index, setIndex] = useState(startIndex);
|
||||
|
||||
@@ -148,11 +140,13 @@ function SingleImageCarousel({
|
||||
autoPlay = true,
|
||||
interval = 5000,
|
||||
onExpand,
|
||||
focalPoints,
|
||||
}: {
|
||||
images: GalleryImage[];
|
||||
autoPlay?: boolean;
|
||||
interval?: number;
|
||||
onExpand?: (index: number) => void;
|
||||
focalPoints: FocalPointMap;
|
||||
}) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
@@ -181,7 +175,7 @@ function SingleImageCarousel({
|
||||
src={current.large || current.thumb}
|
||||
alt={current.artist || current.fileName}
|
||||
className="absolute inset-0 w-full h-full object-cover transition-opacity duration-500"
|
||||
style={{ objectPosition: "center 35%" }}
|
||||
style={{ objectPosition: getObjectPosition(current.fileName, focalPoints, "center 35%") }}
|
||||
loading="lazy"
|
||||
data-testid="img-gallery-current"
|
||||
/>
|
||||
@@ -245,6 +239,9 @@ export function PhotoGalleryWidget({ reverseOrder = false }: { reverseOrder?: bo
|
||||
queryKey: ["/api/gallery", "widget"],
|
||||
queryFn: () => fetch("/api/gallery?limit=30").then((r) => r.json()),
|
||||
});
|
||||
const { data: focalPoints } = useQuery<FocalPointMap>({
|
||||
queryKey: ["/api/gallery/focal-points"],
|
||||
});
|
||||
const images = reverseOrder && rawImages ? [...rawImages].reverse() : rawImages;
|
||||
|
||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||||
@@ -275,6 +272,7 @@ export function PhotoGalleryWidget({ reverseOrder = false }: { reverseOrder?: bo
|
||||
autoPlay={true}
|
||||
interval={10000}
|
||||
onExpand={(i) => setLightboxIndex(i)}
|
||||
focalPoints={focalPoints || {}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -283,6 +281,7 @@ export function PhotoGalleryWidget({ reverseOrder = false }: { reverseOrder?: bo
|
||||
images={images}
|
||||
startIndex={lightboxIndex}
|
||||
onClose={() => setLightboxIndex(null)}
|
||||
focalPoints={focalPoints || {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -290,15 +289,18 @@ export function PhotoGalleryWidget({ reverseOrder = false }: { reverseOrder?: bo
|
||||
}
|
||||
|
||||
export default function GalleryPage() {
|
||||
const isMobile = useIsMobile();
|
||||
const { data: images, isLoading } = useQuery<GalleryImage[]>({
|
||||
queryKey: ["/api/gallery"],
|
||||
});
|
||||
const { data: focalPoints } = useQuery<FocalPointMap>({
|
||||
queryKey: ["/api/gallery/focal-points"],
|
||||
});
|
||||
|
||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||||
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fp = focalPoints || {};
|
||||
const totalCount = images?.length || 0;
|
||||
const visibleImages = images?.slice(0, visibleCount) || [];
|
||||
const hasMore = visibleCount < totalCount;
|
||||
@@ -341,10 +343,10 @@ export default function GalleryPage() {
|
||||
>
|
||||
<div className="relative w-full aspect-[16/9]">
|
||||
<LazyImage
|
||||
src={img.mobile || thumbUrl(img.thumb)}
|
||||
src={img.mobile || img.thumb}
|
||||
alt={img.artist || img.fileName}
|
||||
className="absolute inset-0 w-full h-full transition-transform duration-500 group-hover:scale-110"
|
||||
objectPosition="center 15%"
|
||||
objectPosition={getObjectPosition(img.fileName, fp)}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
||||
{img.artist && (
|
||||
@@ -387,6 +389,7 @@ export default function GalleryPage() {
|
||||
images={images}
|
||||
startIndex={lightboxIndex}
|
||||
onClose={() => setLightboxIndex(null)}
|
||||
focalPoints={fp}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user