fix: widget rooms mode — show real photos, hide mock-only meta fields

- displayRooms now maps r.photos (URL strings) to photoUrls[]
- Collapsed thumbnail: shows <img> if real photo URL exists, falls back to emoji
- Expanded gallery: full-width <img> for real photos, gradient for mock
- Thumbnail strip: renders real photo thumbnails when available
- Photo nav buttons use totalPhotos (real or mock count)
- beds/area/rating hidden when value is 0 (real rooms have no beds/area/rating data)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-12 00:26:06 +03:00
parent c153e12543
commit 66cab853f1

View File

@@ -76,6 +76,7 @@ interface MockRoom {
area: number // m² area: number // m²
description: string description: string
photos: { bg: string; emoji: string; label: string }[] photos: { bg: string; emoji: string; label: string }[]
photoUrls: string[] // real photo URLs (empty for mock rooms)
amenities: string[] amenities: string[]
has3dTour: boolean has3dTour: boolean
} }
@@ -90,7 +91,7 @@ const MOCK_ROOMS: MockRoom[] = [
{ bg: 'from-amber-100 to-amber-200', emoji: '🪟', label: 'Окно' }, { bg: 'from-amber-100 to-amber-200', emoji: '🪟', label: 'Окно' },
], ],
amenities: ['Wi-Fi', 'TV', 'Кондиционер', 'Фен', 'Мини-холодильник', 'Сейф'], amenities: ['Wi-Fi', 'TV', 'Кондиционер', 'Фен', 'Мини-холодильник', 'Сейф'],
has3dTour: false, photoUrls: [], has3dTour: false,
}, },
{ {
id: 'r2', name: 'Комфорт', beds: 1, guests: 2, price: 6500, area: 30, id: 'r2', name: 'Комфорт', beds: 1, guests: 2, price: 6500, area: 30,
@@ -102,7 +103,7 @@ const MOCK_ROOMS: MockRoom[] = [
{ bg: 'from-lime-100 to-lime-200', emoji: '🛋️', label: 'Зона отдыха' }, { bg: 'from-lime-100 to-lime-200', emoji: '🛋️', label: 'Зона отдыха' },
], ],
amenities: ['Wi-Fi', 'TV', 'Кондиционер', 'Мини-бар', 'Халат', 'Фен', 'Сейф', 'Кофемашина'], amenities: ['Wi-Fi', 'TV', 'Кондиционер', 'Мини-бар', 'Халат', 'Фен', 'Сейф', 'Кофемашина'],
has3dTour: false, photoUrls: [], has3dTour: false,
}, },
{ {
id: 'r3', name: 'Делюкс', beds: 2, guests: 3, price: 8900, area: 45, id: 'r3', name: 'Делюкс', beds: 2, guests: 3, price: 8900, area: 45,
@@ -115,7 +116,7 @@ const MOCK_ROOMS: MockRoom[] = [
{ bg: 'from-cyan-100 to-cyan-200', emoji: '🍽️', label: 'Мини-кухня' }, { bg: 'from-cyan-100 to-cyan-200', emoji: '🍽️', label: 'Мини-кухня' },
], ],
amenities: ['Wi-Fi', 'Smart TV', 'Кондиционер', 'Джакузи', 'Мини-бар', 'Халат', 'Тапочки', 'Кофемашина', 'Сейф', 'Балкон'], amenities: ['Wi-Fi', 'Smart TV', 'Кондиционер', 'Джакузи', 'Мини-бар', 'Халат', 'Тапочки', 'Кофемашина', 'Сейф', 'Балкон'],
has3dTour: true, photoUrls: [], has3dTour: true,
}, },
{ {
id: 'r4', name: 'Пентхаус', beds: 2, guests: 4, price: 15000, area: 80, id: 'r4', name: 'Пентхаус', beds: 2, guests: 4, price: 15000, area: 80,
@@ -129,7 +130,7 @@ const MOCK_ROOMS: MockRoom[] = [
{ bg: 'from-amber-100 to-yellow-100', emoji: '🍸', label: 'Бар' }, { bg: 'from-amber-100 to-yellow-100', emoji: '🍸', label: 'Бар' },
], ],
amenities: ['Wi-Fi 1Гбит', 'Smart TV 75"', 'Кондиционер', 'Сауна', 'Мини-бар', 'Джакузи', 'Терраса', 'Дворецкий', 'Кофемашина', 'Халат', 'Тапочки', 'Сейф'], amenities: ['Wi-Fi 1Гбит', 'Smart TV 75"', 'Кондиционер', 'Сауна', 'Мини-бар', 'Джакузи', 'Терраса', 'Дворецкий', 'Кофемашина', 'Халат', 'Тапочки', 'Сейф'],
has3dTour: true, photoUrls: [], has3dTour: true,
}, },
] ]
@@ -239,9 +240,12 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR
// Use real rooms if available, otherwise mock // Use real rooms if available, otherwise mock
const displayRooms = realRooms && realRooms.length > 0 const displayRooms = realRooms && realRooms.length > 0
? realRooms.map(r => ({ ? realRooms.map(r => ({
id: r.id, name: r.name || `Номер ${r.number}`, beds: 1, id: r.id, name: r.name || `Номер ${r.number}`, beds: 0,
guests: r.maxGuests, price: r.baseRate, area: 0, guests: r.maxGuests, price: r.baseRate, area: 0,
description: r.description, photos: [], amenities: r.amenities, description: r.description ?? '',
photos: [] as { bg: string; emoji: string; label: string }[],
photoUrls: r.photos ?? [],
amenities: r.amenities ?? [],
has3dTour: false, has3dTour: false,
})) }))
: MOCK_ROOMS : MOCK_ROOMS
@@ -966,8 +970,11 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR
{displayRooms.map(room => { {displayRooms.map(room => {
const isExpanded = expandedRoom === room.id const isExpanded = expandedRoom === room.id
const isSelected = selected === room.id const isSelected = selected === room.id
const curPhoto = photoIndex[room.id] ?? 0 const hasRealPhotos = room.photoUrls.length > 0
const totalPhotos = hasRealPhotos ? room.photoUrls.length : room.photos.length
const curPhoto = Math.min(photoIndex[room.id] ?? 0, Math.max(0, totalPhotos - 1))
const photo = room.photos[curPhoto] ?? { bg: 'from-slate-200 to-slate-300', emoji: '🛏️', label: 'Фото' } const photo = room.photos[curPhoto] ?? { bg: 'from-slate-200 to-slate-300', emoji: '🛏️', label: 'Фото' }
const curPhotoUrl = hasRealPhotos ? room.photoUrls[curPhoto] : null
return ( return (
<div <div
key={room.id} key={room.id}
@@ -987,9 +994,13 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR
onClick={() => setSelected(room.id === selected ? null : room.id)} onClick={() => setSelected(room.id === selected ? null : room.id)}
> >
{/* Thumbnail */} {/* Thumbnail */}
{curPhotoUrl ? (
<img src={curPhotoUrl} alt={room.name} className="w-14 h-14 rounded-xl object-cover shrink-0" />
) : (
<div className={cn('w-14 h-14 rounded-xl bg-gradient-to-br flex items-center justify-center text-2xl shrink-0', photo.bg)}> <div className={cn('w-14 h-14 rounded-xl bg-gradient-to-br flex items-center justify-center text-2xl shrink-0', photo.bg)}>
{photo.emoji} {photo.emoji}
</div> </div>
)}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-semibold text-slate-900 text-sm">{room.name}</p> <p className="font-semibold text-slate-900 text-sm">{room.name}</p>
@@ -997,13 +1008,14 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR
<span className="text-xs text-slate-500 flex items-center gap-0.5"> <span className="text-xs text-slate-500 flex items-center gap-0.5">
<Users size={10} /> до {room.guests} <Users size={10} /> до {room.guests}
</span> </span>
{room.beds > 0 && (
<span className="text-xs text-slate-500 flex items-center gap-0.5"> <span className="text-xs text-slate-500 flex items-center gap-0.5">
<BedDouble size={10} /> {room.beds} кровати <BedDouble size={10} /> {room.beds} кр.
</span> </span>
)}
{room.area > 0 && (
<span className="text-xs text-slate-500">{room.area} м²</span> <span className="text-xs text-slate-500">{room.area} м²</span>
<span className="text-xs text-slate-500 flex items-center gap-0.5"> )}
<Star size={10} className="fill-amber-400 text-amber-400" />4.8
</span>
</div> </div>
</div> </div>
@@ -1033,16 +1045,17 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR
<div className="border-t border-slate-100"> <div className="border-t border-slate-100">
{/* Photo gallery */} {/* Photo gallery */}
<div className="relative"> <div className="relative">
<div className={cn( {curPhotoUrl ? (
'h-44 bg-gradient-to-br flex items-center justify-center', <img src={curPhotoUrl} alt={room.name} className="w-full h-44 object-cover" />
photo.bg, ) : (
)}> <div className={cn('h-44 bg-gradient-to-br flex items-center justify-center', photo.bg)}>
<span className="text-6xl">{photo.emoji}</span> <span className="text-6xl">{photo.emoji}</span>
<span className="absolute bottom-2 left-3 text-xs text-white/90 font-medium drop-shadow">{photo.label}</span> <span className="absolute bottom-2 left-3 text-xs text-white/90 font-medium drop-shadow">{photo.label}</span>
</div> </div>
)}
{/* Photo nav */} {/* Photo nav */}
{room.photos.length > 1 && ( {totalPhotos > 1 && (
<> <>
<button <button
onClick={() => setPhotoIndex(prev => ({ ...prev, [room.id]: Math.max(0, (prev[room.id] ?? 0) - 1) }))} onClick={() => setPhotoIndex(prev => ({ ...prev, [room.id]: Math.max(0, (prev[room.id] ?? 0) - 1) }))}
@@ -1052,15 +1065,15 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR
<ChevronLeft size={14} /> <ChevronLeft size={14} />
</button> </button>
<button <button
onClick={() => setPhotoIndex(prev => ({ ...prev, [room.id]: Math.min(room.photos.length - 1, (prev[room.id] ?? 0) + 1) }))} onClick={() => setPhotoIndex(prev => ({ ...prev, [room.id]: Math.min(totalPhotos - 1, (prev[room.id] ?? 0) + 1) }))}
disabled={curPhoto === room.photos.length - 1} disabled={curPhoto === totalPhotos - 1}
className="absolute right-2 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/30 hover:bg-black/50 flex items-center justify-center text-white disabled:opacity-0 transition-all" className="absolute right-2 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/30 hover:bg-black/50 flex items-center justify-center text-white disabled:opacity-0 transition-all"
> >
<ChevronRight size={14} /> <ChevronRight size={14} />
</button> </button>
{/* Dots */} {/* Dots */}
<div className="absolute bottom-2 right-3 flex gap-1"> <div className="absolute bottom-2 right-3 flex gap-1">
{room.photos.map((_, i) => ( {Array.from({ length: totalPhotos }).map((_, i) => (
<button <button
key={i} key={i}
onClick={() => setPhotoIndex(prev => ({ ...prev, [room.id]: i }))} onClick={() => setPhotoIndex(prev => ({ ...prev, [room.id]: i }))}
@@ -1072,10 +1085,12 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR
)} )}
{/* Photo count */} {/* Photo count */}
{totalPhotos > 0 && (
<div className="absolute top-2 right-2 flex items-center gap-1 bg-black/30 text-white text-[10px] px-1.5 py-0.5 rounded-full"> <div className="absolute top-2 right-2 flex items-center gap-1 bg-black/30 text-white text-[10px] px-1.5 py-0.5 rounded-full">
<Image size={9} /> <Image size={9} />
{curPhoto + 1}/{room.photos.length} {curPhoto + 1}/{totalPhotos}
</div> </div>
)}
{/* 3D tour badge */} {/* 3D tour badge */}
{room.has3dTour && ( {room.has3dTour && (
@@ -1087,22 +1102,32 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, realR
</div> </div>
{/* Thumbnail strip */} {/* Thumbnail strip */}
{totalPhotos > 1 && (
<div className="flex gap-1.5 px-3 py-2 overflow-x-auto"> <div className="flex gap-1.5 px-3 py-2 overflow-x-auto">
{room.photos.map((p, i) => ( {hasRealPhotos
? room.photoUrls.map((url, i) => (
<button <button
key={i} key={i}
onClick={() => setPhotoIndex(prev => ({ ...prev, [room.id]: i }))} onClick={() => setPhotoIndex(prev => ({ ...prev, [room.id]: i }))}
className={cn( className={cn('w-12 h-9 rounded-lg overflow-hidden shrink-0 transition-all', i === curPhoto ? 'ring-2 ring-offset-1' : 'opacity-60 hover:opacity-90')}
'w-12 h-9 rounded-lg bg-gradient-to-br flex items-center justify-center text-lg shrink-0 transition-all', style={i === curPhoto ? { '--tw-ring-color': settings.primaryColor } as any : {}}
p.bg, >
i === curPhoto ? 'ring-2 ring-offset-1' : 'opacity-60 hover:opacity-90', <img src={url} alt="" className="w-full h-full object-cover" />
)} </button>
))
: room.photos.map((p, i) => (
<button
key={i}
onClick={() => setPhotoIndex(prev => ({ ...prev, [room.id]: i }))}
className={cn('w-12 h-9 rounded-lg bg-gradient-to-br flex items-center justify-center text-lg shrink-0 transition-all', p.bg, i === curPhoto ? 'ring-2 ring-offset-1' : 'opacity-60 hover:opacity-90')}
style={i === curPhoto ? { '--tw-ring-color': settings.primaryColor } as any : {}} style={i === curPhoto ? { '--tw-ring-color': settings.primaryColor } as any : {}}
> >
{p.emoji} {p.emoji}
</button> </button>
))} ))
}
</div> </div>
)}
{/* Description */} {/* Description */}
<div className="px-3 pb-2"> <div className="px-3 pb-2">