// Amenities — three-column hi-fi viewer, replicated from the Embassy Citadel // amenities layout (the stronger of our two reference builds) and re-skinned in // The Universe's ivory/gold brand on this app's React/Babel stack. // // REFERENCE LAYOUT (Embassy Citadel · v4/amenities.html): // · LEFT editorial rail — eyebrow + big serif "Amenities" + tagline + rule, // then a category list (icon · label · NN count) where one row is active, // and a LEVEL filter beneath it. // · CENTER column — one large feature HERO plate (the active item's // render, or the group hero) with a soft gradient + caption, and a floating // prev / NN—NN counter / next control pill overlaid at the foot. // · RIGHT rail — a vertical THUMBNAIL STACK of every item in the // active category (photo + name + level), active thumb carries a gold ring; // scrolls when the group has many items. // // Citadel chosen over Embassy One because its 3-column category-rail + hero + // thumb-stack maps 1:1 onto our data (5 experience groups · 33 items · 4 levels) // and is the richer, more catalogue-forward pattern. We use a RECTANGULAR hero // plate (not Citadel's circle) — our client renders are wide 16:9 and read far // better un-cropped in a rectangle. // // Data: AMENITY_GROUPS (5 groups · 33 items) · AMENITY_COUNT (33). Image paths // assets/renders/client/.jpg and .jpg. Falls back to the group hero // on any per-item image error. Ends with window.Amenities = Amenities. const AM_KEYS_ID = 'uni-amenities-keys'; function ensureAmKeys() { if (typeof document === 'undefined') return; if (document.getElementById(AM_KEYS_ID)) return; const s = document.createElement('style'); s.id = AM_KEYS_ID; s.textContent = ` @keyframes amHeroIn { 0% { opacity: 0; transform: scale(1.05); } 100% { opacity: 1; transform: scale(1.02); } } @keyframes amThumbIn { 0% { opacity: 0; transform: translateX(14px); } 100% { opacity: 1; transform: translateX(0); } } .am-stack::-webkit-scrollbar { width: 8px; } .am-stack::-webkit-scrollbar-track { background: transparent; } .am-stack::-webkit-scrollbar-thumb { background: rgba(176,138,63,0.30); border-radius: 4px; } .am-stack::-webkit-scrollbar-thumb:hover { background: rgba(176,138,63,0.55); } `; document.head.appendChild(s); } const AM_R = (name) => `assets/renders/client/${name}.jpg`; const AM_LEVELS = ['All', 'GF', 'Clubhouse', 'Podium', 'L2']; const AM_LEVEL_LABEL = { All:'All Levels', GF:'Ground Floor', Clubhouse:'Clubhouse', Podium:'Podium', L2:'Level 2' }; // One-line editorial descriptor per experience group (Frank-Curator register). const AM_DESC = { sport: 'A podium of courts, pitches and a running track for recreation at every age.', wellness: 'An Olympic-grade pool, jacuzzi, a clubhouse gym and a yoga & crossfit floor.', social: 'Banquet, café, library, games and guest rooms, interiors curated by HBA.', family: 'A crèche, kids’ water play and a dedicated children’s playground, safe by design.', landscape: 'Acres of landscaping by SWA: water features, seating gardens and accent groves.', }; // ============================================================================ // AmImg — that falls back to the group hero render on load error. // ============================================================================ function AmImg({ src, fallback, alt, style, className }) { const triedRef = React.useRef(false); React.useEffect(() => { triedRef.current = false; }, [src]); return ( {alt { if (!triedRef.current && fallback && ev.currentTarget.src.indexOf(fallback) === -1) { triedRef.current = true; ev.currentTarget.src = fallback; } else { ev.currentTarget.style.opacity = '0'; } }} /> ); } // ============================================================================ // AmCategoryRow — left-rail entry: icon · label · count, active = gold tint. // NOTE: namespaced (Am-) to avoid colliding with gallery.jsx's own top-level // `CategoryRow` — both screens are