Преглед на файлове

fix animation

Sviluppo_Carrello_Immagini
Lorenzo Pollutri преди 1 месец
родител
ревизия
e1d60e5087
променени са 4 файла, в които са добавени 136 реда и са изтрити 170 реда
  1. +136
    -103
      components/PublicGrid.tsx
  2. +0
    -21
      lib/page-flip/LICENSE
  3. +0
    -45
      lib/page-flip/index.d.ts
  4. +0
    -1
      lib/page-flip/index.js

+ 136
- 103
components/PublicGrid.tsx Целия файл

@@ -422,83 +422,31 @@ function FullscreenViewer({
}

function FlipBook({ pages, onClose }: { pages: string[]; onClose: () => void }) {
const containerRef = useRef<HTMLDivElement>(null);
const flipRef = useRef<import('@/lib/page-flip').PageFlip | null>(null);
const [currentPage, setCurrentPage] = useState(0);
const [pageCount, setPageCount] = useState(pages.length);

useEffect(() => {
if (!containerRef.current || pages.length === 0) return;
let cancelled = false;

// page-flip's destroy() removes its own block from the DOM. Give it a child
// div we own so React's managed container stays intact across StrictMode cycles.
const block = document.createElement('div');
block.style.width = '100%';
block.style.height = '100%';
containerRef.current.appendChild(block);

// HTML mode: each page is a div with an <img> using object-fit:contain so
// mixed aspect ratios get letterboxed with white margins (like a real book
// page) instead of being stretched to fill the slot.
const pageElements = pages.map((url) => {
const page = document.createElement('div');
page.style.cssText = 'background:#ffffff;width:100%;height:100%;overflow:hidden;display:flex;align-items:center;justify-content:center;';
const img = document.createElement('img');
img.src = url;
img.draggable = false;
img.style.cssText = 'max-width:100%;max-height:100%;object-fit:contain;display:block;pointer-events:none;user-select:none;';
page.appendChild(img);
return page;
});

import('@/lib/page-flip').then(({ PageFlip }) => {
if (cancelled) return;
const flip = new PageFlip(block, {
width: 550,
height: 733,
size: 'stretch',
minWidth: 315,
maxWidth: 1400,
minHeight: 420,
maxHeight: 900,
drawShadow: true,
flippingTime: 800,
usePortrait: true,
maxShadowOpacity: 0.5,
showCover: false,
mobileScrollSupport: false,
useMouseEvents: true,
swipeDistance: 30,
showPageCorners: true,
disableFlipByClick: false,
});

flip.on('flip', (e) => {
const newPage = typeof e.data === 'number' ? e.data : 0;
setCurrentPage(newPage);
});
flip.on('init', () => {
setPageCount(flip.getPageCount());
});

flip.loadFromHTML(pageElements);
flipRef.current = flip;
}).catch(() => {});

return () => {
cancelled = true;
if (flipRef.current) {
try { flipRef.current.destroy(); } catch {}
flipRef.current = null;
}
try { block.remove(); } catch {}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const goNext = useCallback(() => flipRef.current?.flipNext(), []);
const goPrev = useCallback(() => flipRef.current?.flipPrev(), []);
const [spread, setSpread] = useState(0);
const [flipping, setFlipping] = useState<'forward' | 'backward' | null>(null);
const dragStartX = useRef<number | null>(null);

const totalSpreads = Math.max(1, Math.ceil(pages.length / 2));
const getPage = (i: number) => (i >= 0 && i < pages.length ? pages[i] : '');

const leftIdx = spread * 2;
const rightIdx = spread * 2 + 1;
const nextLeftIdx = (spread + 1) * 2;
const nextRightIdx = (spread + 1) * 2 + 1;
const prevLeftIdx = (spread - 1) * 2;
const prevRightIdx = (spread - 1) * 2 + 1;

const goNext = useCallback(() => {
if (flipping !== null || spread >= totalSpreads - 1) return;
setFlipping('forward');
window.setTimeout(() => { setSpread(s => s + 1); setFlipping(null); }, 700);
}, [flipping, spread, totalSpreads]);

const goPrev = useCallback(() => {
if (flipping !== null || spread <= 0) return;
setFlipping('backward');
window.setTimeout(() => { setSpread(s => s - 1); setFlipping(null); }, 700);
}, [flipping, spread]);

useEffect(() => {
const onKey = (e: KeyboardEvent) => {
@@ -510,8 +458,39 @@ function FlipBook({ pages, onClose }: { pages: string[]; onClose: () => void })
return () => window.removeEventListener('keydown', onKey);
}, [onClose, goNext, goPrev]);

const onPointerDown = (e: React.PointerEvent) => { dragStartX.current = e.clientX; };
const onPointerUp = (e: React.PointerEvent) => {
if (dragStartX.current === null) return;
const dx = e.clientX - dragStartX.current;
if (Math.abs(dx) > 50) (dx < 0 ? goNext() : goPrev());
dragStartX.current = null;
};
const onPointerCancel = () => { dragStartX.current = null; };

const visibleLeft = flipping === 'backward' ? getPage(prevLeftIdx) : getPage(leftIdx);
const visibleRight = flipping === 'forward' ? getPage(nextRightIdx) : getPage(rightIdx);

const currentPageNum = Math.min(leftIdx + 1, pages.length);
const lastPageNum = Math.min(rightIdx + 1, pages.length);
const indicatorLabel = currentPageNum === lastPageNum
? `${currentPageNum} / ${pages.length}`
: `${currentPageNum}-${lastPageNum} / ${pages.length}`;

const pageImgClass = 'w-full h-full object-contain';

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/95 select-none">
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/95 select-none"
onPointerDown={onPointerDown}
onPointerUp={onPointerUp}
onPointerCancel={onPointerCancel}
style={{ perspective: '2500px', touchAction: 'pan-y' }}
>
<style dangerouslySetInnerHTML={{ __html: `
@keyframes flipBookForward { from { transform: rotateY(0deg); } to { transform: rotateY(-180deg); } }
@keyframes flipBookBackward { from { transform: rotateY(0deg); } to { transform: rotateY(180deg); } }
`}} />

<button
onClick={onClose}
className="absolute top-4 right-4 bg-white/10 hover:bg-white/25 text-white w-11 h-11 flex items-center justify-center rounded-full text-xl z-30 transition-colors"
@@ -519,49 +498,103 @@ function FlipBook({ pages, onClose }: { pages: string[]; onClose: () => void })
aria-label="Chiudi"
>✕</button>

<style dangerouslySetInnerHTML={{ __html: `
.stf__item[class~="--left"]::after,
.stf__item[class~="--right"]::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
width: 50px;
pointer-events: none;
z-index: 10;
}
.stf__item[class~="--left"]::after {
right: 0;
background: linear-gradient(to left, rgba(0,0,0,0.65), transparent);
}
.stf__item[class~="--right"]::after {
left: 0;
background: linear-gradient(to right, rgba(0,0,0,0.65), transparent);
}
`}} />
<div
ref={containerRef}
className="relative shadow-2xl"
style={{
width: 'min(95vw, 1400px)',
height: 'min(85vh, 900px)',
transformStyle: 'preserve-3d',
}}
/>
>
{/* Static left page */}
{visibleLeft ? (
<div className="absolute left-0 top-0 w-1/2 h-full bg-white overflow-hidden flex items-center justify-center">
<img src={visibleLeft} className={pageImgClass} alt="" draggable={false} />
</div>
) : (
<div
className="absolute left-0 top-0 w-1/2 h-full overflow-hidden"
style={{
background: 'linear-gradient(135deg, #4a3826 0%, #2e2114 55%, #1a1108 100%)',
boxShadow: 'inset 0 0 80px rgba(0,0,0,0.55)',
}}
aria-hidden
/>
)}
{/* Static right page */}
{visibleRight ? (
<div className="absolute right-0 top-0 w-1/2 h-full bg-white overflow-hidden flex items-center justify-center">
<img src={visibleRight} className={pageImgClass} alt="" draggable={false} />
</div>
) : (
<div
className="absolute right-0 top-0 w-1/2 h-full overflow-hidden"
style={{
background: 'linear-gradient(225deg, #4a3826 0%, #2e2114 55%, #1a1108 100%)',
boxShadow: 'inset 0 0 80px rgba(0,0,0,0.55)',
}}
aria-hidden
/>
)}

{/* Flipping overlay — forward (right page rotates left) */}
{flipping === 'forward' && (
<div
className="absolute right-0 top-0 w-1/2 h-full"
style={{
transformOrigin: 'left center',
transformStyle: 'preserve-3d',
animation: 'flipBookForward 700ms ease-in-out forwards',
zIndex: 20,
}}
>
<div className="absolute inset-0 bg-white overflow-hidden flex items-center justify-center" style={{ backfaceVisibility: 'hidden' }}>
{getPage(rightIdx) && <img src={getPage(rightIdx)} className={pageImgClass} alt="" draggable={false} />}
</div>
<div className="absolute inset-0 bg-white overflow-hidden flex items-center justify-center" style={{ backfaceVisibility: 'hidden', transform: 'rotateY(180deg)' }}>
{getPage(nextLeftIdx) && <img src={getPage(nextLeftIdx)} className={pageImgClass} alt="" draggable={false} />}
</div>
</div>
)}

{/* Flipping overlay — backward (left page rotates right) */}
{flipping === 'backward' && (
<div
className="absolute left-0 top-0 w-1/2 h-full"
style={{
transformOrigin: 'right center',
transformStyle: 'preserve-3d',
animation: 'flipBookBackward 700ms ease-in-out forwards',
zIndex: 20,
}}
>
<div className="absolute inset-0 bg-white overflow-hidden flex items-center justify-center" style={{ backfaceVisibility: 'hidden' }}>
{getPage(leftIdx) && <img src={getPage(leftIdx)} className={pageImgClass} alt="" draggable={false} />}
</div>
<div className="absolute inset-0 bg-white overflow-hidden flex items-center justify-center" style={{ backfaceVisibility: 'hidden', transform: 'rotateY(180deg)' }}>
{getPage(prevRightIdx) && <img src={getPage(prevRightIdx)} className={pageImgClass} alt="" draggable={false} />}
</div>
</div>
)}
</div>

<button
onClick={goPrev}
className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/25 text-white w-14 h-14 flex items-center justify-center rounded-full text-3xl z-30 transition-colors"
disabled={spread === 0 || flipping !== null}
className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/25 disabled:opacity-25 disabled:cursor-not-allowed text-white w-14 h-14 flex items-center justify-center rounded-full text-3xl z-30 transition-colors"
title="Pagina precedente"
aria-label="Pagina precedente"
>‹</button>
<button
onClick={goNext}
className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/25 text-white w-14 h-14 flex items-center justify-center rounded-full text-3xl z-30 transition-colors"
disabled={spread >= totalSpreads - 1 || flipping !== null}
className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/10 hover:bg-white/25 disabled:opacity-25 disabled:cursor-not-allowed text-white w-14 h-14 flex items-center justify-center rounded-full text-3xl z-30 transition-colors"
title="Pagina successiva"
aria-label="Pagina successiva"
>›</button>

<div className="absolute bottom-5 left-1/2 -translate-x-1/2 bg-white/15 text-white px-4 py-1.5 rounded-full text-sm font-medium z-30">
{pageCount === 0 ? 'Nessuna pagina' : `${currentPage + 1} / ${pageCount}`}
{pages.length === 0 ? 'Nessuna pagina' : indicatorLabel}
</div>
</div>
);


+ 0
- 21
lib/page-flip/LICENSE Целия файл

@@ -1,21 +0,0 @@
MIT License

Copyright (c) 2020 Nodlik

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 0
- 45
lib/page-flip/index.d.ts Целия файл

@@ -1,45 +0,0 @@
// Type declarations for the vendored page-flip ESM bundle (./index.js).
// Library: https://github.com/Nodlik/StPageFlip — MIT, vendored at v2.0.7.

export interface FlipEvent {
data: unknown;
object: PageFlip;
}

export interface PageFlipSettings {
startPage?: number;
size?: 'fixed' | 'stretch';
width?: number;
height?: number;
minWidth?: number;
maxWidth?: number;
minHeight?: number;
maxHeight?: number;
drawShadow?: boolean;
flippingTime?: number;
usePortrait?: boolean;
startZIndex?: number;
autoSize?: boolean;
maxShadowOpacity?: number;
showCover?: boolean;
mobileScrollSupport?: boolean;
clickEventForward?: boolean;
useMouseEvents?: boolean;
swipeDistance?: number;
showPageCorners?: boolean;
disableFlipByClick?: boolean;
}

export class PageFlip {
constructor(element: HTMLElement, settings: PageFlipSettings);
loadFromImages(urls: string[]): void;
loadFromHTML(elements: HTMLElement[] | NodeListOf<HTMLElement>): void;
flipNext(): void;
flipPrev(): void;
destroy(): void;
update(): void;
getCurrentPageIndex(): number;
getPageCount(): number;
on(event: 'flip' | 'init' | 'update' | 'changeOrientation' | 'changeState', cb: (e: FlipEvent) => void): void;
off(event: string, cb: (e: FlipEvent) => void): void;
}

+ 0
- 1
lib/page-flip/index.js
Файловите разлики са ограничени, защото са твърде много
Целия файл


Зареждане…
Отказ
Запис