From 7d5d31afe6cdda688da39db724a7a86fb8fb41ee Mon Sep 17 00:00:00 2001 From: pollutri Date: Thu, 7 May 2026 15:52:18 +0200 Subject: [PATCH] =?UTF-8?q?Sviluppata=20funzionalit=C3=A0=20per=20il=20car?= =?UTF-8?q?rello=20di=20immagini=20per=20le=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/page.tsx | 93 +++++++++++-- components/PublicGrid.tsx | 271 +++++++++++++++++++++++++------------- types/index.ts | 11 +- 3 files changed, 264 insertions(+), 111 deletions(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 53cdafa..54e894c 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -33,12 +33,12 @@ export default function AdminDashboard() { const handleUpload = async (e: React.ChangeEvent, field: string, isPortal = false) => { if (!e.target.files?.[0]) return; setUploading(prev => ({ ...prev, [field]: true })); - + const formData = new FormData(); formData.append('file', e.target.files[0]); const res = await fetch('/api/upload', { method: 'POST', body: formData }); const data = await res.json(); - + if (data.url) { if (isPortal) { setPortal(prev => ({ ...prev, [field]: data.url })); @@ -49,6 +49,36 @@ export default function AdminDashboard() { setUploading(prev => ({ ...prev, [field]: false })); }; + const handleUploadExtraImage = async (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) return; + setUploading(prev => ({ ...prev, extraImages: true })); + + const uploaded: string[] = []; + for (const file of Array.from(files)) { + const formData = new FormData(); + formData.append('file', file); + const res = await fetch('/api/upload', { method: 'POST', body: formData }); + const data = await res.json(); + if (data.url) uploaded.push(data.url); + } + + setIsEditing(prev => ({ + ...prev, + extraImages: [...(prev?.extraImages || []), ...uploaded], + })); + setUploading(prev => ({ ...prev, extraImages: false })); + // Reset input so the same file can be re-selected if needed + e.target.value = ''; + }; + + const removeExtraImage = (index: number) => { + setIsEditing(prev => ({ + ...prev, + extraImages: (prev?.extraImages || []).filter((_, i) => i !== index), + })); + }; + const handleSaveCard = async () => { if (!isEditing) return; const generateSafeId = () => 'card-' + Date.now().toString(36) + Math.random().toString(36).substring(2); @@ -315,22 +345,59 @@ export default function AdminDashboard() {
+ {/* Cover Image */}
- -
+ +
handleUpload(e, 'imageUrl')} className="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer" /> - {uploading['imageUrl'] &&

Uploading image...

} + {uploading['imageUrl'] &&

Uploading...

}
{isEditing.imageUrl && ( -
- Preview - + title="Remove cover image" + >✕ +
+ )} +
+ + {/* Gallery Images */} +
+ +
+ + {uploading['extraImages'] &&

Uploading...

} +
+ + {/* Thumbnails strip */} + {(isEditing.extraImages || []).length > 0 && ( +
+ {(isEditing.extraImages || []).map((url, i) => ( +
+ {`Gallery +
+ +
+ {i + 1} +
+ ))}
)}
diff --git a/components/PublicGrid.tsx b/components/PublicGrid.tsx index ab76447..bc349a5 100644 --- a/components/PublicGrid.tsx +++ b/components/PublicGrid.tsx @@ -1,88 +1,183 @@ -'use client'; -import { useState, useEffect } from 'react'; -import { Card } from '@/types'; - -export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxCols?: number }) { - const [activeCard, setActiveCard] = useState(null); - - // Prevent background scrolling when modal is open - useEffect(() => { - if (activeCard) document.body.style.overflow = 'hidden'; - else document.body.style.overflow = 'unset'; - return () => { document.body.style.overflow = 'unset'; } - }, [activeCard]); - - // Tailwind classes mapping based on the admin's chosen max columns - const gridClasses: Record = { - 3: 'xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-2 grid-cols-1', // ADDED THIS LINE - 4: 'xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2 grid-cols-1', - 5: 'xl:grid-cols-5 lg:grid-cols-4 md:grid-cols-3 grid-cols-1', - 6: 'xl:grid-cols-6 lg:grid-cols-4 md:grid-cols-3 grid-cols-2', - 7: 'xl:grid-cols-7 lg:grid-cols-5 md:grid-cols-4 grid-cols-2', - 8: 'xl:grid-cols-8 lg:grid-cols-6 md:grid-cols-4 grid-cols-2', - }; - - const activeGridClass = gridClasses[maxCols] || gridClasses[5]; - - return ( - <> -
- {cards.map((card) => ( -
setActiveCard(card)} - 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" - > - {card.imageUrl ? ( - {card.title} - ) : ( -
No Image
- )} -
-

{card.title}

-

- {card.shortDescription} -

-
-
- ))} -
- - {/* Improved Modal Pop-up */} - {activeCard && ( -
setActiveCard(null)} // Click outside to close - > -
e.stopPropagation()} // Prevent clicks inside modal from closing it - > -
- {activeCard.imageUrl && ( - - )} - {/* Improved Close Button */} - -
-
-
{activeCard.cardType.replace('_', ' ')}
-

{activeCard.title}

- {activeCard.fullContent ? ( -
- ) : ( -

{activeCard.shortDescription}

- )} -
-
-
- )} - - ); -} \ No newline at end of file +'use client'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { Card } from '@/types'; + +function ImageCarousel({ images }: { images: string[] }) { + const [current, setCurrent] = useState(0); + const touchStartX = useRef(null); + + const prev = useCallback(() => setCurrent(i => (i - 1 + images.length) % images.length), [images.length]); + const next = useCallback(() => setCurrent(i => (i + 1) % images.length), [images.length]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'ArrowLeft') prev(); + if (e.key === 'ArrowRight') next(); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [prev, next]); + + // Reset to first image when a new card is shown + useEffect(() => { setCurrent(0); }, [images]); + + const onTouchStart = (e: React.TouchEvent) => { + touchStartX.current = e.touches[0].clientX; + }; + const onTouchEnd = (e: React.TouchEvent) => { + if (touchStartX.current === null) return; + const delta = e.changedTouches[0].clientX - touchStartX.current; + if (Math.abs(delta) > 50) delta < 0 ? next() : prev(); + touchStartX.current = null; + }; + + if (images.length === 0) { + return
No Image
; + } + + return ( +
+ {/* Images */} + {images.map((src, i) => ( + + ))} + + {/* Arrows — only if more than one image */} + {images.length > 1 && ( + <> + + + + {/* Dot indicators */} +
+ {images.map((_, i) => ( +
+ + {/* Counter badge */} +
+ {current + 1} / {images.length} +
+ + )} +
+ ); +} + +export default function PublicGrid({ cards, maxCols = 5 }: { cards: Card[], maxCols?: number }) { + const [activeCard, setActiveCard] = useState(null); + + useEffect(() => { + if (activeCard) document.body.style.overflow = 'hidden'; + else document.body.style.overflow = 'unset'; + return () => { document.body.style.overflow = 'unset'; }; + }, [activeCard]); + + const gridClasses: Record = { + 3: 'xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-2 grid-cols-1', + 4: 'xl:grid-cols-4 lg:grid-cols-3 md:grid-cols-2 grid-cols-1', + 5: 'xl:grid-cols-5 lg:grid-cols-4 md:grid-cols-3 grid-cols-1', + 6: 'xl:grid-cols-6 lg:grid-cols-4 md:grid-cols-3 grid-cols-2', + 7: 'xl:grid-cols-7 lg:grid-cols-5 md:grid-cols-4 grid-cols-2', + 8: 'xl:grid-cols-8 lg:grid-cols-6 md:grid-cols-4 grid-cols-2', + }; + + const activeGridClass = gridClasses[maxCols] || gridClasses[5]; + + return ( + <> +
+ {cards.map((card) => ( +
setActiveCard(card)} + 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" + > + {card.imageUrl ? ( + {card.title} + ) : ( +
No Image
+ )} + {/* Gallery badge */} + {card.extraImages && card.extraImages.length > 0 && ( +
+ + {1 + card.extraImages.length} +
+ )} +
+

{card.title}

+

+ {card.shortDescription} +

+
+
+ ))} +
+ + {activeCard && ( +
setActiveCard(null)} + > +
e.stopPropagation()} + > +
+ + +
+
+
{activeCard.cardType.replace('_', ' ')}
+

{activeCard.title}

+ {activeCard.fullContent ? ( +
+ ) : ( +

{activeCard.shortDescription}

+ )} +
+
+
+ )} + + ); +} diff --git a/types/index.ts b/types/index.ts index da55c5f..21fbc8a 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,20 +1,11 @@ export type CardType = 'INFO_PAGE' | 'EXTERNAL_LINK' | 'IMAGE_GALLERY' | 'SERVICE_REQUEST'; -export interface Portal { - id: string; - tenantId: string; - title: string; - welcomeText: string; - heroImageUrl: string; - logoUrl: string; - themeColor: string; -} - export interface Card { id: string; portalId: string; title: string; imageUrl: string; + extraImages?: string[]; shortDescription: string; fullContent: string; cardType: CardType;