Ver a proveniência

Book card type added

Sviluppo_Carrello_Immagini
Lorenzo Pollutri há 1 mês
ascendente
cometimento
c3911fcf30
3 ficheiros alterados com 207 adições e 3 eliminações
  1. +3
    -0
      app/admin/page.tsx
  2. +203
    -2
      components/PublicGrid.tsx
  3. +1
    -1
      types/index.ts

+ 3
- 0
app/admin/page.tsx Ver ficheiro

@@ -667,6 +667,7 @@ export default function AdminDashboard() {
options={[
{ value: 'INFO_PAGE', label: 'Info Page' },
{ value: 'IMAGE_GALLERY', label: 'Image Gallery' },
{ value: 'BOOK', label: 'Book (Flip-Book)' },
...(externalLinksOn ? [{ value: 'EXTERNAL_LINK' as CardType, label: 'External Link' }] : []),
]}
/>
@@ -715,6 +716,7 @@ export default function AdminDashboard() {
<textarea value={isEditing.shortDescription || ''} onChange={e => setIsEditing({ ...isEditing, shortDescription: e.target.value })} className={`${inputClasses} h-24 resize-none`} placeholder="Brief summary..." />
</div>
)}
{isEditing.cardType !== 'BOOK' && (
<div className="bg-gray-50 p-3 rounded-lg border border-gray-200 space-y-3">
<label className="flex items-start gap-3 cursor-pointer">
<input
@@ -741,6 +743,7 @@ export default function AdminDashboard() {
</div>
</label>
</div>
)}
</div>
<div className="space-y-5">
{/* Cover Image */}


+ 203
- 2
components/PublicGrid.tsx Ver ficheiro

@@ -421,6 +421,200 @@ function FullscreenViewer({
);
}

function playFlipSound(ctx: AudioContext | null) {
if (!ctx) return;
const duration = 0.28;
const sr = ctx.sampleRate;
const buffer = ctx.createBuffer(1, Math.floor(sr * duration), sr);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
const t = i / sr;
const envelope = Math.exp(-t * 14) * (1 - Math.exp(-t * 80));
data[i] = (Math.random() * 2 - 1) * envelope * 0.45;
}
const src = ctx.createBufferSource();
src.buffer = buffer;
const bp = ctx.createBiquadFilter();
bp.type = 'bandpass';
bp.frequency.value = 2200;
bp.Q.value = 0.9;
const gain = ctx.createGain();
gain.gain.value = 0.6;
src.connect(bp); bp.connect(gain); gain.connect(ctx.destination);
src.start();
}

function FlipBook({ pages, onClose }: { pages: string[]; onClose: () => void }) {
const [spread, setSpread] = useState(0);
const [flipping, setFlipping] = useState<'forward' | 'backward' | null>(null);
const audioRef = useRef<AudioContext | null>(null);
const touchStartX = useRef<number | null>(null);

const getCtx = () => {
if (!audioRef.current) {
const Ctx = (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext }).AudioContext
?? (window as unknown as { AudioContext?: typeof AudioContext; webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (Ctx) audioRef.current = new Ctx();
}
return audioRef.current;
};

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;
playFlipSound(getCtx());
setFlipping('forward');
window.setTimeout(() => { setSpread(s => s + 1); setFlipping(null); }, 650);
}, [flipping, spread, totalSpreads]);

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

useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
else if (e.key === 'ArrowRight') goNext();
else if (e.key === 'ArrowLeft') goPrev();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose, goNext, goPrev]);

const onTouchStart = (e: React.TouchEvent) => { touchStartX.current = e.touches[0].clientX; };
const onTouchEnd = (e: React.TouchEvent) => {
if (touchStartX.current === null) return;
const dx = e.changedTouches[0].clientX - touchStartX.current;
if (Math.abs(dx) > 50) (dx < 0 ? goNext() : goPrev());
touchStartX.current = null;
};

