fixed imports issue
This commit is contained in:
parent
0a36a87cd4
commit
ad9c788b21
2
frontend/next-env.d.ts
vendored
2
frontend/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./build/types/routes.d.ts" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -25,8 +25,14 @@ const PageSelector: React.FC<PageSelectorProps> = ({
|
||||
const sortedPages = useMemo(() => {
|
||||
return [...pages].sort((a, b) => {
|
||||
// Primary sort: sort_order ascending (undefined/null goes to end)
|
||||
const orderA = typeof a.sort_order === 'number' ? a.sort_order : Number.MAX_SAFE_INTEGER;
|
||||
const orderB = typeof b.sort_order === 'number' ? b.sort_order : Number.MAX_SAFE_INTEGER;
|
||||
const orderA =
|
||||
typeof a.sort_order === 'number'
|
||||
? a.sort_order
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
const orderB =
|
||||
typeof b.sort_order === 'number'
|
||||
? b.sort_order
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
if (orderA !== orderB) {
|
||||
return orderA - orderB;
|
||||
}
|
||||
|
||||
@ -27,15 +27,15 @@ export const RotatePrompt: React.FC<RotatePromptProps> = ({
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90">
|
||||
<div className="text-center text-white px-8">
|
||||
<div className='fixed inset-0 z-[100] flex items-center justify-center bg-black/90'>
|
||||
<div className='text-center text-white px-8'>
|
||||
<Icon
|
||||
path={mdiScreenRotation}
|
||||
size={3}
|
||||
className="mx-auto mb-6 animate-pulse"
|
||||
className='mx-auto mb-6 animate-pulse'
|
||||
/>
|
||||
<p className="text-lg font-medium">{message}</p>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
<p className='text-lg font-medium'>{message}</p>
|
||||
<p className='text-sm text-gray-400 mt-2'>
|
||||
This presentation is optimized for landscape viewing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -94,7 +94,7 @@ export default function RuntimePresentation({
|
||||
useCanvasScale({
|
||||
designWidth: currentPage?.design_width ?? undefined,
|
||||
designHeight: currentPage?.design_height ?? undefined,
|
||||
});
|
||||
});
|
||||
|
||||
const [transitionPreview, setTransitionPreview] = useState<{
|
||||
targetPageId: string;
|
||||
@ -542,186 +542,193 @@ export default function RuntimePresentation({
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<BackdropPortalProvider>
|
||||
{/* Background image element - z-1 keeps it below backdrop blur (z-5).
|
||||
<BackdropPortalProvider>
|
||||
{/* Background image element - z-1 keeps it below backdrop blur (z-5).
|
||||
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 z-1 pointer-events-none'>
|
||||
{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-contain'
|
||||
onLoad={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
onError={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
key={backgroundImageUrl}
|
||||
src={backgroundImageUrl}
|
||||
alt=''
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-contain'
|
||||
priority
|
||||
unoptimized
|
||||
onLoad={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
onError={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
{backgroundImageUrl && !backgroundVideoUrl && (
|
||||
<div className='absolute inset-0 z-1 pointer-events-none'>
|
||||
{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-contain'
|
||||
onLoad={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
onError={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
key={backgroundImageUrl}
|
||||
src={backgroundImageUrl}
|
||||
alt=''
|
||||
fill
|
||||
sizes='100vw'
|
||||
className='object-contain'
|
||||
priority
|
||||
unoptimized
|
||||
onLoad={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
onError={() => {
|
||||
setIsBackgroundReady(true);
|
||||
pageSwitch.markBackgroundReady();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Previous background overlay - shows during direct navigation until new bg is ready */}
|
||||
{pageSwitch.previousBgImageUrl &&
|
||||
pageSwitch.isSwitching &&
|
||||
!pageSwitch.isNewBgReady && (
|
||||
<div
|
||||
className='absolute inset-0 pointer-events-none z-10'
|
||||
style={{
|
||||
backgroundImage: `url("${pageSwitch.previousBgImageUrl}")`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Previous background overlay - shows during direct navigation until new bg is ready */}
|
||||
{pageSwitch.previousBgImageUrl &&
|
||||
pageSwitch.isSwitching &&
|
||||
!pageSwitch.isNewBgReady && (
|
||||
<div
|
||||
className='absolute inset-0 pointer-events-none z-10'
|
||||
style={{
|
||||
backgroundImage: `url("${pageSwitch.previousBgImageUrl}")`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
{/* Background video - z-1 keeps it below backdrop blur (z-5) */}
|
||||
{backgroundVideoUrl && (
|
||||
<video
|
||||
ref={bgVideoRef}
|
||||
key={backgroundVideoUrl}
|
||||
className='absolute inset-0 z-1 h-full w-full object-contain'
|
||||
src={backgroundVideoUrl}
|
||||
autoPlay={videoAutoplay}
|
||||
loop={useNativeLoop}
|
||||
muted={videoMuted}
|
||||
playsInline
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Background video - z-1 keeps it below backdrop blur (z-5) */}
|
||||
{backgroundVideoUrl && (
|
||||
<video
|
||||
ref={bgVideoRef}
|
||||
key={backgroundVideoUrl}
|
||||
className='absolute inset-0 z-1 h-full w-full object-contain'
|
||||
src={backgroundVideoUrl}
|
||||
autoPlay={videoAutoplay}
|
||||
loop={useNativeLoop}
|
||||
muted={videoMuted}
|
||||
playsInline
|
||||
/>
|
||||
)}
|
||||
{/* Page elements - z-40 ensures they appear above carousel background (z-10) and carousel controls (z-30) */}
|
||||
<div
|
||||
className='absolute inset-0 z-40'
|
||||
style={{
|
||||
opacity: elementsOpacity,
|
||||
transition: isFadingIn
|
||||
? `opacity ${CANVAS_CONFIG.pageTransition.fadeInDurationMs}ms ${CANVAS_CONFIG.pageTransition.easing}`
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
{pageElements.map((element: CanvasElement) => (
|
||||
<RuntimeElement
|
||||
key={element.id}
|
||||
element={element}
|
||||
onClick={() => handleElementClick(element)}
|
||||
resolveUrl={resolveUrlWithBlob}
|
||||
onGalleryCardClick={(cardIndex) =>
|
||||
handleGalleryCardClick(element, cardIndex)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Page elements - z-40 ensures they appear above carousel background (z-10) and carousel controls (z-30) */}
|
||||
<div
|
||||
className='absolute inset-0 z-40'
|
||||
style={{
|
||||
opacity: elementsOpacity,
|
||||
transition: isFadingIn
|
||||
? `opacity ${CANVAS_CONFIG.pageTransition.fadeInDurationMs}ms ${CANVAS_CONFIG.pageTransition.easing}`
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
{pageElements.map((element: CanvasElement) => (
|
||||
<RuntimeElement
|
||||
key={element.id}
|
||||
element={element}
|
||||
onClick={() => handleElementClick(element)}
|
||||
resolveUrl={resolveUrlWithBlob}
|
||||
onGalleryCardClick={(cardIndex) =>
|
||||
handleGalleryCardClick(element, cardIndex)
|
||||
}
|
||||
{/* Controls: Offline toggle and Fullscreen button */}
|
||||
<div className='absolute top-4 right-4 z-50 flex items-center gap-2'>
|
||||
<OfflineToggle
|
||||
projectId={project?.id || null}
|
||||
projectSlug={projectSlug}
|
||||
projectName={project?.name}
|
||||
pages={pages}
|
||||
showLabel={false}
|
||||
size='small'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Controls: Offline toggle and Fullscreen button */}
|
||||
<div className='absolute top-4 right-4 z-50 flex items-center gap-2'>
|
||||
<OfflineToggle
|
||||
projectId={project?.id || null}
|
||||
projectSlug={projectSlug}
|
||||
projectName={project?.name}
|
||||
pages={pages}
|
||||
showLabel={false}
|
||||
size='small'
|
||||
/>
|
||||
<BaseButton
|
||||
icon={isFullscreen ? mdiFullscreenExit : mdiFullscreen}
|
||||
color='info'
|
||||
small
|
||||
onClick={toggleFullscreen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
|
||||
{/* Opacity is 0 during 'preparing' phase to show old page while video loads */}
|
||||
{/* Also fades to 0 when isOverlayFadingOut to reveal the new page underneath */}
|
||||
{transitionPreview && (
|
||||
<div className='fixed inset-0 z-50 overflow-hidden pointer-events-none'>
|
||||
<video
|
||||
ref={transitionVideoRef}
|
||||
className='absolute inset-0 h-full w-full object-cover transition-opacity duration-300 ease-linear'
|
||||
style={{
|
||||
opacity:
|
||||
transitionPhase === 'preparing' ||
|
||||
isBuffering ||
|
||||
isOverlayFadingOut
|
||||
? 0
|
||||
: 1,
|
||||
}}
|
||||
muted
|
||||
playsInline
|
||||
preload='auto'
|
||||
disablePictureInPicture
|
||||
<BaseButton
|
||||
icon={isFullscreen ? mdiFullscreenExit : mdiFullscreen}
|
||||
color='info'
|
||||
small
|
||||
onClick={toggleFullscreen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gallery Carousel Overlay */}
|
||||
{activeGalleryCarousel && (
|
||||
<GalleryCarouselOverlay
|
||||
cards={activeGalleryCarousel.element.galleryCards || []}
|
||||
initialIndex={activeGalleryCarousel.initialIndex}
|
||||
onClose={() => setActiveGalleryCarousel(null)}
|
||||
resolveUrl={resolveUrlWithBlob}
|
||||
prevIconUrl={
|
||||
activeGalleryCarousel.element.galleryCarouselPrevIconUrl
|
||||
}
|
||||
nextIconUrl={
|
||||
activeGalleryCarousel.element.galleryCarouselNextIconUrl
|
||||
}
|
||||
backIconUrl={
|
||||
activeGalleryCarousel.element.galleryCarouselBackIconUrl
|
||||
}
|
||||
backLabel={
|
||||
activeGalleryCarousel.element.galleryCarouselBackLabel || 'BACK'
|
||||
}
|
||||
prevX={activeGalleryCarousel.element.galleryCarouselPrevX}
|
||||
prevY={activeGalleryCarousel.element.galleryCarouselPrevY}
|
||||
nextX={activeGalleryCarousel.element.galleryCarouselNextX}
|
||||
nextY={activeGalleryCarousel.element.galleryCarouselNextY}
|
||||
backX={activeGalleryCarousel.element.galleryCarouselBackX}
|
||||
backY={activeGalleryCarousel.element.galleryCarouselBackY}
|
||||
prevWidth={activeGalleryCarousel.element.galleryCarouselPrevWidth}
|
||||
prevHeight={
|
||||
activeGalleryCarousel.element.galleryCarouselPrevHeight
|
||||
}
|
||||
nextWidth={activeGalleryCarousel.element.galleryCarouselNextWidth}
|
||||
nextHeight={
|
||||
activeGalleryCarousel.element.galleryCarouselNextHeight
|
||||
}
|
||||
backWidth={activeGalleryCarousel.element.galleryCarouselBackWidth}
|
||||
backHeight={
|
||||
activeGalleryCarousel.element.galleryCarouselBackHeight
|
||||
}
|
||||
isEditMode={false}
|
||||
/>
|
||||
)}
|
||||
</BackdropPortalProvider>
|
||||
{/* Transition overlay - uses useTransitionPlayback hook for smooth transitions */}
|
||||
{/* Opacity is 0 during 'preparing' phase to show old page while video loads */}
|
||||
{/* Also fades to 0 when isOverlayFadingOut to reveal the new page underneath */}
|
||||
{transitionPreview && (
|
||||
<div className='fixed inset-0 z-50 overflow-hidden pointer-events-none'>
|
||||
<video
|
||||
ref={transitionVideoRef}
|
||||
className='absolute inset-0 h-full w-full object-cover transition-opacity duration-300 ease-linear'
|
||||
style={{
|
||||
opacity:
|
||||
transitionPhase === 'preparing' ||
|
||||
isBuffering ||
|
||||
isOverlayFadingOut
|
||||
? 0
|
||||
: 1,
|
||||
}}
|
||||
muted
|
||||
playsInline
|
||||
preload='auto'
|
||||
disablePictureInPicture
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gallery Carousel Overlay */}
|
||||
{activeGalleryCarousel && (
|
||||
<GalleryCarouselOverlay
|
||||
cards={activeGalleryCarousel.element.galleryCards || []}
|
||||
initialIndex={activeGalleryCarousel.initialIndex}
|
||||
onClose={() => setActiveGalleryCarousel(null)}
|
||||
resolveUrl={resolveUrlWithBlob}
|
||||
prevIconUrl={
|
||||
activeGalleryCarousel.element.galleryCarouselPrevIconUrl
|
||||
}
|
||||
nextIconUrl={
|
||||
activeGalleryCarousel.element.galleryCarouselNextIconUrl
|
||||
}
|
||||
backIconUrl={
|
||||
activeGalleryCarousel.element.galleryCarouselBackIconUrl
|
||||
}
|
||||
backLabel={
|
||||
activeGalleryCarousel.element.galleryCarouselBackLabel ||
|
||||
'BACK'
|
||||
}
|
||||
prevX={activeGalleryCarousel.element.galleryCarouselPrevX}
|
||||
prevY={activeGalleryCarousel.element.galleryCarouselPrevY}
|
||||
nextX={activeGalleryCarousel.element.galleryCarouselNextX}
|
||||
nextY={activeGalleryCarousel.element.galleryCarouselNextY}
|
||||
backX={activeGalleryCarousel.element.galleryCarouselBackX}
|
||||
backY={activeGalleryCarousel.element.galleryCarouselBackY}
|
||||
prevWidth={
|
||||
activeGalleryCarousel.element.galleryCarouselPrevWidth
|
||||
}
|
||||
prevHeight={
|
||||
activeGalleryCarousel.element.galleryCarouselPrevHeight
|
||||
}
|
||||
nextWidth={
|
||||
activeGalleryCarousel.element.galleryCarouselNextWidth
|
||||
}
|
||||
nextHeight={
|
||||
activeGalleryCarousel.element.galleryCarouselNextHeight
|
||||
}
|
||||
backWidth={
|
||||
activeGalleryCarousel.element.galleryCarouselBackWidth
|
||||
}
|
||||
backHeight={
|
||||
activeGalleryCarousel.element.galleryCarouselBackHeight
|
||||
}
|
||||
isEditMode={false}
|
||||
/>
|
||||
)}
|
||||
</BackdropPortalProvider>
|
||||
</div>
|
||||
{/* End inner canvas container */}
|
||||
|
||||
|
||||
@ -248,7 +248,8 @@ const CarouselElement: React.FC<CarouselElementProps> = ({
|
||||
if (!value || value.trim() === '') return undefined;
|
||||
const trimmed = value.trim();
|
||||
// If value already uses canvas units or calc, preserve it
|
||||
if (trimmed.includes('var(--cu') || trimmed.includes('--cu')) return trimmed;
|
||||
if (trimmed.includes('var(--cu') || trimmed.includes('--cu'))
|
||||
return trimmed;
|
||||
if (/^calc\(/i.test(trimmed)) return trimmed;
|
||||
// If value already has other units, convert them
|
||||
const vwMatch = trimmed.match(/^(-?\d*\.?\d+)vw$/i);
|
||||
|
||||
@ -1,217 +0,0 @@
|
||||
/**
|
||||
* useBackgroundDimensionSuggestion Hook
|
||||
*
|
||||
* Detects background media dimensions and suggests canvas size updates.
|
||||
* Used in constructor to prompt user when background resolution differs from project settings.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, useState, useRef } from 'react';
|
||||
import { logger } from '../lib/logger';
|
||||
|
||||
interface MediaDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface UseBackgroundDimensionSuggestionOptions {
|
||||
/** URL of the background media (image or video) */
|
||||
mediaUrl: string | undefined;
|
||||
/** Current project design width */
|
||||
currentDesignWidth: number;
|
||||
/** Current project design height */
|
||||
currentDesignHeight: number;
|
||||
/** Callback when dimensions differ from current settings */
|
||||
onSuggest: (width: number, height: number) => void;
|
||||
/** Whether suggestion is enabled */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect dimensions of an image URL.
|
||||
*/
|
||||
async function getImageDimensions(
|
||||
url: string,
|
||||
): Promise<MediaDimensions | null> {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
resolve({
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight,
|
||||
});
|
||||
};
|
||||
img.onerror = () => {
|
||||
resolve(null);
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect dimensions of a video URL.
|
||||
*/
|
||||
async function getVideoDimensions(
|
||||
url: string,
|
||||
): Promise<MediaDimensions | null> {
|
||||
return new Promise((resolve) => {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
resolve({
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight,
|
||||
});
|
||||
// Clean up
|
||||
video.src = '';
|
||||
video.load();
|
||||
};
|
||||
|
||||
video.onerror = () => {
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
video.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if URL is likely a video based on extension.
|
||||
*/
|
||||
function isVideoUrl(url: string): boolean {
|
||||
const videoExtensions = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'];
|
||||
const lowercaseUrl = url.toLowerCase();
|
||||
return videoExtensions.some((ext) => lowercaseUrl.includes(ext));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if URL is a resolved/loadable URL (not a raw storage path).
|
||||
* Only process: http(s) URLs, blob URLs, data URLs
|
||||
*/
|
||||
function isLoadableUrl(url: string): boolean {
|
||||
return (
|
||||
url.startsWith('http://') ||
|
||||
url.startsWith('https://') ||
|
||||
url.startsWith('blob:') ||
|
||||
url.startsWith('data:')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get media dimensions from URL (image or video).
|
||||
*/
|
||||
async function getMediaDimensions(
|
||||
url: string,
|
||||
): Promise<MediaDimensions | null> {
|
||||
if (isVideoUrl(url)) {
|
||||
return getVideoDimensions(url);
|
||||
}
|
||||
return getImageDimensions(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to detect background media dimensions and suggest canvas updates.
|
||||
*
|
||||
* @example
|
||||
* const { suggestion, dismissSuggestion } = useBackgroundDimensionSuggestion({
|
||||
* mediaUrl: backgroundImageUrl,
|
||||
* currentDesignWidth: project.design_width ?? 1920,
|
||||
* currentDesignHeight: project.design_height ?? 1080,
|
||||
* onSuggest: (width, height) => {
|
||||
* // Show suggestion UI
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useBackgroundDimensionSuggestion({
|
||||
mediaUrl,
|
||||
currentDesignWidth,
|
||||
currentDesignHeight,
|
||||
onSuggest,
|
||||
enabled = true,
|
||||
}: UseBackgroundDimensionSuggestionOptions) {
|
||||
const [suggestion, setSuggestion] = useState<MediaDimensions | null>(null);
|
||||
const lastCheckedUrlRef = useRef<string | null>(null);
|
||||
const dismissedUrlsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const dismissSuggestion = useCallback(() => {
|
||||
if (mediaUrl) {
|
||||
dismissedUrlsRef.current.add(mediaUrl);
|
||||
}
|
||||
setSuggestion(null);
|
||||
}, [mediaUrl]);
|
||||
|
||||
const acceptSuggestion = useCallback(() => {
|
||||
if (suggestion) {
|
||||
onSuggest(suggestion.width, suggestion.height);
|
||||
setSuggestion(null);
|
||||
}
|
||||
}, [suggestion, onSuggest]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !mediaUrl) {
|
||||
setSuggestion(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip raw storage paths - only process resolved URLs (http, https, blob, data)
|
||||
if (!isLoadableUrl(mediaUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if already checked this URL
|
||||
if (lastCheckedUrlRef.current === mediaUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if user dismissed this URL
|
||||
if (dismissedUrlsRef.current.has(mediaUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastCheckedUrlRef.current = mediaUrl;
|
||||
|
||||
const detectDimensions = async () => {
|
||||
try {
|
||||
const dimensions = await getMediaDimensions(mediaUrl);
|
||||
|
||||
if (!dimensions) {
|
||||
logger.warn('[CanvasSuggestion] Failed to detect media dimensions', {
|
||||
url: mediaUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if dimensions differ significantly (allow for small rounding)
|
||||
const widthDiff = Math.abs(dimensions.width - currentDesignWidth);
|
||||
const heightDiff = Math.abs(dimensions.height - currentDesignHeight);
|
||||
const threshold = 10; // pixels
|
||||
|
||||
if (widthDiff > threshold || heightDiff > threshold) {
|
||||
logger.info('[CanvasSuggestion] Detected different dimensions', {
|
||||
media: dimensions,
|
||||
current: { width: currentDesignWidth, height: currentDesignHeight },
|
||||
});
|
||||
setSuggestion(dimensions);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'[CanvasSuggestion] Error detecting dimensions',
|
||||
error instanceof Error ? error : { error },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
void detectDimensions();
|
||||
}, [mediaUrl, currentDesignWidth, currentDesignHeight, enabled]);
|
||||
|
||||
return {
|
||||
/** Suggested dimensions from detected media */
|
||||
suggestion,
|
||||
/** Dismiss the current suggestion */
|
||||
dismissSuggestion,
|
||||
/** Accept the suggestion and call onSuggest callback */
|
||||
acceptSuggestion,
|
||||
/** Whether a suggestion is currently available */
|
||||
hasSuggestion: suggestion !== null,
|
||||
};
|
||||
}
|
||||
@ -53,7 +53,9 @@ interface CanvasScaleResult {
|
||||
*
|
||||
* return <div style={cssVars}>{content}</div>;
|
||||
*/
|
||||
export function useCanvasScale(options?: UseCanvasScaleOptions): CanvasScaleResult {
|
||||
export function useCanvasScale(
|
||||
options?: UseCanvasScaleOptions,
|
||||
): CanvasScaleResult {
|
||||
const designWidth = options?.designWidth ?? CANVAS_CONFIG.defaults.width;
|
||||
const designHeight = options?.designHeight ?? CANVAS_CONFIG.defaults.height;
|
||||
|
||||
|
||||
@ -29,7 +29,11 @@ const EMPTY_ASSETS: Asset[] = [];
|
||||
|
||||
interface UseConstructorDataResult {
|
||||
// Project
|
||||
project: { name: string; design_width?: number; design_height?: number } | null;
|
||||
project: {
|
||||
name: string;
|
||||
design_width?: number;
|
||||
design_height?: number;
|
||||
} | null;
|
||||
projectName: string;
|
||||
|
||||
// Pages
|
||||
|
||||
@ -82,7 +82,13 @@ const formatBytes = (bytes: number): string => {
|
||||
export function useOfflineMode(
|
||||
options: UseOfflineModeOptions,
|
||||
): UseOfflineModeResult {
|
||||
const { projectId, projectSlug, projectName, pages, enabled = true } = options;
|
||||
const {
|
||||
projectId,
|
||||
projectSlug,
|
||||
projectName,
|
||||
pages,
|
||||
enabled = true,
|
||||
} = options;
|
||||
|
||||
const [projectInfo, setProjectInfo] = useState<OfflineProject | null>(null);
|
||||
const [discoveredAssets, setDiscoveredAssets] = useState<AssetToCache[]>([]);
|
||||
@ -141,7 +147,9 @@ export function useOfflineMode(
|
||||
|
||||
// Find the asset to get its size
|
||||
const asset = assetsRef.current.find(
|
||||
(a) => a.storageKey === data.storageKey || `offline-${a.storageKey}` === data.assetId,
|
||||
(a) =>
|
||||
a.storageKey === data.storageKey ||
|
||||
`offline-${a.storageKey}` === data.assetId,
|
||||
);
|
||||
const assetSize = asset?.sizeBytes || 0;
|
||||
|
||||
@ -152,7 +160,10 @@ export function useOfflineMode(
|
||||
const downloaded = downloadedCountRef.current;
|
||||
const dlBytes = downloadedBytesRef.current;
|
||||
const total = assetsRef.current.length;
|
||||
const totalSize = assetsRef.current.reduce((sum, a) => sum + (a.sizeBytes || 0), 0);
|
||||
const totalSize = assetsRef.current.reduce(
|
||||
(sum, a) => sum + (a.sizeBytes || 0),
|
||||
0,
|
||||
);
|
||||
|
||||
// Update state
|
||||
setDownloadedAssets(downloaded);
|
||||
@ -266,7 +277,9 @@ export function useOfflineMode(
|
||||
// Discover assets from pages (frontend-only, no backend call)
|
||||
const assets = discoverAssets();
|
||||
if (assets.length === 0) {
|
||||
throw new Error('No assets discovered. Make sure pages data is provided.');
|
||||
throw new Error(
|
||||
'No assets discovered. Make sure pages data is provided.',
|
||||
);
|
||||
}
|
||||
|
||||
// Store assets for event-driven progress tracking
|
||||
@ -275,7 +288,10 @@ export function useOfflineMode(
|
||||
setTotalAssets(assets.length);
|
||||
|
||||
// Estimate total size (may not have exact sizes for all assets)
|
||||
const estimatedTotalSize = assets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0);
|
||||
const estimatedTotalSize = assets.reduce(
|
||||
(sum, a) => sum + (a.sizeBytes || 0),
|
||||
0,
|
||||
);
|
||||
setTotalBytes(estimatedTotalSize);
|
||||
|
||||
// Create or update project record
|
||||
@ -307,7 +323,8 @@ export function useOfflineMode(
|
||||
// Partial downloads from online preload need to be re-downloaded fully for offline
|
||||
const assetsToDownload: AssetToCache[] = [];
|
||||
for (const asset of assets) {
|
||||
const assetInfo: CachedAssetInfo | null = await StorageManager.getAssetInfo(asset.storageKey);
|
||||
const assetInfo: CachedAssetInfo | null =
|
||||
await StorageManager.getAssetInfo(asset.storageKey);
|
||||
|
||||
if (assetInfo?.exists && !assetInfo.isPartial) {
|
||||
// Fully cached, skip
|
||||
@ -318,10 +335,13 @@ export function useOfflineMode(
|
||||
});
|
||||
} else if (assetInfo?.exists && assetInfo.isPartial) {
|
||||
// Partial download - need full download for offline
|
||||
logger.info('[useOfflineMode] Upgrading partial download for offline', {
|
||||
storageKey: asset.storageKey.slice(-50),
|
||||
partialSize: assetInfo.sizeBytes,
|
||||
});
|
||||
logger.info(
|
||||
'[useOfflineMode] Upgrading partial download for offline',
|
||||
{
|
||||
storageKey: asset.storageKey.slice(-50),
|
||||
partialSize: assetInfo.sizeBytes,
|
||||
},
|
||||
);
|
||||
assetsToDownload.push(asset);
|
||||
} else {
|
||||
// Not cached at all
|
||||
@ -367,9 +387,12 @@ export function useOfflineMode(
|
||||
count: Object.keys(presignedUrls).length,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('[useOfflineMode] Failed to fetch presigned URLs, using proxy', {
|
||||
error: err instanceof Error ? err.message : 'unknown',
|
||||
});
|
||||
logger.warn(
|
||||
'[useOfflineMode] Failed to fetch presigned URLs, using proxy',
|
||||
{
|
||||
error: err instanceof Error ? err.message : 'unknown',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,7 +401,9 @@ export function useOfflineMode(
|
||||
// Progress is tracked via event subscriptions (see useEffect above)
|
||||
for (const asset of assetsToDownload) {
|
||||
// Resolve download URL - prefer presigned, fallback to proxy
|
||||
const downloadUrl = presignedUrls[asset.storageKey] || resolveAssetPlaybackUrl(asset.storageKey);
|
||||
const downloadUrl =
|
||||
presignedUrls[asset.storageKey] ||
|
||||
resolveAssetPlaybackUrl(asset.storageKey);
|
||||
|
||||
downloadManager
|
||||
.addJob({
|
||||
@ -491,7 +516,9 @@ export function useOfflineMode(
|
||||
// Computed values
|
||||
const isDownloaded = status === 'downloaded';
|
||||
const isDownloading = status === 'downloading' && !isPaused;
|
||||
const estimatedSize = discoveredAssets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0) || totalBytes;
|
||||
const estimatedSize =
|
||||
discoveredAssets.reduce((sum, a) => sum + (a.sizeBytes || 0), 0) ||
|
||||
totalBytes;
|
||||
|
||||
return {
|
||||
isOfflineCapable,
|
||||
|
||||
@ -33,7 +33,11 @@ import type {
|
||||
PreloadPageLink,
|
||||
PreloadElement,
|
||||
} from '../../types/preload';
|
||||
import type { AssetType, AssetVariantType, CachedAssetInfo } from '../../types/offline';
|
||||
import type {
|
||||
AssetType,
|
||||
AssetVariantType,
|
||||
CachedAssetInfo,
|
||||
} from '../../types/offline';
|
||||
|
||||
// Re-export for convenience
|
||||
export type { CachedAssetInfo };
|
||||
@ -102,7 +106,9 @@ export class AssetCacheService {
|
||||
* Get cache status for a single asset
|
||||
* Returns whether it exists, if it's partial, and size info
|
||||
*/
|
||||
static async getAssetInfo(storageKey: string): Promise<CachedAssetInfo | null> {
|
||||
static async getAssetInfo(
|
||||
storageKey: string,
|
||||
): Promise<CachedAssetInfo | null> {
|
||||
return StorageManager.getAssetInfo(storageKey);
|
||||
}
|
||||
|
||||
@ -225,7 +231,8 @@ export class AssetCacheService {
|
||||
|
||||
// Determine download parameters based on mode
|
||||
const isOnlineMode = mode === 'online';
|
||||
const isCurrentPageAsset = currentPageId && asset.pageId === currentPageId;
|
||||
const isCurrentPageAsset =
|
||||
currentPageId && asset.pageId === currentPageId;
|
||||
|
||||
// Online mode: use partial downloads for neighbor page media
|
||||
// Offline mode: always full downloads
|
||||
|
||||
@ -47,10 +47,7 @@ const DEFAULT_OPTIONS: AssetDiscoveryOptions = {
|
||||
/**
|
||||
* Classify asset type based on field name
|
||||
*/
|
||||
export function classifyAssetType(
|
||||
fieldName: string,
|
||||
url: string,
|
||||
): AssetType {
|
||||
export function classifyAssetType(fieldName: string, url: string): AssetType {
|
||||
const lowerField = fieldName.toLowerCase();
|
||||
const lowerUrl = url.toLowerCase();
|
||||
|
||||
@ -331,7 +328,11 @@ export function getPrioritizedAssets(
|
||||
|
||||
assets.push({
|
||||
...asset,
|
||||
priority: calculateAssetPriority(asset.assetType, isCurrentPage, distance),
|
||||
priority: calculateAssetPriority(
|
||||
asset.assetType,
|
||||
isCurrentPage,
|
||||
distance,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@ -348,8 +349,8 @@ export function getPrioritizedAssets(
|
||||
const currentPageElements = elements.filter(
|
||||
(el) => el.pageId === currentPageId,
|
||||
);
|
||||
extractElementAssets(currentPageElements, currentPageId).forEach((asset) =>
|
||||
addAssetWithPriority(asset, true, 0),
|
||||
extractElementAssets(currentPageElements, currentPageId).forEach(
|
||||
(asset) => addAssetWithPriority(asset, true, 0),
|
||||
);
|
||||
}
|
||||
|
||||
@ -402,7 +403,12 @@ export function toPreloadAssetInfo(asset: AssetToCache): PreloadAssetInfo {
|
||||
return {
|
||||
url: asset.originalUrl,
|
||||
pageId: asset.pageId,
|
||||
assetType: assetType as 'image' | 'video' | 'audio' | 'transition' | 'other',
|
||||
assetType: assetType as
|
||||
| 'image'
|
||||
| 'video'
|
||||
| 'audio'
|
||||
| 'transition'
|
||||
| 'other',
|
||||
priority: asset.priority,
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,7 +4,11 @@
|
||||
* Unified asset caching for online preload and offline download.
|
||||
*/
|
||||
|
||||
export { AssetCacheService, type DownloadProgress, type QueueDownloadOptions } from './AssetCacheService';
|
||||
export {
|
||||
AssetCacheService,
|
||||
type DownloadProgress,
|
||||
type QueueDownloadOptions,
|
||||
} from './AssetCacheService';
|
||||
export type { CachedAssetInfo } from '../../types/offline';
|
||||
|
||||
export {
|
||||
|
||||
@ -130,7 +130,13 @@ export function remToDesignPx(value: number): number {
|
||||
*/
|
||||
export function normalizeToCanvasUnits(
|
||||
value: string | number | undefined,
|
||||
property: 'width' | 'height' | 'fontSize' | 'padding' | 'borderRadius' | 'gap',
|
||||
property:
|
||||
| 'width'
|
||||
| 'height'
|
||||
| 'fontSize'
|
||||
| 'padding'
|
||||
| 'borderRadius'
|
||||
| 'gap',
|
||||
designWidth: number = CANVAS_CONFIG.defaults.width,
|
||||
designHeight: number = CANVAS_CONFIG.defaults.height,
|
||||
): string {
|
||||
|
||||
@ -417,7 +417,8 @@ export function buildGallerySpanGridStyle(
|
||||
element: Partial<CanvasElement>,
|
||||
): CSSProperties {
|
||||
const columns = getGalleryGridColumns(element, 'span');
|
||||
const gap = normalizeCanvasUnit(element.gallerySpanGap) || 'calc(8 * var(--cu, 1px))';
|
||||
const gap =
|
||||
normalizeCanvasUnit(element.gallerySpanGap) || 'calc(8 * var(--cu, 1px))';
|
||||
|
||||
return {
|
||||
display: 'grid',
|
||||
@ -518,7 +519,8 @@ export function buildGalleryCardGridStyle(
|
||||
element: Partial<CanvasElement>,
|
||||
): CSSProperties {
|
||||
const columns = getGalleryGridColumns(element, 'card');
|
||||
const gap = normalizeCanvasUnit(element.galleryCardGap) || 'calc(8 * var(--cu, 1px))';
|
||||
const gap =
|
||||
normalizeCanvasUnit(element.galleryCardGap) || 'calc(8 * var(--cu, 1px))';
|
||||
|
||||
return {
|
||||
display: 'grid',
|
||||
|
||||
@ -92,9 +92,12 @@ class DownloadManagerClass {
|
||||
|
||||
// For partial downloads, check session cache first (fast path)
|
||||
if (isPartialDownload && this.partialDownloadsReady.has(storageKey)) {
|
||||
logger.info('[DownloadManager] Partial download already ready (session)', {
|
||||
storageKey: storageKey.slice(-50),
|
||||
});
|
||||
logger.info(
|
||||
'[DownloadManager] Partial download already ready (session)',
|
||||
{
|
||||
storageKey: storageKey.slice(-50),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -416,10 +419,13 @@ class DownloadManagerClass {
|
||||
markPresignedUrlFailed(job.storageKey);
|
||||
const proxyUrl = buildProxyUrl(job.storageKey);
|
||||
|
||||
logger.info('[DownloadManager] Presigned URL failed, retrying with proxy', {
|
||||
storageKey: job.storageKey.slice(-50),
|
||||
error: errorMessage,
|
||||
});
|
||||
logger.info(
|
||||
'[DownloadManager] Presigned URL failed, retrying with proxy',
|
||||
{
|
||||
storageKey: job.storageKey.slice(-50),
|
||||
error: errorMessage,
|
||||
},
|
||||
);
|
||||
|
||||
// Update job to use proxy URL and retry immediately
|
||||
job.url = proxyUrl;
|
||||
@ -718,7 +724,10 @@ class DownloadManagerClass {
|
||||
* This enables the SW to cache the full response when the browser fetches the media
|
||||
* during playback, using the canonical storage key instead of the expiring presigned URL.
|
||||
*/
|
||||
private registerUrlForCaching(presignedUrl: string, storageKey: string): void {
|
||||
private registerUrlForCaching(
|
||||
presignedUrl: string,
|
||||
storageKey: string,
|
||||
): void {
|
||||
if (navigator.serviceWorker?.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'REGISTER_CACHE_URL',
|
||||
|
||||
@ -86,9 +86,7 @@ import {
|
||||
type NavigationElementType,
|
||||
} from '../context/ConstructorContext';
|
||||
import { useCanvasScale } from '../hooks/useCanvasScale';
|
||||
import { useBackgroundDimensionSuggestion } from '../hooks/useBackgroundDimensionSuggestion';
|
||||
import { CANVAS_CONFIG } from '../config/canvas.config';
|
||||
import { CanvasDimensionSuggestion } from '../components/CanvasDimensionSuggestion';
|
||||
|
||||
// Constructor helpers (extracted utilities)
|
||||
import {
|
||||
@ -195,35 +193,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
backgroundVideoEndTime,
|
||||
} = usePageBackground();
|
||||
|
||||
// Background dimension auto-detection for canvas size suggestions
|
||||
const handleDimensionSuggestionAccept = useCallback(
|
||||
async (width: number, height: number) => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
await axios.put(`/projects/${projectId}`, {
|
||||
data: { design_width: width, design_height: height },
|
||||
});
|
||||
// Refetch to update the project data
|
||||
await refetchData();
|
||||
} catch (error) {
|
||||
logger.error('Failed to update project dimensions', error instanceof Error ? error : { error });
|
||||
}
|
||||
},
|
||||
[projectId, refetchData],
|
||||
);
|
||||
|
||||
const {
|
||||
suggestion: dimensionSuggestion,
|
||||
dismissSuggestion: dismissDimensionSuggestion,
|
||||
acceptSuggestion: acceptDimensionSuggestion,
|
||||
} = useBackgroundDimensionSuggestion({
|
||||
mediaUrl: backgroundImageUrl || backgroundVideoUrl,
|
||||
currentDesignWidth: project?.design_width ?? 1920,
|
||||
currentDesignHeight: project?.design_height ?? 1080,
|
||||
onSuggest: handleDimensionSuggestionAccept,
|
||||
enabled: !isElementEditMode && !!project,
|
||||
});
|
||||
|
||||
const [selectedMenuItem, setSelectedMenuItem] =
|
||||
useState<EditorMenuItem>('none');
|
||||
// Transition preview state managed by useTransitionPreview hook (below)
|
||||
@ -389,6 +358,16 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
// Destructure stable callback reference to avoid infinite loops in useEffect deps
|
||||
const pageSwitchToPage = pageSwitch.switchToPage;
|
||||
|
||||
// Use shared background transition hook for direct navigation clearing and fade-in
|
||||
// (No fade-out needed in constructor - transitions complete immediately)
|
||||
// NOTE: Must be defined before switchToPage callback which uses resetFadeIn
|
||||
const { isFadingIn, elementsOpacity, resetFadeIn } = useBackgroundTransition({
|
||||
pageSwitch,
|
||||
fadeIn: {
|
||||
hasActiveTransition: Boolean(transitionPreview),
|
||||
},
|
||||
});
|
||||
|
||||
// Helper to switch pages without flash
|
||||
// Uses usePageSwitch hook to resolve blob URLs from preload cache
|
||||
// Also updates storage path state for editing/saving purposes
|
||||
@ -424,7 +403,12 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
},
|
||||
);
|
||||
},
|
||||
[pageSwitchToPage, updateBackgroundFromPage, applyPageSelection, resetFadeIn],
|
||||
[
|
||||
pageSwitchToPage,
|
||||
updateBackgroundFromPage,
|
||||
applyPageSelection,
|
||||
resetFadeIn,
|
||||
],
|
||||
);
|
||||
|
||||
const { isBuffering: isReverseBuffering } = useTransitionPlayback({
|
||||
@ -481,15 +465,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Use shared background transition hook for direct navigation clearing and fade-in
|
||||
// (No fade-out needed in constructor - transitions complete immediately)
|
||||
const { isFadingIn, elementsOpacity, resetFadeIn } = useBackgroundTransition({
|
||||
pageSwitch,
|
||||
fadeIn: {
|
||||
hasActiveTransition: Boolean(transitionPreview),
|
||||
},
|
||||
});
|
||||
|
||||
const iconPreloadTargets = useMemo(() => {
|
||||
const preloadableTypes: CanvasElementType[] = [
|
||||
'navigation_next',
|
||||
@ -1479,7 +1454,11 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
ref={canvasRef}
|
||||
tabIndex={-1}
|
||||
className={`z-20 overflow-clip ${hasFullWidthCarousel ? 'bg-transparent' : 'bg-black'}`}
|
||||
style={{ ...canvasCssVars, ...letterboxStyles, ...canvasBackgroundStyle }}
|
||||
style={{
|
||||
...canvasCssVars,
|
||||
...letterboxStyles,
|
||||
...canvasBackgroundStyle,
|
||||
}}
|
||||
>
|
||||
<BackdropPortalProvider>
|
||||
<CanvasBackground
|
||||
@ -1643,18 +1622,6 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Canvas Dimension Suggestion */}
|
||||
{dimensionSuggestion && (
|
||||
<CanvasDimensionSuggestion
|
||||
suggestedWidth={dimensionSuggestion.width}
|
||||
suggestedHeight={dimensionSuggestion.height}
|
||||
currentWidth={project?.design_width ?? 1920}
|
||||
currentHeight={project?.design_height ?? 1080}
|
||||
onAccept={acceptDimensionSuggestion}
|
||||
onDismiss={dismissDimensionSuggestion}
|
||||
/>
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
.menu-action-btn {
|
||||
width: 100%;
|
||||
|
||||
@ -89,7 +89,11 @@ const Dashboard = () => {
|
||||
});
|
||||
|
||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles) as {
|
||||
rolesWidgets: Array<{ id?: string; widget_id?: string; [key: string]: unknown }>;
|
||||
rolesWidgets: Array<{
|
||||
id?: string;
|
||||
widget_id?: string;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
|
||||
@ -122,8 +122,10 @@ const EditProjectsPage = () => {
|
||||
useEffect(() => {
|
||||
if (typeof project === 'object' && project !== null) {
|
||||
const projectData = project as unknown as Record<string, unknown>;
|
||||
const width = Number(projectData.design_width) || CANVAS_CONFIG.defaults.width;
|
||||
const height = Number(projectData.design_height) || CANVAS_CONFIG.defaults.height;
|
||||
const width =
|
||||
Number(projectData.design_width) || CANVAS_CONFIG.defaults.width;
|
||||
const height =
|
||||
Number(projectData.design_height) || CANVAS_CONFIG.defaults.height;
|
||||
|
||||
// Check if dimensions match a preset
|
||||
const matchesPreset = CANVAS_CONFIG.presets.some(
|
||||
@ -409,7 +411,8 @@ const EditProjectsPage = () => {
|
||||
</div>
|
||||
<p className='text-xs text-gray-500 mt-1'>
|
||||
Set to match your background image/video resolution for best
|
||||
quality. UI elements scale proportionally on different screens.
|
||||
quality. UI elements scale proportionally on different
|
||||
screens.
|
||||
</p>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
@ -418,7 +418,8 @@ self.addEventListener('message', (event) => {
|
||||
if (payload?.storageKey) {
|
||||
// Extract storage path from presigned URL (or use storageKey directly)
|
||||
const storagePath = payload.presignedUrl
|
||||
? extractStoragePathFromUrl(payload.presignedUrl) || payload.storageKey
|
||||
? extractStoragePathFromUrl(payload.presignedUrl) ||
|
||||
payload.storageKey
|
||||
: payload.storageKey;
|
||||
storagePathToKeyMap.set(storagePath, payload.storageKey);
|
||||
console.log('[SW] Registered storage path for caching', {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user