performance optimisation
This commit is contained in:
parent
7ea063390d
commit
9f63911c78
@ -60,6 +60,10 @@ const config = {
|
|||||||
clientSecret: process.env.MS_CLIENT_SECRET || '',
|
clientSecret: process.env.MS_CLIENT_SECRET || '',
|
||||||
},
|
},
|
||||||
uploadDir: os.tmpdir(),
|
uploadDir: os.tmpdir(),
|
||||||
|
// Local cache for S3 proxy downloads (improves performance for repeated requests)
|
||||||
|
s3CacheDir: process.env.S3_CACHE_DIR || path.join(os.tmpdir(), 's3-cache'),
|
||||||
|
s3CacheEnabled: process.env.S3_CACHE_ENABLED !== 'false', // Enabled by default
|
||||||
|
s3CacheMaxAge: parseInt(process.env.S3_CACHE_MAX_AGE, 10) || 86400, // 24 hours
|
||||||
email: {
|
email: {
|
||||||
from: 'Tour Builder Platform <app@flatlogic.app>',
|
from: 'Tour Builder Platform <app@flatlogic.app>',
|
||||||
host: 'email-smtp.us-east-1.amazonaws.com',
|
host: 'email-smtp.us-east-1.amazonaws.com',
|
||||||
|
|||||||
@ -13,10 +13,59 @@
|
|||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
const { pipeline } = require('stream/promises');
|
const { pipeline } = require('stream/promises');
|
||||||
const { format } = require('util');
|
const { format } = require('util');
|
||||||
|
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// S3 Cache Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the local cache path for an S3 key
|
||||||
|
*/
|
||||||
|
const getCachePath = (privateUrl) => {
|
||||||
|
// Create a safe filename from the URL
|
||||||
|
const hash = crypto.createHash('md5').update(privateUrl).digest('hex');
|
||||||
|
const ext = path.extname(privateUrl) || '';
|
||||||
|
return path.join(config.s3CacheDir, `${hash}${ext}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a cached file exists and is still valid
|
||||||
|
*/
|
||||||
|
const getCachedFile = async (cachePath) => {
|
||||||
|
try {
|
||||||
|
const stats = await fs.promises.stat(cachePath);
|
||||||
|
const age = (Date.now() - stats.mtimeMs) / 1000;
|
||||||
|
if (age < config.s3CacheMaxAge) {
|
||||||
|
return { stats, valid: true };
|
||||||
|
}
|
||||||
|
return { stats, valid: false };
|
||||||
|
} catch {
|
||||||
|
return { stats: null, valid: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure cache directory exists
|
||||||
|
*/
|
||||||
|
const ensureCacheDir = async () => {
|
||||||
|
try {
|
||||||
|
await fs.promises.mkdir(config.s3CacheDir, { recursive: true });
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'EEXIST') throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate ETag from file stats
|
||||||
|
*/
|
||||||
|
const generateETag = (stats) => {
|
||||||
|
return `"${stats.size.toString(16)}-${stats.mtimeMs.toString(16)}"`;
|
||||||
|
};
|
||||||
const { logger } = require('../utils/logger');
|
const { logger } = require('../utils/logger');
|
||||||
const S3StorageProvider = require('./file/S3StorageProvider');
|
const S3StorageProvider = require('./file/S3StorageProvider');
|
||||||
const LocalStorageProvider = require('./file/LocalStorageProvider');
|
const LocalStorageProvider = require('./file/LocalStorageProvider');
|
||||||
@ -317,24 +366,121 @@ const downloadFile = async (req, res) => {
|
|||||||
|
|
||||||
if (provider === 's3') {
|
if (provider === 's3') {
|
||||||
const s3 = getS3Provider();
|
const s3 = getS3Provider();
|
||||||
|
const cachePath = getCachePath(privateUrl);
|
||||||
|
const useCache = config.s3CacheEnabled;
|
||||||
|
|
||||||
|
// Check local cache first
|
||||||
|
if (useCache) {
|
||||||
|
const { stats, valid } = await getCachedFile(cachePath);
|
||||||
|
|
||||||
|
if (valid && stats) {
|
||||||
|
// Serve from cache
|
||||||
|
const etag = generateETag(stats);
|
||||||
|
|
||||||
|
// Check If-None-Match for conditional request
|
||||||
|
if (req.headers['if-none-match'] === etag) {
|
||||||
|
return res.status(304).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set caching headers
|
||||||
|
res.setHeader('ETag', etag);
|
||||||
|
res.setHeader(
|
||||||
|
'Cache-Control',
|
||||||
|
`public, max-age=${config.s3CacheMaxAge}`,
|
||||||
|
);
|
||||||
|
res.setHeader('Content-Length', stats.size);
|
||||||
|
|
||||||
|
// Determine content type from extension
|
||||||
|
const ext = path.extname(privateUrl).toLowerCase();
|
||||||
|
const mimeTypes = {
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.mp4': 'video/mp4',
|
||||||
|
'.webm': 'video/webm',
|
||||||
|
'.mp3': 'audio/mpeg',
|
||||||
|
'.wav': 'audio/wav',
|
||||||
|
'.pdf': 'application/pdf',
|
||||||
|
};
|
||||||
|
if (mimeTypes[ext]) {
|
||||||
|
res.setHeader('Content-Type', mimeTypes[ext]);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
{
|
||||||
|
provider,
|
||||||
|
privateUrl,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
cached: true,
|
||||||
|
},
|
||||||
|
'File served from cache',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stream from cache
|
||||||
|
return fs.createReadStream(cachePath).pipe(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download from S3
|
||||||
const result = await s3.download(privateUrl, { signal });
|
const result = await s3.download(privateUrl, { signal });
|
||||||
|
|
||||||
if (result.contentType) res.setHeader('Content-Type', result.contentType);
|
if (result.contentType) res.setHeader('Content-Type', result.contentType);
|
||||||
if (result.contentLength)
|
if (result.contentLength)
|
||||||
res.setHeader('Content-Length', result.contentLength);
|
res.setHeader('Content-Length', result.contentLength);
|
||||||
|
|
||||||
if (typeof result.body.pipe === 'function') {
|
// Add caching headers for browser
|
||||||
|
res.setHeader(
|
||||||
|
'Cache-Control',
|
||||||
|
`public, max-age=${config.s3CacheMaxAge}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (useCache && typeof result.body.pipe === 'function') {
|
||||||
|
// Stream to both response and cache file
|
||||||
|
await ensureCacheDir();
|
||||||
|
const cacheStream = fs.createWriteStream(cachePath);
|
||||||
|
|
||||||
|
// Use pipeline for proper error handling
|
||||||
|
const { PassThrough } = require('stream');
|
||||||
|
const passThrough = new PassThrough();
|
||||||
|
|
||||||
|
result.body.pipe(passThrough);
|
||||||
|
passThrough.pipe(res);
|
||||||
|
passThrough.pipe(cacheStream);
|
||||||
|
|
||||||
|
cacheStream.on('error', (err) => {
|
||||||
|
log.warn({ err, cachePath }, 'Failed to write to cache');
|
||||||
|
});
|
||||||
|
} else if (typeof result.body.pipe === 'function') {
|
||||||
result.body.pipe(res);
|
result.body.pipe(res);
|
||||||
} else if (typeof result.body.transformToByteArray === 'function') {
|
} else if (typeof result.body.transformToByteArray === 'function') {
|
||||||
const bytes = await result.body.transformToByteArray();
|
const bytes = await result.body.transformToByteArray();
|
||||||
res.send(Buffer.from(bytes));
|
const buffer = Buffer.from(bytes);
|
||||||
|
|
||||||
|
// Cache the buffer
|
||||||
|
if (useCache) {
|
||||||
|
await ensureCacheDir();
|
||||||
|
fs.promises.writeFile(cachePath, buffer).catch((err) => {
|
||||||
|
log.warn({ err, cachePath }, 'Failed to write to cache');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(buffer);
|
||||||
} else {
|
} else {
|
||||||
res.send(result.body);
|
res.send(result.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
{ provider, privateUrl, duration: Date.now() - startTime },
|
{
|
||||||
'File downloaded',
|
provider,
|
||||||
|
privateUrl,
|
||||||
|
duration: Date.now() - startTime,
|
||||||
|
cached: false,
|
||||||
|
},
|
||||||
|
'File downloaded from S3',
|
||||||
);
|
);
|
||||||
} else if (provider === 'gcloud') {
|
} else if (provider === 'gcloud') {
|
||||||
const { bucket, hash } = getGCloudBucket();
|
const { bucket, hash } = getGCloudBucket();
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import {
|
|||||||
hasAnyEffects,
|
hasAnyEffects,
|
||||||
type ElementEffectProperties,
|
type ElementEffectProperties,
|
||||||
} from '../lib/elementEffects';
|
} from '../lib/elementEffects';
|
||||||
|
import { isNavigationElementType } from '../lib/elementDefaults';
|
||||||
|
import { isBackNavigation } from '../lib/navigationHelpers';
|
||||||
import type { CanvasElement } from '../types/constructor';
|
import type { CanvasElement } from '../types/constructor';
|
||||||
|
|
||||||
interface RuntimeElementProps {
|
interface RuntimeElementProps {
|
||||||
@ -26,6 +28,8 @@ interface RuntimeElementProps {
|
|||||||
onGalleryCardClick?: (cardIndex: number) => void;
|
onGalleryCardClick?: (cardIndex: number) => void;
|
||||||
/** Letterbox styles for constraining fullscreen elements to canvas bounds */
|
/** Letterbox styles for constraining fullscreen elements to canvas bounds */
|
||||||
letterboxStyles?: React.CSSProperties;
|
letterboxStyles?: React.CSSProperties;
|
||||||
|
/** Whether forward navigation is disabled (neighbor pages not yet preloaded) */
|
||||||
|
isForwardNavDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp position to canvas bounds (0-100%)
|
// Clamp position to canvas bounds (0-100%)
|
||||||
@ -38,6 +42,7 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
|||||||
resolveUrl,
|
resolveUrl,
|
||||||
onGalleryCardClick,
|
onGalleryCardClick,
|
||||||
letterboxStyles,
|
letterboxStyles,
|
||||||
|
isForwardNavDisabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
// Clamp coordinates to canvas bounds
|
// Clamp coordinates to canvas bounds
|
||||||
const xPercent = clamp(element.xPercent ?? 50, 0, 100);
|
const xPercent = clamp(element.xPercent ?? 50, 0, 100);
|
||||||
@ -97,6 +102,14 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
|||||||
positionStyle = { ...positionStyle, ...animationStyle };
|
positionStyle = { ...positionStyle, ...animationStyle };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute disabled state for navigation elements
|
||||||
|
// Forward navigation disabled when neighbor pages not preloaded
|
||||||
|
// Back navigation always enabled (previous pages are already visited)
|
||||||
|
const isDisabled =
|
||||||
|
isNavigationElementType(element.type) &&
|
||||||
|
!isBackNavigation(element) &&
|
||||||
|
isForwardNavDisabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='absolute cursor-pointer'
|
className='absolute cursor-pointer'
|
||||||
@ -110,6 +123,7 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
|||||||
resolveUrl={resolveUrl}
|
resolveUrl={resolveUrl}
|
||||||
onGalleryCardClick={onGalleryCardClick}
|
onGalleryCardClick={onGalleryCardClick}
|
||||||
letterboxStyles={letterboxStyles}
|
letterboxStyles={letterboxStyles}
|
||||||
|
isDisabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -48,6 +48,7 @@ import {
|
|||||||
resolveNavigationTarget,
|
resolveNavigationTarget,
|
||||||
isTransitionBlocking,
|
isTransitionBlocking,
|
||||||
isBackNavigation,
|
isBackNavigation,
|
||||||
|
isNavigationType,
|
||||||
} from '../lib/navigationHelpers';
|
} from '../lib/navigationHelpers';
|
||||||
import type { TransitionPhase } from '../types/presentation';
|
import type { TransitionPhase } from '../types/presentation';
|
||||||
import type { CanvasElement } from '../types/constructor';
|
import type { CanvasElement } from '../types/constructor';
|
||||||
@ -249,7 +250,8 @@ export default function RuntimePresentation({
|
|||||||
const { isFadingIn, resetFadeIn } = useBackgroundTransition({
|
const { isFadingIn, resetFadeIn } = useBackgroundTransition({
|
||||||
pageSwitch,
|
pageSwitch,
|
||||||
fadeIn: {
|
fadeIn: {
|
||||||
hasActiveTransition: Boolean(transitionPreview) || pendingTransitionComplete,
|
hasActiveTransition:
|
||||||
|
Boolean(transitionPreview) || pendingTransitionComplete,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -401,6 +403,13 @@ export default function RuntimePresentation({
|
|||||||
[pages, pageSwitch, resetFadeIn, applyPageSelection],
|
[pages, pageSwitch, resetFadeIn, applyPageSelection],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Compute whether all neighbor backgrounds are ready for instant navigation
|
||||||
|
const areNeighborBackgroundsReady =
|
||||||
|
preloadOrchestrator?.areNeighborBackgroundsReady ?? true;
|
||||||
|
|
||||||
|
// Compute disabled state for forward navigation elements
|
||||||
|
const isForwardNavDisabled = !areNeighborBackgroundsReady;
|
||||||
|
|
||||||
const handleElementClick = useCallback(
|
const handleElementClick = useCallback(
|
||||||
(element: CanvasElement) => {
|
(element: CanvasElement) => {
|
||||||
// Block navigation while transition is actively playing or buffering
|
// Block navigation while transition is actively playing or buffering
|
||||||
@ -410,6 +419,17 @@ export default function RuntimePresentation({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block forward navigation if neighbor backgrounds not yet preloaded
|
||||||
|
// Back navigation is always allowed (previous pages are already visited)
|
||||||
|
if (
|
||||||
|
isNavigationType(element.type) &&
|
||||||
|
!isBackNavigation(element) &&
|
||||||
|
!areNeighborBackgroundsReady
|
||||||
|
) {
|
||||||
|
logger.info('Navigation blocked - neighbors not preloaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get navigation context from hook for history-based back navigation
|
// Get navigation context from hook for history-based back navigation
|
||||||
const navContext = getNavigationContext();
|
const navContext = getNavigationContext();
|
||||||
|
|
||||||
@ -437,7 +457,14 @@ export default function RuntimePresentation({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigateToPage, pages, transitionPhase, isBuffering, getNavigationContext],
|
[
|
||||||
|
navigateToPage,
|
||||||
|
pages,
|
||||||
|
transitionPhase,
|
||||||
|
isBuffering,
|
||||||
|
getNavigationContext,
|
||||||
|
areNeighborBackgroundsReady,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handler for gallery card clicks
|
// Handler for gallery card clicks
|
||||||
@ -614,7 +641,6 @@ export default function RuntimePresentation({
|
|||||||
</div>
|
</div>
|
||||||
{/* End page background wrapper */}
|
{/* End page background wrapper */}
|
||||||
|
|
||||||
|
|
||||||
{/* Page elements wrapper - z-[46] keeps it ABOVE carousel slide (z-10) AND carousel controls (z-45).
|
{/* Page elements wrapper - z-[46] keeps it ABOVE carousel slide (z-10) AND carousel controls (z-45).
|
||||||
UI controls (z-50) remain on top.
|
UI controls (z-50) remain on top.
|
||||||
Fades in together with background. */}
|
Fades in together with background. */}
|
||||||
@ -632,6 +658,7 @@ export default function RuntimePresentation({
|
|||||||
handleGalleryCardClick(element, cardIndex)
|
handleGalleryCardClick(element, cardIndex)
|
||||||
}
|
}
|
||||||
letterboxStyles={letterboxStyles}
|
letterboxStyles={letterboxStyles}
|
||||||
|
isForwardNavDisabled={isForwardNavDisabled}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -131,10 +131,7 @@ const decodeImage = (url: string): Promise<void> => {
|
|||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
if (typeof img.decode === 'function') {
|
if (typeof img.decode === 'function') {
|
||||||
img
|
img.decode().then(onReady).catch(onReady); // Resolve even on decode error
|
||||||
.decode()
|
|
||||||
.then(onReady)
|
|
||||||
.catch(onReady); // Resolve even on decode error
|
|
||||||
} else {
|
} else {
|
||||||
onReady();
|
onReady();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* Manages the priority queue and orchestrates downloads based on navigation.
|
* Manages the priority queue and orchestrates downloads based on navigation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
|
||||||
import { useNeighborGraph } from './useNeighborGraph';
|
import { useNeighborGraph } from './useNeighborGraph';
|
||||||
import { useNetworkAware } from './useNetworkAware';
|
import { useNetworkAware } from './useNetworkAware';
|
||||||
import { downloadEventBus } from '../lib/offline/DownloadEventBus';
|
import { downloadEventBus } from '../lib/offline/DownloadEventBus';
|
||||||
@ -60,6 +60,8 @@ interface UsePreloadOrchestratorResult {
|
|||||||
isUrlPreloaded: (url: string) => Promise<boolean>;
|
isUrlPreloaded: (url: string) => Promise<boolean>;
|
||||||
/** Instant lookup - returns decoded blob URL or null */
|
/** Instant lookup - returns decoded blob URL or null */
|
||||||
getReadyBlobUrl: (url: string) => string | null;
|
getReadyBlobUrl: (url: string) => string | null;
|
||||||
|
/** Whether all neighbor page backgrounds are ready for instant navigation */
|
||||||
|
areNeighborBackgroundsReady: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,6 +114,38 @@ export function usePreloadOrchestrator(
|
|||||||
// Use network info for adaptive preloading
|
// Use network info for adaptive preloading
|
||||||
const { networkInfo } = useNetworkAware();
|
const { networkInfo } = useNetworkAware();
|
||||||
|
|
||||||
|
// Compute whether all neighbor page backgrounds are ready for instant navigation
|
||||||
|
// Uses readyUrlsVersion to trigger re-computation when blob URLs become ready
|
||||||
|
const areNeighborBackgroundsReady = useMemo(() => {
|
||||||
|
if (!currentPageId || !enabled) return true; // Assume ready if disabled
|
||||||
|
|
||||||
|
// Use existing neighborGraph infrastructure
|
||||||
|
const neighbors = neighborGraph.getNeighbors(currentPageId, 1);
|
||||||
|
if (neighbors.length === 0) return true; // No neighbors = ready
|
||||||
|
|
||||||
|
// Check if ALL neighbor background images have READY blob URLs
|
||||||
|
// IMPORTANT: Use downloadManager.getReadyBlobUrl() NOT preloadedUrls.has()
|
||||||
|
// preloadedUrls contains URLs that are QUEUED, not URLs that are READY
|
||||||
|
// We need to check if the blob URL is actually available for instant display
|
||||||
|
return neighbors.every(({ pageId }) => {
|
||||||
|
const page = pages.find((p) => p.id === pageId);
|
||||||
|
if (!page) return true; // Page not found = skip
|
||||||
|
|
||||||
|
// If page has background image, check if blob URL is actually ready
|
||||||
|
if (page.background_image_url) {
|
||||||
|
const imageKey = extractStoragePath(page.background_image_url);
|
||||||
|
// Check if blob URL is ready (not just queued)
|
||||||
|
if (!downloadManager.getReadyBlobUrl(imageKey)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If page has only video background (no image), it can stream - consider ready
|
||||||
|
// This allows navigation to video-only pages without blocking
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentPageId, enabled, neighborGraph, pages, readyUrlsVersion]);
|
||||||
|
|
||||||
// Subscribe to blob URL ready events from DownloadManager
|
// Subscribe to blob URL ready events from DownloadManager
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = downloadEventBus.on(
|
const unsubscribe = downloadEventBus.on(
|
||||||
@ -751,5 +785,6 @@ export function usePreloadOrchestrator(
|
|||||||
getCachedBlobUrl,
|
getCachedBlobUrl,
|
||||||
isUrlPreloaded,
|
isUrlPreloaded,
|
||||||
getReadyBlobUrl,
|
getReadyBlobUrl,
|
||||||
|
areNeighborBackgroundsReady,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1063,6 +1063,16 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block forward navigation if neighbor backgrounds not yet preloaded
|
||||||
|
// Back navigation is always allowed (previous pages are already visited)
|
||||||
|
if (
|
||||||
|
!isBackNavigation(element) &&
|
||||||
|
!preloadOrchestrator.areNeighborBackgroundsReady
|
||||||
|
) {
|
||||||
|
logger.info('Navigation blocked - neighbors not preloaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Use shared navigation helpers
|
// Use shared navigation helpers
|
||||||
const direction = getNavigationDirection(element);
|
const direction = getNavigationDirection(element);
|
||||||
|
|
||||||
@ -1492,7 +1502,9 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
>
|
>
|
||||||
<BackdropPortalProvider>
|
<BackdropPortalProvider>
|
||||||
{/* Background wrapper - z-5 keeps it BELOW carousel slide (z-10) */}
|
{/* Background wrapper - z-5 keeps it BELOW carousel slide (z-10) */}
|
||||||
<div className={`absolute inset-0 z-5 ${isFadingIn ? 'animate-crossfade-in' : ''}`}>
|
<div
|
||||||
|
className={`absolute inset-0 z-5 ${isFadingIn ? 'animate-crossfade-in' : ''}`}
|
||||||
|
>
|
||||||
<CanvasBackground
|
<CanvasBackground
|
||||||
backgroundImageUrl={backgroundImageSrc}
|
backgroundImageUrl={backgroundImageSrc}
|
||||||
backgroundVideoUrl={backgroundVideoSrc}
|
backgroundVideoUrl={backgroundVideoSrc}
|
||||||
@ -1543,11 +1555,21 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
isElementReadyForCanvasRender(element));
|
isElementReadyForCanvasRender(element));
|
||||||
if (!shouldRender) return null;
|
if (!shouldRender) return null;
|
||||||
|
|
||||||
|
// Compute disabled state for navigation elements
|
||||||
|
// Forward navigation disabled when:
|
||||||
|
// - Element explicitly disabled (navDisabled)
|
||||||
|
// - Transition is playing or buffering
|
||||||
|
// - Neighbor backgrounds not yet preloaded (forward only)
|
||||||
|
const isForwardNav =
|
||||||
|
isNavigationElementType(element.type) &&
|
||||||
|
!isBackNavigation(element);
|
||||||
const isNavDisabled =
|
const isNavDisabled =
|
||||||
isNavigationElementType(element.type) &&
|
isNavigationElementType(element.type) &&
|
||||||
(element.navDisabled ||
|
(element.navDisabled ||
|
||||||
Boolean(transitionPreview) ||
|
Boolean(transitionPreview) ||
|
||||||
isReverseBuffering);
|
isReverseBuffering ||
|
||||||
|
(isForwardNav &&
|
||||||
|
!preloadOrchestrator.areNeighborBackgroundsReady));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasElementComponent
|
<CanvasElementComponent
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user