고택이 stay2 DOM 이식 1차에 머물러 화면 전체 폭·다른 머리글로 나가 시안과 달랐다. - layouts/paper/paper.css: stay2 CSS 를 .w4p 안으로 옮겼다(@layer components, Tailwind 유틸이 이긴다) - layouts/paper: 머리글·탭·첫 화면·날씨·블로그·소개·객실·사진·영상·주변·예약·이용안내·오시는 길·FAQ·축제·일정·지역 이야기·예약 요청 창 - stay2 하위 페이지(지역 소개·이용안내·이야기)는 #/area · #/stay · #/story 탭 전환, 내용은 한 HTML 에 둔다 - 공용 섹션에서 계산 함수만 꺼내 쓴다: infoView · useBookingRequest · StayBookingDemo 달력 함수 - SectionList: 레이아웃이 blog · reviews · postcardMaker 자리를 바꿀 수 있다 - templates.json paper: 보조색 #5b564e · 본문 고딕 · 모서리 4px · 간격 2.5rem(stay2 값) - app.test: 고택 Frame 표식을 새 클래스(w4p)로 머뭄 데이터로 구워 1280·390px 에서 stay2 와 나란히 비교(첫 페이지 4855/4857px). site tsc·eslint·vitest 105 passed, frontend vite build 통과 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
104 lines
4.0 KiB
TypeScript
104 lines
4.0 KiB
TypeScript
import {useState} from 'react';
|
|
import type {ItineraryItem, PlannerStop} from '@o2o/shared';
|
|
import {useSite} from '@site/lib/site-context';
|
|
import {sectionItems, sectionName} from '@site/lib/derive';
|
|
import {Sheet} from './Sheet';
|
|
|
|
const FIRST = 4;
|
|
|
|
function stopsOf(item: ItineraryItem): PlannerStop[] {
|
|
return item.days?.[0]?.stops ?? item.stops ?? [];
|
|
}
|
|
|
|
export function Itinerary() {
|
|
const payload = useSite();
|
|
const parsed = sectionItems<ItineraryItem>(payload, 'itinerary');
|
|
const [tag, setTag] = useState('');
|
|
const [open, setOpen] = useState<ItineraryItem | null>(null);
|
|
const items = parsed.items;
|
|
if (items.length === 0) return null;
|
|
const tags = [...new Set(items.map((item) => item.duration).filter((d): d is string => Boolean(d)))];
|
|
const shown = items.filter((item) => !tag || item.duration === tag);
|
|
|
|
const card = (item: ItineraryItem) => (
|
|
<button
|
|
key={item.name}
|
|
type="button"
|
|
className="itemcard"
|
|
onClick={() => setOpen(item)}
|
|
style={{padding: '16px 18px', border: '1px solid var(--line)', marginBottom: 10}}
|
|
>
|
|
<p style={{margin: '0 0 4px', fontSize: '15.5px', fontWeight: 500}}>{item.name}</p>
|
|
<p className="cap dim" style={{margin: '0 0 8px'}}>
|
|
{[item.duration, item.audience].filter(Boolean).join(' · ')}
|
|
</p>
|
|
{item.why && <p style={{margin: '0 0 8px', fontSize: '16.5px', lineHeight: 1.8}}>{item.why}</p>}
|
|
<p className="note" style={{margin: 0}}>
|
|
{stopsOf(item)
|
|
.slice(0, 4)
|
|
.map((stop) => [stop.name.split(' · ').pop(), stop.minutes && `${stop.minutes}분`].filter(Boolean).join(' '))
|
|
.join(' → ')}
|
|
</p>
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<section className="sec" id="itinerary" aria-labelledby="itinerary-heading">
|
|
<p className="eyebrow">ITINERARY</p>
|
|
<h2 id="itinerary-heading">
|
|
{parsed.title || sectionName(payload, 'itinerary', '추천 일정')} <span>{items.length}</span>
|
|
</h2>
|
|
<p className="note" style={{margin: '-8px 0 14px'}}>
|
|
{parsed.subtitle || '하루를 어떻게 쓸지 미리 짜 두었습니다.'}
|
|
</p>
|
|
{tags.length > 1 && (
|
|
<div className="tabrow" role="tablist">
|
|
{['', ...tags].map((t) => (
|
|
<button key={t || 'all'} type="button" role="tab" aria-current={t === tag} onClick={() => setTag(t)}>
|
|
{t || '전체'} {items.filter((item) => !t || item.duration === t).length}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
{shown.slice(0, FIRST).map(card)}
|
|
{shown.length > FIRST && (
|
|
<details>
|
|
<summary className="more" style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
|
{shown.length - FIRST}개 더 보기
|
|
</summary>
|
|
{shown.slice(FIRST).map(card)}
|
|
</details>
|
|
)}
|
|
</section>
|
|
<Sheet open={open != null} onClose={() => setOpen(null)} label={open?.name ?? ''}>
|
|
{open && (
|
|
<>
|
|
<h3>{open.name}</h3>
|
|
<p className="meta">{[open.duration, open.audience].filter(Boolean).join(' · ')}</p>
|
|
{open.why && <p>{open.why}</p>}
|
|
{(open.days?.length ? open.days : [{label: undefined, stops: open.stops}]).map((day, i) => (
|
|
<div key={i}>
|
|
{day.label && <p className="meta">{day.label}</p>}
|
|
<ul className="stops">
|
|
{(day.stops ?? []).map((stop, j) => (
|
|
<li key={j}>
|
|
<b>{stop.name}</b>
|
|
{(stop.note || stop.minutes) && (
|
|
<span>
|
|
{[stop.minutes && `${stop.minutes}분 머뭅니다`, stop.note].filter(Boolean).join(' · ')}
|
|
</span>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</Sheet>
|
|
<div className="rule" />
|
|
</>
|
|
);
|
|
}
|