条件に合うパンフレット・ポスター・チラシはまだありません。
すべて見終わりました
キーワードや地域で探すこともできます。
一覧で探す/**
* リール: スマホ向けの全画面・縦スワイプのパンフレット閲覧。
*
* - 上下スワイプで次 / 前のパンフレット(CSS の scroll-snap で 1 件ずつ止める)
* - 左右スワイプでそのパンフレットのページ送り(同じく scroll-snap)
* - 「近く」を押すと現在地から近い順になり、その地点を配信範囲に含む優先枠(PR)を先頭に混ぜる
* - 一覧は公開 API(/api/v1/pamphlets)をページ単位で読み足す。ページ画像は画面に近づいた分だけ詳細 API で取る
* - 1.5 秒以上表示されたパンフレットを 1 回だけ閲覧として記録する
*/
(function renderPamphletReels() {
'use strict';
const { useState, useEffect, useCallback, useRef } = React;
const rootElement = document.getElementById('pamphlet-reels-root');
const searchEndpointUrl = rootElement.dataset.searchEndpoint;
const detailEndpointTemplate = rootElement.dataset.detailEndpointTemplate;
const viewEndpointTemplate = rootElement.dataset.viewEndpointTemplate;
const listUrl = rootElement.dataset.listUrl;
const categoryLabelsByKey = JSON.parse(rootElement.dataset.categories);
// お気に入り。ログインしていなければ、押したときにログイン画面へ(ログイン後にリールへ戻る)
const isSignedIn = rootElement.dataset.isSignedIn === '1';
const favoritePamphletIdSet = new Set(JSON.parse(rootElement.dataset.favoritePamphletIds || '[]'));
const favoriteEndpointTemplate = rootElement.dataset.favoriteEndpointTemplate;
const loginUrl = rootElement.dataset.loginUrl;
const PAMPHLETS_PER_REQUEST = 12;
/** 残りがこの件数になったら次のページを読み足す */
const PREFETCH_REMAINING_COUNT = 3;
/** 表示中の前後この件数までページ画像を読んでおく */
const DETAIL_PRELOAD_RANGE = 1;
const VIEW_RECORD_DELAY_MS = 1500;
const SWIPE_HINT_STORAGE_KEY = 'pamphletReelsSwipeHintSeen';
function formatEventPeriod(startsOn, endsOn) {
if (!startsOn) return null;
const formatDate = (isoDate) => {
const [, month, day] = isoDate.split('-');
return `${Number(month)}/${Number(day)}`;
};
return endsOn && endsOn !== startsOn ? `${formatDate(startsOn)}〜${formatDate(endsOn)}` : formatDate(startsOn);
}
const ICON_PATHS = {
book: 'M4 5.5C6.5 4.5 9.5 4.5 12 6c2.5-1.5 5.5-1.5 8-.5v13c-2.5-1-5.5-1-8 .5-2.5-1.5-5.5-1.5-8-.5z M12 6v13.5',
pin: 'M12 21s-6.5-6.2-6.5-11a6.5 6.5 0 0 1 13 0c0 4.8-6.5 11-6.5 11z M12 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z',
share: 'M12 3v12 M7.5 7.5 12 3l4.5 4.5 M5 12v7.5h14V12',
list: 'M4 6h16 M4 12h16 M4 18h16',
near: 'M12 2v3 M12 19v3 M2 12h3 M19 12h3 M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10z',
heart: 'M12 20.5s-7.5-4.6-7.5-10.2A4.3 4.3 0 0 1 12 7.6a4.3 4.3 0 0 1 7.5 2.7c0 5.6-7.5 10.2-7.5 10.2z',
calendar: 'M5 5h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z M3 10h18 M8 3v4 M16 3v4 M12 13v5 M9.5 15.5h5',
};
function ReelIcon({ name, isFilled = false }) {
return (
);
}
function buildSearchUrl(searchConditions, pageNumber) {
// 開催地の無いチラシ・広告なども受け取れるので include_without_venue を付ける
const queryParameters = new URLSearchParams({ page: String(pageNumber), per_page: String(PAMPHLETS_PER_REQUEST), include_without_venue: '1' });
Object.entries(searchConditions).forEach(([parameterName, parameterValue]) => {
if (parameterValue !== null && parameterValue !== undefined && parameterValue !== '') {
queryParameters.set(parameterName, String(parameterValue));
}
});
return `${searchEndpointUrl}?${queryParameters}`;
}
/** 1 件分。横方向のページ送りと、下部の情報・右側の操作ボタン */
/**
* ページのタップ: 1 回でバーやボタンを消してパンフレットだけを見せる(もう 1 回で戻す)。
* 2 回(ダブルタップ)で、拡大できる閲覧ページを開く。
*/
function usePageTapHandler(onSingleTap, onDoubleTap) {
const pendingTapTimerRef = useRef(null);
return useCallback(() => {
if (pendingTapTimerRef.current) {
window.clearTimeout(pendingTapTimerRef.current);
pendingTapTimerRef.current = null;
onDoubleTap();
return;
}
pendingTapTimerRef.current = window.setTimeout(() => {
pendingTapTimerRef.current = null;
onSingleTap();
}, 260);
}, [onSingleTap, onDoubleTap]);
}
function ReelItem({ reelEntry, pamphletDetail, itemIndex, onBecameVisible, onToggleChrome }) {
const itemElementRef = useRef(null);
const pageTrackRef = useRef(null);
const [currentPageIndex, setCurrentPageIndex] = useState(0);
const { pamphlet, isPromoted } = reelEntry;
// 縦スクロールで画面の大半に入ったら「いま表示中」と親に知らせる
useEffect(() => {
const visibilityObserver = new IntersectionObserver(([observedEntry]) => {
if (observedEntry.isIntersecting) onBecameVisible(itemIndex);
}, { threshold: 0.6 });
visibilityObserver.observe(itemElementRef.current);
return () => visibilityObserver.disconnect();
}, [itemIndex, onBecameVisible]);
const handlePageTrackScroll = useCallback(() => {
const pageTrack = pageTrackRef.current;
setCurrentPageIndex(Math.round(pageTrack.scrollLeft / pageTrack.clientWidth));
}, []);
const scrollToPage = useCallback((targetPageIndex) => {
const pageTrack = pageTrackRef.current;
pageTrack.scrollTo({ left: targetPageIndex * pageTrack.clientWidth, behavior: 'smooth' });
}, []);
const [isFavorite, setIsFavorite] = useState(() => favoritePamphletIdSet.has(pamphlet.id));
const handleFavoriteClick = useCallback(async () => {
if (!isSignedIn) {
window.location.href = loginUrl;
return;
}
const willBeFavorite = !isFavorite;
setIsFavorite(willBeFavorite);
try {
await requestJson(favoriteEndpointTemplate.replace('__ID__', String(pamphlet.id)), { method: willBeFavorite ? 'PUT' : 'DELETE' });
if (willBeFavorite) favoritePamphletIdSet.add(pamphlet.id); else favoritePamphletIdSet.delete(pamphlet.id);
} catch (favoriteError) {
setIsFavorite(!willBeFavorite);
window.alert(favoriteError.message || 'お気に入りを変更できませんでした。');
}
}, [isFavorite, pamphlet.id]);
const handleShareClick = useCallback(async () => {
const shareData = { title: pamphlet.title, url: pamphlet.viewer_url };
if (navigator.share) {
navigator.share(shareData).catch(() => {});
} else if (navigator.clipboard) {
await navigator.clipboard.writeText(pamphlet.viewer_url);
window.alert('リンクをコピーしました。');
}
}, [pamphlet]);
// 詳細が届くまでは表紙 1 枚だけで表示しておく
const pages = pamphletDetail
? pamphletDetail.pages
: [{ number: 1, image_url: pamphlet.cover_image_url, width: 3, height: 4 }];
const eventPeriod = formatEventPeriod(pamphlet.starts_on, pamphlet.ends_on);
const totalPageCount = pamphlet.page_count || pages.length;
const handlePageTap = usePageTapHandler(onToggleChrome, () => { window.location.href = pamphlet.viewer_url; });
return (
{
// 横長のページ(フロアマップなど)は、全画面のときに 90 度回して縦いっぱいに出す
const loadedImage = loadEvent.currentTarget;
const pageElement = loadedImage.parentElement;
pageElement.classList.toggle('is-landscape', loadedImage.naturalWidth > loadedImage.naturalHeight * 1.15);
pageElement.style.setProperty('--page-aspect', String(loadedImage.naturalWidth / loadedImage.naturalHeight));
}}
/>
)}
条件に合うパンフレット・ポスター・チラシはまだありません。
すべて見終わりました
キーワードや地域で探すこともできます。
一覧で探す