preloading fix
This commit is contained in:
parent
b8f2274572
commit
41713d1274
@ -349,13 +349,15 @@ Assets are preloaded directly from S3 for better performance:
|
||||
// 1. Request presigned URLs (max 50 per batch, 1-hour expiry)
|
||||
POST /api/file/presign { urls: ["assets/img.jpg", ...] }
|
||||
|
||||
// 2. Download directly from S3 → Store in Cache API
|
||||
// 3. Create blob URL → Decode image → Store in readyBlobUrlsRef
|
||||
// 2. Download directly from S3 → Store in Cache API (dual key)
|
||||
// 3. Create blob URL → Decode image → Store in readyBlobUrlsRef (dual key)
|
||||
|
||||
// 4. Instant lookup during navigation (O(1))
|
||||
const blobUrl = preloadOrchestrator.getReadyBlobUrl(originalUrl);
|
||||
// 4. Instant lookup during navigation (O(1)) - use storage key for reliability
|
||||
const blobUrl = preloadOrchestrator.getReadyBlobUrl(storageKey);
|
||||
```
|
||||
|
||||
**Storage Key Mapping:** Assets are cached under both download URL and canonical storage key (e.g., `assets/project/video.mp4`). Lookups prioritize storage key because presigned URL signatures change on each resolution. This ensures cache hits even after URL regeneration or page refresh.
|
||||
|
||||
**Preload Priority:**
|
||||
| Type | Priority | Notes |
|
||||
|------|----------|-------|
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -31,6 +31,7 @@ export default function ImageField({
|
||||
fill
|
||||
sizes='100vw'
|
||||
className={`rounded-full object-cover bg-gray-100 dark:bg-dark-900 ${imageClassName}`}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -51,6 +51,7 @@ export default function RuntimePresentation({
|
||||
const [transitionPreview, setTransitionPreview] = useState<{
|
||||
targetPageId: string;
|
||||
videoUrl: string;
|
||||
storageKey: string;
|
||||
isReverse: boolean;
|
||||
} | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
@ -120,6 +121,7 @@ export default function RuntimePresentation({
|
||||
transition: transitionPreview
|
||||
? {
|
||||
videoUrl: transitionPreview.videoUrl,
|
||||
storageKey: transitionPreview.storageKey,
|
||||
reverseMode: transitionPreview.isReverse ? 'reverse' : 'none',
|
||||
targetPageId: transitionPreview.targetPageId,
|
||||
displayName: 'Transition',
|
||||
@ -156,6 +158,7 @@ export default function RuntimePresentation({
|
||||
preload: {
|
||||
preloadedUrls: preloadOrchestrator?.preloadedUrls || new Set(),
|
||||
getCachedBlobUrl: preloadOrchestrator?.getCachedBlobUrl,
|
||||
getReadyBlobUrl: preloadOrchestrator?.getReadyBlobUrl,
|
||||
},
|
||||
});
|
||||
|
||||
@ -374,6 +377,7 @@ export default function RuntimePresentation({
|
||||
setTransitionPreview({
|
||||
targetPageId,
|
||||
videoUrl: resolveAssetPlaybackUrl(transitionVideoUrl),
|
||||
storageKey: transitionVideoUrl, // Raw storage path for cache lookup
|
||||
isReverse: isBack,
|
||||
});
|
||||
} else {
|
||||
@ -682,28 +686,46 @@ export default function RuntimePresentation({
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
>
|
||||
{/* Background image element - CSS backgroundImage provides instant display,
|
||||
Image component enhances with optimized loading. bg-black prevents white flash. */}
|
||||
{/* Background image element - CSS backgroundImage provides instant display.
|
||||
Use native img for blob URLs to prevent repeated fetch requests from Next.js Image. */}
|
||||
{backgroundImageUrl && !backgroundVideoUrl && (
|
||||
<div className='absolute inset-0 pointer-events-none'>
|
||||
<Image
|
||||
key={backgroundImageUrl}
|
||||
src={backgroundImageUrl}
|
||||
alt=''
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-cover'
|
||||
priority
|
||||
unoptimized
|
||||
onLoad={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
onError={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
/>
|
||||
{backgroundImageUrl.startsWith('blob:') ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
key={backgroundImageUrl}
|
||||
src={backgroundImageUrl}
|
||||
alt=''
|
||||
className='absolute inset-0 w-full h-full object-cover'
|
||||
onLoad={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
onError={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
key={backgroundImageUrl}
|
||||
src={backgroundImageUrl}
|
||||
alt=''
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-cover'
|
||||
priority
|
||||
unoptimized
|
||||
onLoad={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
onError={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ export default function UserAvatar({
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='rounded-full object-cover bg-gray-100 dark:bg-slate-800'
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -225,18 +225,47 @@ export function usePageSwitch(
|
||||
|
||||
/**
|
||||
* Resolve a storage path to a displayable URL.
|
||||
* Priority: 1) ready blob URL (instant, already decoded), 2) cached blob URL, 3) presigned URL with fallback
|
||||
* Priority: 1) ready blob URL by storage path, 2) cached blob URL by storage path,
|
||||
* 3) ready blob URL by resolved URL, 4) cached blob URL, 5) presigned URL with fallback
|
||||
*/
|
||||
const resolveToDisplayUrl = useCallback(
|
||||
async (storagePath: string | undefined): Promise<string> => {
|
||||
if (!storagePath) return '';
|
||||
|
||||
const cache = preloadCacheRef.current;
|
||||
|
||||
// 1. Try in-memory storage path lookup first (instant, same session)
|
||||
if (cache?.getReadyBlobUrl) {
|
||||
const readyUrl = cache.getReadyBlobUrl(storagePath);
|
||||
if (readyUrl) {
|
||||
logger.info('Using ready blob URL (storage key)', {
|
||||
storagePath: storagePath.slice(-50),
|
||||
});
|
||||
return readyUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try persistent cache by storage path (survives page refresh)
|
||||
if (cache?.getCachedBlobUrl) {
|
||||
try {
|
||||
const blobUrl = await cache.getCachedBlobUrl(storagePath);
|
||||
if (blobUrl) {
|
||||
createdBlobUrlsRef.current.add(blobUrl);
|
||||
logger.info('Using cached blob URL (storage key)', {
|
||||
storagePath: storagePath.slice(-50),
|
||||
});
|
||||
return blobUrl;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to URL resolution
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Resolve to playback URL and try lookup (fallback for resolved URLs)
|
||||
const originalUrl = resolveAssetPlaybackUrl(storagePath);
|
||||
if (!originalUrl) return '';
|
||||
|
||||
const cache = preloadCacheRef.current;
|
||||
|
||||
// 1. Try instant blob URL lookup (already decoded, ready to paint)
|
||||
// Try instant blob URL lookup by resolved URL
|
||||
if (cache?.getReadyBlobUrl) {
|
||||
const readyUrl = cache.getReadyBlobUrl(originalUrl);
|
||||
if (readyUrl) {
|
||||
@ -245,7 +274,7 @@ export function usePageSwitch(
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback: try cached blob URL (creates new blob, needs decode)
|
||||
// Fallback: try cached blob URL by resolved URL
|
||||
if (cache?.getCachedBlobUrl && cache?.preloadedUrls?.has(originalUrl)) {
|
||||
try {
|
||||
const blobUrl = await cache.getCachedBlobUrl(originalUrl);
|
||||
@ -257,11 +286,11 @@ export function usePageSwitch(
|
||||
return blobUrl;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to fallback
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Load with presigned URL fallback (handles CORS failures)
|
||||
// Load with presigned URL fallback (handles CORS failures)
|
||||
const storageKey = isRelativeStoragePath(storagePath)
|
||||
? storagePath
|
||||
: undefined;
|
||||
@ -271,18 +300,41 @@ export function usePageSwitch(
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolve video/audio URL (no fallback needed, just blob check)
|
||||
* Resolve video/audio URL.
|
||||
* Priority: 1) ready blob URL by storage path, 2) cached blob URL by storage path,
|
||||
* 3) ready blob URL by resolved URL, 4) cached blob URL, 5) resolved URL
|
||||
*/
|
||||
const resolveMediaUrl = useCallback(
|
||||
async (storagePath: string | undefined): Promise<string> => {
|
||||
if (!storagePath) return '';
|
||||
|
||||
const cache = preloadCacheRef.current;
|
||||
|
||||
// 1. Try in-memory storage path lookup first (instant, same session)
|
||||
if (cache?.getReadyBlobUrl) {
|
||||
const readyUrl = cache.getReadyBlobUrl(storagePath);
|
||||
if (readyUrl) {
|
||||
return readyUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try persistent cache by storage path (survives page refresh)
|
||||
if (cache?.getCachedBlobUrl) {
|
||||
try {
|
||||
const blobUrl = await cache.getCachedBlobUrl(storagePath);
|
||||
if (blobUrl) {
|
||||
createdBlobUrlsRef.current.add(blobUrl);
|
||||
return blobUrl;
|
||||
}
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Resolve URL and try lookup by resolved URL
|
||||
const originalUrl = resolveAssetPlaybackUrl(storagePath);
|
||||
if (!originalUrl) return '';
|
||||
|
||||
const cache = preloadCacheRef.current;
|
||||
|
||||
// 1. Try instant blob URL lookup first
|
||||
if (cache?.getReadyBlobUrl) {
|
||||
const readyUrl = cache.getReadyBlobUrl(originalUrl);
|
||||
if (readyUrl) {
|
||||
@ -290,7 +342,6 @@ export function usePageSwitch(
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback: try cached blob URL
|
||||
if (cache?.getCachedBlobUrl && cache?.preloadedUrls?.has(originalUrl)) {
|
||||
try {
|
||||
const blobUrl = await cache.getCachedBlobUrl(originalUrl);
|
||||
|
||||
@ -273,9 +273,10 @@ export function usePreloadOrchestrator(
|
||||
/**
|
||||
* Create a blob URL from cache and decode if image.
|
||||
* Stores the ready-to-display blob URL in readyBlobUrlsRef.
|
||||
* If storageKey is provided, also maps the storage key to the blob URL for canonical lookup.
|
||||
*/
|
||||
const createReadyBlobUrl = useCallback(
|
||||
async (url: string): Promise<void> => {
|
||||
async (url: string, storageKey?: string): Promise<void> => {
|
||||
try {
|
||||
// Get blob from Cache API
|
||||
const blob = await StorageManager.getAsset(url);
|
||||
@ -294,12 +295,19 @@ export function usePreloadOrchestrator(
|
||||
await decodeImage(blobUrl);
|
||||
}
|
||||
|
||||
// Store ready blob URL
|
||||
// Store ready blob URL keyed by download URL
|
||||
readyBlobUrlsRef.current.set(url, blobUrl);
|
||||
preloadedUrls.add(url);
|
||||
|
||||
// Also map storage key for canonical lookup (most reliable)
|
||||
if (storageKey) {
|
||||
readyBlobUrlsRef.current.set(storageKey, blobUrl);
|
||||
preloadedUrls.add(storageKey);
|
||||
}
|
||||
|
||||
logger.info('[PRELOAD] Asset ready', {
|
||||
url: url.slice(-50),
|
||||
storageKey: storageKey?.slice(-50),
|
||||
blobUrl: blobUrl.slice(0, 30),
|
||||
});
|
||||
} catch (error) {
|
||||
@ -345,7 +353,7 @@ export function usePreloadOrchestrator(
|
||||
if (cached) {
|
||||
logger.info('[PRELOAD] Already cached', { url: item.url.slice(-50) });
|
||||
// Create blob URL and decode - makes asset ready to display instantly
|
||||
await createReadyBlobUrl(item.url);
|
||||
await createReadyBlobUrl(item.url, item.storageKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -362,18 +370,34 @@ export function usePreloadOrchestrator(
|
||||
logger.info('[PRELOAD] Download complete', {
|
||||
url: item.url.slice(-50),
|
||||
});
|
||||
await createReadyBlobUrl(item.url);
|
||||
await createReadyBlobUrl(item.url, item.storageKey);
|
||||
if (isPresignedUrl(item.url)) {
|
||||
markPresignedUrlsVerified();
|
||||
}
|
||||
// Also map proxy URL to the same blob URL for fallback lookup
|
||||
// Map proxy URL and storage key to the same blob URL for fallback lookup
|
||||
if (item.storageKey) {
|
||||
const proxyUrl = buildProxyUrl(item.storageKey);
|
||||
const blobUrl = readyBlobUrlsRef.current.get(item.url);
|
||||
if (blobUrl) {
|
||||
// Map proxy URL for fallback compatibility
|
||||
const proxyUrl = buildProxyUrl(item.storageKey);
|
||||
readyBlobUrlsRef.current.set(proxyUrl, blobUrl);
|
||||
preloadedUrls.add(proxyUrl);
|
||||
}
|
||||
|
||||
// Store in Cache API under storage key for post-refresh lookups
|
||||
if (typeof caches !== 'undefined') {
|
||||
try {
|
||||
const cache = await caches.open(OFFLINE_CONFIG.cacheNames.assets);
|
||||
const existingResponse = await cache.match(item.url);
|
||||
if (existingResponse) {
|
||||
await cache.put(item.storageKey, existingResponse.clone());
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('[PRELOAD] Failed to store under storage key', {
|
||||
storageKey: item.storageKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(async (err) => {
|
||||
@ -396,7 +420,7 @@ export function usePreloadOrchestrator(
|
||||
logger.info('[PRELOAD] Proxy download complete', {
|
||||
url: proxyUrl.slice(-60),
|
||||
});
|
||||
await createReadyBlobUrl(proxyUrl);
|
||||
await createReadyBlobUrl(proxyUrl, item.storageKey);
|
||||
} catch (retryErr) {
|
||||
logger.error('[PRELOAD] Proxy download also failed', {
|
||||
url: proxyUrl.slice(-60),
|
||||
|
||||
@ -16,6 +16,7 @@ export type ReverseMode = 'none' | 'reverse' | 'separate';
|
||||
|
||||
export interface TransitionConfig {
|
||||
videoUrl: string;
|
||||
storageKey?: string; // Raw storage path for cache lookup
|
||||
reverseMode: ReverseMode;
|
||||
reverseVideoUrl?: string;
|
||||
durationSec?: number;
|
||||
@ -42,6 +43,7 @@ export interface UseTransitionPlaybackOptions {
|
||||
preload?: {
|
||||
preloadedUrls?: Set<string>;
|
||||
getCachedBlobUrl?: (url: string) => Promise<string | null>;
|
||||
getReadyBlobUrl?: (url: string) => string | null;
|
||||
};
|
||||
}
|
||||
|
||||
@ -414,6 +416,38 @@ export function useTransitionPlayback(
|
||||
};
|
||||
|
||||
const resolvePlayableSource = async (): Promise<string> => {
|
||||
// 1. Try storage key lookup first (most reliable for cache hits)
|
||||
const getReadyBlobUrl = preloadRef.current?.getReadyBlobUrl;
|
||||
const storageKey = currentTransition.storageKey;
|
||||
if (getReadyBlobUrl && storageKey) {
|
||||
const readyUrl = getReadyBlobUrl(storageKey);
|
||||
if (readyUrl) {
|
||||
logger.info('Using ready blob URL from storage key', {
|
||||
storageKey: storageKey.slice(-50),
|
||||
});
|
||||
return readyUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try cached blob URL by storage key (post-refresh scenario)
|
||||
const getCachedBlobUrl = preloadRef.current?.getCachedBlobUrl;
|
||||
if (getCachedBlobUrl && storageKey) {
|
||||
try {
|
||||
const cachedBlobUrl = await getCachedBlobUrl(storageKey);
|
||||
if (cachedBlobUrl) {
|
||||
logger.info('Using cached blob URL from storage key', {
|
||||
storageKey: storageKey.slice(-50),
|
||||
});
|
||||
lastLoadedBlobUrlRef.current = cachedBlobUrl;
|
||||
lastLoadedSourceUrlRef.current = sourceUrl;
|
||||
return cachedBlobUrl;
|
||||
}
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Reuse cached blob URL if same source (existing logic)
|
||||
if (
|
||||
lastLoadedBlobUrlRef.current &&
|
||||
lastLoadedSourceUrlRef.current === sourceUrl
|
||||
@ -439,7 +473,18 @@ export function useTransitionPlayback(
|
||||
return sourceUrl;
|
||||
}
|
||||
|
||||
const getCachedBlobUrl = preloadRef.current?.getCachedBlobUrl;
|
||||
// 4. Try ready blob URL by resolved URL
|
||||
if (getReadyBlobUrl) {
|
||||
const readyUrl = getReadyBlobUrl(sourceUrl);
|
||||
if (readyUrl) {
|
||||
logger.info('Using ready blob URL from resolved URL', {
|
||||
url: sourceUrl.slice(-50),
|
||||
});
|
||||
return readyUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Try cached blob URL by resolved URL
|
||||
if (getCachedBlobUrl) {
|
||||
try {
|
||||
const cachedBlobUrl = await getCachedBlobUrl(sourceUrl);
|
||||
@ -458,6 +503,7 @@ export function useTransitionPlayback(
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Fetch video as blob (network fallback)
|
||||
logger.info('Fetching video as blob for seeking support', {
|
||||
reverseMode: currentTransition.reverseMode,
|
||||
});
|
||||
|
||||
@ -86,8 +86,10 @@ type DragElementState = {
|
||||
|
||||
type TransitionPreviewState = {
|
||||
videoUrl: string;
|
||||
storageKey: string; // Raw storage path for cache lookup
|
||||
reverseMode: 'none' | 'reverse' | 'separate';
|
||||
reverseVideoUrl?: string;
|
||||
reverseStorageKey?: string; // Raw storage path for reverse video cache lookup
|
||||
durationSec?: number;
|
||||
title: string;
|
||||
};
|
||||
@ -679,6 +681,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
transition: transitionPreview
|
||||
? {
|
||||
videoUrl: resolveAssetPlaybackUrl(transitionPreview.videoUrl),
|
||||
storageKey: transitionPreview.storageKey,
|
||||
reverseMode: transitionPreview.reverseMode,
|
||||
reverseVideoUrl: transitionPreview.reverseVideoUrl
|
||||
? resolveAssetPlaybackUrl(transitionPreview.reverseVideoUrl)
|
||||
@ -723,6 +726,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
preload: {
|
||||
preloadedUrls: preloadOrchestrator.preloadedUrls,
|
||||
getCachedBlobUrl: preloadOrchestrator.getCachedBlobUrl,
|
||||
getReadyBlobUrl: preloadOrchestrator.getReadyBlobUrl,
|
||||
},
|
||||
});
|
||||
|
||||
@ -2019,6 +2023,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
|
||||
setTransitionPreview({
|
||||
videoUrl: element.transitionVideoUrl,
|
||||
storageKey: element.transitionVideoUrl, // Raw storage path for cache lookup
|
||||
reverseMode:
|
||||
direction === 'forward'
|
||||
? 'none'
|
||||
@ -2026,6 +2031,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
? 'separate'
|
||||
: 'reverse',
|
||||
reverseVideoUrl: element.reverseVideoUrl,
|
||||
reverseStorageKey: element.reverseVideoUrl,
|
||||
durationSec: element.transitionDurationSec,
|
||||
title: `${element.navLabel || element.label} · ${direction}`,
|
||||
});
|
||||
@ -2187,12 +2193,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
if (element.iconUrl) {
|
||||
return (
|
||||
<div className='relative h-auto w-auto max-h-[220px] max-w-[220px]'>
|
||||
<NextImage
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
||||
alt='Tooltip icon'
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-contain'
|
||||
className='max-h-[220px] max-w-[220px] object-contain'
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
@ -2213,12 +2218,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
if (element.iconUrl) {
|
||||
return (
|
||||
<div className='relative h-auto w-auto max-h-[220px] max-w-[220px]'>
|
||||
<NextImage
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={resolveAssetPlaybackUrl(element.iconUrl)}
|
||||
alt='Description icon'
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-contain'
|
||||
className='max-h-[220px] max-w-[220px] object-contain'
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
@ -2272,12 +2276,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
>
|
||||
{card.imageUrl ? (
|
||||
<div className='relative h-full w-full'>
|
||||
<NextImage
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={resolveAssetPlaybackUrl(card.imageUrl)}
|
||||
alt={card.title || 'Gallery card'}
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-cover'
|
||||
className='absolute inset-0 w-full h-full object-cover'
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
@ -2303,12 +2306,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
<div className='h-20 overflow-hidden rounded bg-gray-100'>
|
||||
{firstSlide?.imageUrl ? (
|
||||
<div className='relative h-full w-full'>
|
||||
<NextImage
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={resolveAssetPlaybackUrl(firstSlide.imageUrl)}
|
||||
alt={firstSlide.caption || 'Carousel slide'}
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-cover'
|
||||
className='absolute inset-0 w-full h-full object-cover'
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
@ -2326,12 +2328,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
<span className='flex items-center gap-1'>
|
||||
{element.carouselPrevIconUrl ? (
|
||||
<div className='relative h-3 w-3'>
|
||||
<NextImage
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={resolveAssetPlaybackUrl(element.carouselPrevIconUrl)}
|
||||
alt='Previous icon'
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-contain'
|
||||
className='w-3 h-3 object-contain'
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
@ -2342,12 +2343,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
Next
|
||||
{element.carouselNextIconUrl ? (
|
||||
<div className='relative h-3 w-3'>
|
||||
<NextImage
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={resolveAssetPlaybackUrl(element.carouselNextIconUrl)}
|
||||
alt='Next icon'
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-contain'
|
||||
className='w-3 h-3 object-contain'
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
@ -2602,21 +2602,36 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
className='absolute inset-0 bg-black overflow-hidden'
|
||||
style={canvasBackgroundStyle}
|
||||
>
|
||||
{/* Background image - CSS backgroundImage on canvas provides instant display,
|
||||
NextImage enhances with optimized loading. bg-black prevents white flash. */}
|
||||
{/* Background image - CSS backgroundImage on canvas provides instant display.
|
||||
Use native img for blob URLs to prevent repeated fetch requests from Next.js Image.
|
||||
NextImage only used for non-blob URLs that benefit from optimization. */}
|
||||
{backgroundImageSrc ? (
|
||||
<div className='absolute inset-0 h-full w-full pointer-events-none select-none'>
|
||||
<NextImage
|
||||
key={`bg_image_${backgroundImageSrc}`}
|
||||
src={backgroundImageSrc}
|
||||
alt='Background'
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-cover'
|
||||
draggable={false}
|
||||
onLoad={() => pageSwitch.markBackgroundReady()}
|
||||
onError={() => pageSwitch.markBackgroundReady()}
|
||||
/>
|
||||
{backgroundImageSrc.startsWith('blob:') ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
key={`bg_image_${backgroundImageSrc}`}
|
||||
src={backgroundImageSrc}
|
||||
alt='Background'
|
||||
className='absolute inset-0 w-full h-full object-cover'
|
||||
draggable={false}
|
||||
onLoad={() => pageSwitch.markBackgroundReady()}
|
||||
onError={() => pageSwitch.markBackgroundReady()}
|
||||
/>
|
||||
) : (
|
||||
<NextImage
|
||||
key={`bg_image_${backgroundImageSrc}`}
|
||||
src={backgroundImageSrc}
|
||||
alt='Background'
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-cover'
|
||||
draggable={false}
|
||||
unoptimized
|
||||
onLoad={() => pageSwitch.markBackgroundReady()}
|
||||
onError={() => pageSwitch.markBackgroundReady()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user