From c8ff5ad0ed948d6001e1bd6f12a2beb53af4de2b Mon Sep 17 00:00:00 2001 From: HotelSync Date: Fri, 10 Apr 2026 11:56:59 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20widget=20category=20cards=20=E2=80=94?= =?UTF-8?q?=20full-width=20photo=20gallery=20with=20carousel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cards now show wide photo (h-44) at top instead of small thumbnail - Gallery navigation: left/right arrows on hover, dot indicators, counter - Gradient overlay with available badge (top-left) and photo count - Select button at bottom of each card - Graceful fallback to 🛏️ placeholder when no photos Co-Authored-By: Claude Sonnet 4.6 --- src/pages/BookingWidgetPage.tsx | 113 +++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 24 deletions(-) diff --git a/src/pages/BookingWidgetPage.tsx b/src/pages/BookingWidgetPage.tsx index f03142c..4136307 100644 --- a/src/pages/BookingWidgetPage.tsx +++ b/src/pages/BookingWidgetPage.tsx @@ -210,6 +210,7 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR const [selected, setSelected] = useState(null) const [expandedRoom, setExpandedRoom] = useState(null) const [photoIndex, setPhotoIndex] = useState>({}) + const [catPhotoIndex, setCatPhotoIndex] = useState>({}) const [step, setStep] = useState<'browse' | 'form' | 'payment' | 'success'>('browse') const [submitting, setSubmitting] = useState(false) const [confirmUrl, setConfirmUrl] = useState(null) @@ -822,55 +823,108 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR )} {displayCategories.map(cat => { const isSelected = selected === cat.id + const curIdx = catPhotoIndex[cat.id] ?? 0 + const photos = cat.photos ?? [] return (
setSelected(cat.id === selected ? null : cat.id)} > -
-
- {/* Photo or icon */} - {cat.photos?.[0] ? ( + {/* Photo gallery */} +
+ {photos.length > 0 ? ( + <> {cat.name} - ) : ( -
- 🛏️ -
- )} + {/* Gradient overlay */} +
+ {/* Nav arrows */} + {photos.length > 1 && ( + <> + + + {/* Dots */} +
+ {photos.map((_, i) => ( +
+ {/* Counter */} + + {curIdx + 1}/{photos.length} + + + )} + {/* Available badge */} + {cat.availableCount > 0 && ( + + {cat.availableCount} своб. + + )} + + ) : ( +
+ 🛏️ + Нет фото +
+ )} +
+ + {/* Card content */} +
setSelected(cat.id === selected ? null : cat.id)} + > +
-

{cat.name}

+

{cat.name}

до {cat.maxGuests} - {cat.availableCount > 0 && ( - · {cat.availableCount} свободно + {photos.length > 0 && ( + + {photos.length} фото + )}
{cat.description && ( -

{cat.description}

+

{cat.description}

)}
-

+

{nights > 0 ? (cat.minPrice * nights).toLocaleString('ru-RU') : cat.minPrice.toLocaleString('ru-RU')} ₽

{nights > 0 ? `за ${nights} ноч.` : 'от/ночь'}

+ {/* Amenities */} {cat.amenities.length > 0 && (
- {cat.amenities.slice(0, 4).map(a => { + {cat.amenities.slice(0, 5).map(a => { const Icon = AMENITY_ICONS[a] return ( @@ -878,11 +932,22 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR ) })} - {cat.amenities.length > 4 && ( - +{cat.amenities.length - 4} + {cat.amenities.length > 5 && ( + +{cat.amenities.length - 5} )}
)} + + {/* Select button */} +
)