Share gallery layout with loading skeletons and add download progress.
Keep the public gallery hero/masonry geometry stable across route and client loading, and show busy state while photos download. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
ArrowDownTrayIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import Spinner from '@/components/ui/Spinner';
|
||||
|
||||
export interface LightboxItem {
|
||||
id: string;
|
||||
@@ -24,17 +25,35 @@ interface LightboxProps {
|
||||
onNavigate: (index: number) => void;
|
||||
/** Extra per-item action buttons rendered in the top bar (admin use). */
|
||||
renderActions?: (item: LightboxItem, index: number) => React.ReactNode;
|
||||
/**
|
||||
* Handles the download in JS instead of navigating, so the button can show
|
||||
* progress. Without it the button stays a plain <a download>.
|
||||
*/
|
||||
onDownload?: (item: LightboxItem) => void;
|
||||
/** Id of the item currently downloading (pairs with onDownload). */
|
||||
downloadingId?: string | null;
|
||||
downloadLabels?: { download: string; downloading: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-screen photo lightbox with keyboard and swipe navigation, in the
|
||||
* style of the admin gallery preview modal (fixed inset-0 bg-black/90).
|
||||
*/
|
||||
export default function Lightbox({ items, index, onClose, onNavigate, renderActions }: LightboxProps) {
|
||||
export default function Lightbox({
|
||||
items,
|
||||
index,
|
||||
onClose,
|
||||
onNavigate,
|
||||
renderActions,
|
||||
onDownload,
|
||||
downloadingId,
|
||||
downloadLabels,
|
||||
}: LightboxProps) {
|
||||
const touchStart = useRef<{ x: number; y: number } | null>(null);
|
||||
const activeThumbRef = useRef<HTMLButtonElement | null>(null);
|
||||
const item = items[index];
|
||||
const hasMultiple = items.length > 1;
|
||||
const downloading = !!downloadingId && item?.id === downloadingId;
|
||||
|
||||
const prev = useCallback(() => {
|
||||
onNavigate(index > 0 ? index - 1 : items.length - 1);
|
||||
@@ -98,14 +117,37 @@ export default function Lightbox({ items, index, onClose, onNavigate, renderActi
|
||||
className="absolute top-4 left-4 z-10 flex items-center gap-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<a
|
||||
href={item.downloadUrl}
|
||||
download={item.filename || true}
|
||||
className="text-white hover:text-gray-300"
|
||||
aria-label="Download"
|
||||
>
|
||||
<ArrowDownTrayIcon className="w-7 h-7" />
|
||||
</a>
|
||||
{onDownload ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDownload(item)}
|
||||
disabled={downloading}
|
||||
aria-busy={downloading}
|
||||
aria-label={
|
||||
downloading
|
||||
? downloadLabels?.downloading || 'Downloading…'
|
||||
: downloadLabels?.download || 'Download'
|
||||
}
|
||||
className={`text-white hover:text-gray-300 flex items-center ${
|
||||
downloading ? 'cursor-wait' : ''
|
||||
}`}
|
||||
>
|
||||
{downloading ? (
|
||||
<Spinner className="w-6 h-6" />
|
||||
) : (
|
||||
<ArrowDownTrayIcon className="w-7 h-7" />
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={item.downloadUrl}
|
||||
download={item.filename || true}
|
||||
className="text-white hover:text-gray-300"
|
||||
aria-label="Download"
|
||||
>
|
||||
<ArrowDownTrayIcon className="w-7 h-7" />
|
||||
</a>
|
||||
)}
|
||||
{renderActions?.(item, index)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user