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:
Michilis
2026-08-05 22:05:33 +00:00
co-authored by Cursor
parent 498d7d8a7d
commit fa8686276d
10 changed files with 524 additions and 94 deletions
+51 -9
View File
@@ -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>