- 라운드·코랄·미니멀·솔숲: 히어로의 ‘주차 가능’ 칩 삭제. 일러스트: 지역 말풍선 삭제 - kit/heroSub.ts: 소문구 = 사장님 문구, 없으면 가장 가까운 명소까지 거리. 솔숲에만 있던 규칙을 9종 공통으로 - 고택·부티크: 캐치프레이즈 아래로 소문구 순서 변경 - 부티크 모바일: 전체화면 사진(664px)을 데스크톱과 같은 아치로 — 첫 화면 593px - docs/TEMPLATE_DESIGN.md: 첫 화면 규칙 tsc·eslint 통과 · vitest 123 passed · 9종 모바일·1440px 첫 화면 캡처 확인 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
import {useEffect, useState} from 'react';
|
|
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
|
|
import {useSite} from '@site/lib/site-context';
|
|
import {bookingActionLabel, bookingLinks, isSectionEnabled, stayBookingView} from '@site/lib/derive';
|
|
import {PlaceCategory} from '@o2o/shared';
|
|
import {heroSub} from '../kit/heroSub';
|
|
import {BOOK_EVENT} from '../paper/Rooms';
|
|
import {pad2} from './ui';
|
|
import {Weather} from './Weather';
|
|
|
|
const MAX_SLIDES = 5;
|
|
|
|
export function Hero() {
|
|
const payload = useSite();
|
|
const {place, narrative} = payload;
|
|
const slides = [...payload.media]
|
|
.filter((image) => image.alt?.trim())
|
|
.sort((a, b) => Number(b.isPrimary) - Number(a.isPrimary))
|
|
.slice(0, MAX_SLIDES);
|
|
const [index, setIndex] = useState(0);
|
|
const total = slides.length;
|
|
useEffect(() => {
|
|
if (total < 2) return;
|
|
const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
const timer = window.setInterval(() => {
|
|
if (!document.hidden && !motion.matches) setIndex((was) => (was + 1) % total);
|
|
}, 5000);
|
|
return () => window.clearInterval(timer);
|
|
}, [total]);
|
|
const sub = heroSub(payload);
|
|
const english = place.englishName?.trim();
|
|
const locality = [place.addressLocality, place.addressRegion].find(Boolean);
|
|
const stayBooking = place.category === PlaceCategory.LODGING ? stayBookingView(payload) : null;
|
|
const booking = bookingLinks(payload)[0];
|
|
|
|
return (
|
|
<>
|
|
<section
|
|
className={total > 0 ? 'b-hero' : 'b-hero b-hero--plain'}
|
|
id="top"
|
|
data-pg="home"
|
|
role="region"
|
|
aria-roledescription="carousel"
|
|
aria-label={`${place.name} 사진`}
|
|
>
|
|
{slides.map((image, i) => (
|
|
<div key={image.mediaId} className={i === index ? 'b-slide on' : 'b-slide'} aria-hidden={i !== index}>
|
|
<img
|
|
src={image.url}
|
|
alt={image.alt}
|
|
fetchPriority={i === 0 ? 'high' : 'low'}
|
|
loading={i === 0 ? undefined : 'lazy'}
|
|
decoding="async"
|
|
/>
|
|
</div>
|
|
))}
|
|
<div className="b-hero-copy">
|
|
{english && <p className="b-hero-en">{english}</p>}
|
|
<h1 className={english ? 'b-hero-name' : 'b-hero-name b-hero-name--solo'}>{place.name}</h1>
|
|
{locality && <p className="b-hero-loc">{locality}</p>}
|
|
<p className="b-hero-cp">
|
|
<HeroCatchphrase />
|
|
</p>
|
|
{sub && <p className="b-hero-ko">{sub}</p>}
|
|
<div className="b-hero-act">
|
|
{stayBooking ? (
|
|
<button type="button" className="btn" onClick={() => window.dispatchEvent(new Event(BOOK_EVENT))}>
|
|
예약하기
|
|
</button>
|
|
) : (
|
|
booking && (
|
|
<a className="btn" href={booking.url} target="_blank" rel="noopener noreferrer">
|
|
{bookingActionLabel(booking)}
|
|
</a>
|
|
)
|
|
)}
|
|
<a className="btn ghost" href="#/stay">
|
|
이용안내 보기
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{total > 1 && (
|
|
<p className="b-hero-count" aria-live="polite">
|
|
<span>{pad2(index + 1)}</span> / {pad2(total)}
|
|
</p>
|
|
)}
|
|
</section>
|
|
{isSectionEnabled(payload, 'weather') && (
|
|
<div data-editor-id="weather" style={{display: 'contents'}}>
|
|
<Weather />
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|