lightbox and download button fixes

This commit is contained in:
bflatastic committed 2025-07-24 17:23:10 +07:00
1 parent a7270f4685
commit e97e5c239e
6 files changed
+128 -92

No files matched your search

@@ -224,39 +224,43 @@ export function Activity() {
</div>
}
>
<div className="max-h-[480px] overflow-y-auto">
{jobs.length > 0 && (
<div>
{jobs.map((job) => (
<InProgressCard key={job.job_token} job={job} />
))}
</div>
)}
{isFirstLoad.current ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<FontAwesomeIcon
icon={faSpinnerThird}
spin
size="2x"
className="text-gray-400"
/>
<h3 className="text-gray-300">Retrieving Activities</h3>
</div>
) : jobs.length === 0 && completedItems.length === 0 ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<h3 className="text-lg text-gray-300">No activities yet</h3>
</div>
) : (
<div>
<div className="flex flex-col">
{completedItems.map((item) => (
<CompletedCard key={item.token} job={item} />
{(close) => (
<div className="max-h-[480px] overflow-y-auto">
{jobs.length > 0 && (
<div>
{jobs.map((job) => (
<InProgressCard key={job.job_token} job={job} />
))}
</div>
</div>
)}
</div>
)}
{isFirstLoad.current ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<FontAwesomeIcon
icon={faSpinnerThird}
spin
size="2x"
className="text-gray-400"
/>
<h3 className="text-gray-300">Retrieving Activities</h3>
</div>
) : jobs.length === 0 && completedItems.length === 0 ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<h3 className="text-lg text-gray-300">No activities yet</h3>
</div>
) : (
<div>
<div className="flex flex-col">
{completedItems.map((item) => (
<div key={item.token} onClick={close}>
<CompletedCard job={item} />
</div>
))}
</div>
</div>
)}
</div>
)}
</PopoverMenu>
</Tooltip>
);
@@ -1,7 +1,11 @@
import { useState } from "react";
import dayjs from "dayjs";
import { LightboxModal } from "@storyteller/ui-lightbox-modal";
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
import {
galleryModalLightboxMediaId,
galleryModalLightboxVisible,
galleryModalLightboxImage,
} from "@storyteller/ui-gallery-modal";
import type { GalleryItem } from "@storyteller/ui-gallery-modal";
interface Props {
job: {
@@ -13,18 +17,16 @@ interface Props {
maybe_media_links_thumbnail?: string;
maybe_result?: {
entity_token?: string;
media_links?: {
cdn_url?: string;
};
};
};
}
export function CompletedCard({ job }: Props) {
const [loadError, setLoadError] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const cdnOrigin = GetCdnOrigin();
const mediaPath = job?.public_bucket_path || "";
const downloadLink = `${cdnOrigin}${mediaPath}`;
//const imageUrl = downloadLink;
const fullImageUrl = downloadLink;
// Info: Gallery modal will handle displaying full media. Compute thumbnail only.
// NB: Try to use the thumbnail instead of the link to the full asset.
// Passing around full asset URLs and putting them into image tags is causing CORS errors if those assets are not images.
@@ -36,7 +38,19 @@ export function CompletedCard({ job }: Props) {
<>
<div
className="flex w-full items-center justify-between rounded-lg p-2 text-start transition-all duration-150 hover:cursor-pointer hover:bg-white/10"
onClick={() => setIsModalOpen(true)}
onClick={() => {
galleryModalLightboxMediaId.value =
job.maybe_result?.entity_token || null;
galleryModalLightboxImage.value = {
id: job.maybe_result?.entity_token || job.token,
label: job.maybe_title || "Image",
thumbnail: thumbnailUrl,
fullImage: job.maybe_result?.media_links?.cdn_url || thumbnailUrl,
createdAt: job.updated_at,
mediaClass: undefined,
} as GalleryItem;
galleryModalLightboxVisible.value = true;
}}
>
<div className="flex gap-4">
<div className="flex aspect-square h-14 w-14 justify-center overflow-hidden rounded-lg border border-[#A9A9A9]/50 bg-black/60">
@@ -70,25 +84,7 @@ export function CompletedCard({ job }: Props) {
</a> */}
</div>
<LightboxModal
onCloseGallery={() => setIsModalOpen(false)}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
imageUrl={fullImageUrl}
imageAlt={job.maybe_title ?? "unknown"}
onImageError={() => setLoadError(true)}
title={job.maybe_title}
createdAt={job.updated_at}
mediaId={job.maybe_result?.entity_token}
downloadUrl={downloadLink}
additionalInfo={
job.maybe_style_name && (
<div className="text-sm text-white/60">
Style: {job.maybe_style_name}
</div>
)
}
/>
{/* LightboxModal removed – now using top level GalleryModal */}
</>
);
}
@@ -23,6 +23,9 @@ import {
galleryModalVisibleDuringDrag,
galleryReopenAfterDragSignal,
galleryModalVisibleViewMode,
galleryModalLightboxMediaId,
galleryModalLightboxImage,
galleryModalLightboxVisible,
} from "./galleryModalSignals";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@@ -85,10 +88,9 @@ export const GalleryModal = React.memo(
forceFilter,
}: GalleryModalProps) => {
const [loading, setLoading] = useState(false);
const [lightboxImage, setLightboxImage] = useState<GalleryItem | null>(
null
);
const [isLightboxVisible, setIsLightboxVisible] = useState(false);
// Lightbox state is now handled via signals
const lightboxImageSignal = galleryModalLightboxImage;
const lightboxVisibleSignal = galleryModalLightboxVisible;
const [failedImageUrls] = useState<Set<string>>(new Set());
const [username, setUsername] = useState<string>("");
const forceFilterRef = useRef(forceFilter);
@@ -114,7 +116,7 @@ export const GalleryModal = React.memo(
const [hasMore, setHasMore] = useState(true);
const pageSize = 25;
const imageUrl = lightboxImage?.fullImage || "";
const imageUrl = lightboxImageSignal.value?.fullImage || "";
const api = useMemo(() => new GalleryModalApi(), []);
const usersApi = useMemo(() => new UsersApi(), []);
@@ -296,17 +298,18 @@ export const GalleryModal = React.memo(
if (mode === "select" && onSelectItem) {
onSelectItem(item.id);
} else {
setLightboxImage(item);
setIsLightboxVisible(true);
lightboxImageSignal.value = item;
lightboxVisibleSignal.value = true;
galleryModalLightboxMediaId.value = item.id;
}
},
[mode, onSelectItem]
);
const handleCloseLightbox = useCallback(() => {
setIsLightboxVisible(false);
// Wait for animation to complete before removing the image
setTimeout(() => setLightboxImage(null), 300);
lightboxVisibleSignal.value = false;
lightboxImageSignal.value = null;
galleryModalLightboxMediaId.value = null;
}, []);
const handleDeselectAll = useCallback(() => {
@@ -322,6 +325,18 @@ export const GalleryModal = React.memo(
useSignals();
// Synchronize lightbox image when external media id signal is set
useEffect(() => {
if (galleryModalLightboxMediaId.value && allItems.length > 0) {
const target = allItems.find(
(it) => it.id === galleryModalLightboxMediaId.value
);
if (target) {
lightboxImageSignal.value = target;
}
}
}, [galleryModalLightboxMediaId.value, allItems]);
// Compute gap class based on gridColumns
let gapClass = "gap-0.5";
if (gridColumns <= 4) gapClass = "gap-1.5";
@@ -625,25 +640,23 @@ export const GalleryModal = React.memo(
</div>
</Modal>
<LightboxModal
isOpen={isLightboxVisible}
onClose={handleCloseLightbox}
onCloseGallery={() =>
mode === "view"
? (galleryModalVisibleViewMode.value = false)
: onClose?.()
}
imageUrl={imageUrl}
imageAlt={lightboxImage?.label || ""}
onImageError={() => imageUrl && handleImageError(imageUrl)}
title={lightboxImage?.label}
createdAt={lightboxImage?.createdAt}
downloadUrl={imageUrl}
mediaId={lightboxImage?.id}
onDownloadClicked={onDownloadClicked}
onAddToSceneClicked={onAddToSceneClicked}
mediaClass={lightboxImage?.mediaClass}
/>
{mode === "view" && (
<LightboxModal
isOpen={lightboxVisibleSignal.value}
onClose={handleCloseLightbox}
onCloseGallery={() => (galleryModalVisibleViewMode.value = false)}
imageUrl={imageUrl}
imageAlt={lightboxImageSignal.value?.label || ""}
onImageError={() => imageUrl && handleImageError(imageUrl)}
title={lightboxImageSignal.value?.label}
createdAt={lightboxImageSignal.value?.createdAt}
downloadUrl={imageUrl}
mediaId={lightboxImageSignal.value?.id}
onDownloadClicked={onDownloadClicked}
onAddToSceneClicked={onAddToSceneClicked}
mediaClass={lightboxImageSignal.value?.mediaClass}
/>
)}
</>
);
}
@@ -1,5 +1,11 @@
import { signal } from "@preact/signals-react";
// Gallery Modal Signals
export const galleryModalVisibleDuringDrag = signal(true);
export const galleryReopenAfterDragSignal = signal(false);
export const galleryModalVisibleViewMode = signal(false);
// Lightbox Modal Signals
export const galleryModalLightboxMediaId = signal<string | null>(null);
export const galleryModalLightboxVisible = signal(false);
export const galleryModalLightboxImage = signal<any>(null);
@@ -270,7 +270,7 @@ export function LightboxModal({
</Button>
)}
{downloadUrl &&
{/* {downloadUrl &&
(onDownloadClicked ? (
<Button
icon={faDownToLine}
@@ -294,7 +294,20 @@ export function LightboxModal({
>
<Button icon={faDownToLine}>Download</Button>
</a>
))}
))} */}
{onDownloadClicked && downloadUrl && (
<Button
icon={faDownToLine}
onClick={async (e) => {
e.stopPropagation();
gtagEvent("download_clicked");
await onDownloadClicked(downloadUrl, mediaClass);
}}
>
Download
</Button>
)}
</div>
) : null}
</div>
@@ -40,7 +40,7 @@ interface PopoverMenuProps {
showIconsInList?: boolean;
mode?: "default" | "toggle" | "button" | "hoverSelect";
triggerLabel?: string | ReactNode;
children?: ReactNode;
children?: ReactNode | ((close: () => void) => ReactNode);
buttonClassName?: string;
panelClassName?: string;
onPanelAction?: (action: string) => void;
@@ -269,7 +269,11 @@ export const PopoverMenu = ({
</div>
)}
{mode === "default" && children ? (
<div className="text-sm text-white">{children}</div>
<div className="text-sm text-white">
{typeof children === "function"
? children(close)
: children}
</div>
) : mode === "hoverSelect" ? (
<div className="flex flex-col gap-0 text-sm text-white">
{items.map((item, index) => (