// Pages visible underneath the flipping overlay
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 pageBoxClass = 'absolute inset-0 bg-white overflow-hidden';
const pageImgClass = 'w-full h-full object-contain';

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/95"
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
style={{ perspective: '2500px' }}
>
<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"
title="Chiudi"
aria-label="Chiudi"
>✕</button>

<div
className="relative shadow-2xl"
style={{
width: 'min(95vw, 1400px)',
height: 'min(85vh, 900px)',
transformStyle: 'preserve-3d',
}}
>
{/* Static left page */}
<div className="absolute left-0 top-0 w-1/2 h-full bg-white overflow-hidden">
{visibleLeft && <img src={visibleLeft} className={pageImgClass} alt="" draggable={false} />}
</div>
{/* Static right page */}
<div className="absolute right-0 top-0 w-1/2 h-full bg-white overflow-hidden">
{visibleRight && <img src={visibleRight} className={pageImgClass} alt="" draggable={false} />}
</div>
{/* Spine */}
<div className="absolute left-1/2 top-0 -translate-x-1/2 w-2 h-full bg-gradient-to-r from-black/40 via-black/20 to-black/40 z-10 pointer-events-none" />

{/* 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 650ms ease-in-out forwards',
zIndex: 20,
}}
>
<div className={pageBoxClass} style={{ backfaceVisibility: 'hidden' }}>
{getPage(rightIdx) && <img src={getPage(rightIdx)} className={pageImgClass} alt="" draggable={false} />}
</div>
<div className={pageBoxClass} 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 650ms ease-in-out forwards',
zIndex: 20,
}}
>
<div className={pageBoxClass} style={{ backfaceVisibility: 'hidden' }}>
{getPage(leftIdx) && <img src={getPage(leftIdx)} className={pageImgClass} alt="" draggable={false} />}
</div>
<div className={pageBoxClass} style={{ backfaceVisibility: 'hidden', transform: 'rotateY(180deg)' }}>
{getPage(prevRightIdx) && <img src={getPage(prevRightIdx)} className={pageImgClass} alt="" draggable={false} />}
</div>
</div>
)}
</div>

<button
onClick={goPrev}
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}
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">
{pages.length === 0 ? 'Nessuna pagina' : indicatorLabel}
</div>
</div>
);
}

export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxCols?: number }) {
const [activeCard, setActiveCard] = useState<Card | null>(null);
const [fullscreenIndex, setFullscreenIndex] = useState<number | null>(null);
@@ -471,7 +665,7 @@ export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxC
}
}
setActiveCard(card);
if (card.autoFullscreen) setFullscreenIndex(0);
if (card.cardType !== 'BOOK' && card.autoFullscreen) setFullscreenIndex(0);
}}
className="group relative cursor-pointer overflow-hidden rounded-xl shadow-md aspect-square bg-gray-200 transition-all duration-300 hover:shadow-xl hover:-translate-y-1"
>
@@ -515,7 +709,14 @@ export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxC
})}
</div>

{activeCard && fullscreenIndex === null && (
{activeCard && activeCard.cardType === 'BOOK' && (
<FlipBook
pages={(activeCard.extraMedia || []).map(m => m.url)}
onClose={() => setActiveCard(null)}
/>
)}

{activeCard && activeCard.cardType !== 'BOOK' && fullscreenIndex === null && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-md p-4"
onClick={() => setActiveCard(null)}


+ 1
- 1
types/index.ts Ver ficheiro

@@ -1,4 +1,4 @@
export type CardType = 'INFO_PAGE' | 'EXTERNAL_LINK' | 'IMAGE_GALLERY' | 'SERVICE_REQUEST';
export type CardType = 'INFO_PAGE' | 'EXTERNAL_LINK' | 'IMAGE_GALLERY' | 'SERVICE_REQUEST' | 'BOOK';
export type MediaItem = {
url: string;


Carregando…
Cancelar
Guardar