/**
* トップ画面: パンフレットを探す。
*
* - キーワード・都道府県・カテゴリで絞り込み
* - 「現在地から探す」で近い順に並べ、その地点を配信範囲に含む優先枠(PR)を上に出す
* - リスト / 地図 の切り替え(地図は表示範囲が変わるたびに /api/v1/pamphlets/map を読む)
*
* iOS アプリと同じ公開 API だけを使う。画面だけの特別な口は作らない。
*/
(function renderCatalogHomeApp() {
'use strict';
const { useState, useEffect, useCallback, useRef } = React;
const rootElement = document.getElementById('catalog-home-root');
const searchEndpointUrl = rootElement.dataset.searchEndpoint;
const mapEndpointUrl = rootElement.dataset.mapEndpoint;
const reelsUrl = rootElement.dataset.reelsUrl;
const categoryLabelsByKey = JSON.parse(rootElement.dataset.categories);
const prefectureNames = JSON.parse(rootElement.dataset.prefectures);
const JAPAN_CENTER = [36.2, 138.25];
const KIND_OPTIONS = [
{ key: '', label: 'すべて' },
{ key: 'pamphlet', label: 'パンフレット' },
{ key: 'poster', label: 'ポスター' },
];
function buildQueryString(searchConditions) {
const queryParameters = new URLSearchParams();
Object.entries(searchConditions).forEach(([parameterName, parameterValue]) => {
if (parameterValue !== null && parameterValue !== undefined && parameterValue !== '') {
queryParameters.set(parameterName, parameterValue);
}
});
return queryParameters.toString();
}
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);
}
function PamphletCard({ pamphlet, isPromoted }) {
const eventPeriod = formatEventPeriod(pamphlet.starts_on, pamphlet.ends_on);
return (
{pamphlet.cover_image_url &&

}
{pamphlet.is_ongoing && 開催中}
{isPromoted && PR}
{pamphlet.title}
{pamphlet.venue && (
{pamphlet.venue.prefecture}・{pamphlet.venue.name}
)}
{pamphlet.kind === 'poster' && ポスター}
{pamphlet.category_label}
{eventPeriod && {eventPeriod}}
{pamphlet.distance_km !== undefined && {pamphlet.distance_km}km}
);
}
function PamphletMap({ searchConditions, currentLocation }) {
const mapContainerRef = useRef(null);
const leafletMapRef = useRef(null);
const markerLayerRef = useRef(null);
const searchConditionsRef = useRef(searchConditions);
searchConditionsRef.current = searchConditions;
const loadPinsInView = useCallback(async () => {
const leafletMap = leafletMapRef.current;
if (!leafletMap) return;
const visibleBounds = leafletMap.getBounds();
const mapResponse = await requestJson(`${mapEndpointUrl}?${buildQueryString({
south: visibleBounds.getSouth().toFixed(5),
west: visibleBounds.getWest().toFixed(5),
north: visibleBounds.getNorth().toFixed(5),
east: visibleBounds.getEast().toFixed(5),
})}`);
const { category: selectedCategory, kind: selectedKind } = searchConditionsRef.current;
markerLayerRef.current.clearLayers();
mapResponse.data
.filter((pamphlet) => !selectedCategory || pamphlet.category === selectedCategory)
.filter((pamphlet) => !selectedKind || pamphlet.kind === selectedKind)
.forEach((pamphlet) => {
const popupElement = document.createElement('div');
const titleLink = document.createElement('a');
titleLink.href = pamphlet.viewer_url;
titleLink.textContent = pamphlet.title;
titleLink.style.fontWeight = '700';
const venueLine = document.createElement('div');
venueLine.textContent = pamphlet.venue.name;
popupElement.append(titleLink, venueLine);
L.marker([pamphlet.venue.latitude, pamphlet.venue.longitude])
.bindPopup(popupElement)
.addTo(markerLayerRef.current);
});
}, []);
useEffect(() => {
const leafletMap = L.map(mapContainerRef.current).setView(
currentLocation ? [currentLocation.latitude, currentLocation.longitude] : JAPAN_CENTER,
currentLocation ? 12 : 5,
);
L.tileLayer(PamphletHubMap.tileUrl, { attribution: PamphletHubMap.attribution, maxZoom: 18 }).addTo(leafletMap);
markerLayerRef.current = L.layerGroup().addTo(leafletMap);
leafletMapRef.current = leafletMap;
if (currentLocation) {
L.circleMarker([currentLocation.latitude, currentLocation.longitude], { radius: 8, color: '#2b3a67', fillOpacity: 0.8 })
.bindTooltip('現在地')
.addTo(leafletMap);
}
leafletMap.on('moveend', loadPinsInView);
loadPinsInView();
return () => leafletMap.remove();
}, [currentLocation, loadPinsInView]);
useEffect(() => {
loadPinsInView();
}, [searchConditions.category, searchConditions.kind, loadPinsInView]);
return
;
}
const FEATURE_INTERVAL_MS = 6000;
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
/**
* トップの「いま注目の催し」。雑誌の表紙のように 1 件を大きく見せ、数秒ごとに次へ切り替える。
* 表紙は切り抜かず紙のまま見せる。マウスを乗せている間・操作中は止める。
*/
function FeaturedHero({ featuredPamphlets }) {
const [currentFeatureIndex, setCurrentFeatureIndex] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const featureCount = featuredPamphlets.length;
const showFeature = useCallback((targetIndex) => {
setCurrentFeatureIndex(((targetIndex % featureCount) + featureCount) % featureCount);
}, [featureCount]);
useEffect(() => {
if (featureCount < 2 || isPaused || prefersReducedMotion) return undefined;
const advanceTimer = window.setTimeout(() => showFeature(currentFeatureIndex + 1), FEATURE_INTERVAL_MS);
return () => window.clearTimeout(advanceTimer);
}, [currentFeatureIndex, featureCount, isPaused, showFeature]);
if (featureCount === 0) {
return (
全国のパンフレット・ポスター・チラシを、無料でスライド閲覧。
);
}
const featuredPamphlet = featuredPamphlets[Math.min(currentFeatureIndex, featureCount - 1)];
const eventPeriod = formatEventPeriod(featuredPamphlet.starts_on, featuredPamphlet.ends_on);
return (
setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
onFocus={() => setIsPaused(true)}
onBlur={() => setIsPaused(false)}
>
全国のパンフレット・ポスター・チラシを、無料でスライド閲覧
{featuredPamphlet.is_ongoing ? 'いま開催中' : 'いま注目の催し'}
{featuredPamphlet.is_ongoing && 開催中}
{featuredPamphlet.kind === 'poster' && ポスター}
{featuredPamphlet.category_label}
{eventPeriod && <>- 開催
- {eventPeriod}
>}
{featuredPamphlet.venue && <>- 会場
- {featuredPamphlet.venue.prefecture} {featuredPamphlet.venue.name}
>}
{featuredPamphlet.page_count > 1 && <>- ページ
- {featuredPamphlet.page_count}ページ
>}
{featureCount > 1 && (
)}
);
}
function CatalogHomeApp() {
const [enteredKeyword, setEnteredKeyword] = useState('');
const [searchConditions, setSearchConditions] = useState({ keyword: '', kind: '', category: '', prefecture: '' });
const [currentLocation, setCurrentLocation] = useState(null);
const [isLocating, setIsLocating] = useState(false);
const [displayMode, setDisplayMode] = useState('list');
const [listedPamphlets, setListedPamphlets] = useState([]);
const [promotedPamphlets, setPromotedPamphlets] = useState([]);
const [paginationMeta, setPaginationMeta] = useState(null);
const [isLoadingPamphlets, setIsLoadingPamphlets] = useState(true);
const [loadErrorMessage, setLoadErrorMessage] = useState('');
const [heroPamphlets, setHeroPamphlets] = useState([]);
const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false);
const activeFilterCount = [searchConditions.prefecture, searchConditions.category].filter(Boolean).length;
const loadPamphlets = useCallback(async (pageNumber) => {
setIsLoadingPamphlets(true);
setLoadErrorMessage('');
try {
// 開催地の無いチラシ・広告なども受け取れるので include_without_venue を付ける
const searchResponse = await requestJson(`${searchEndpointUrl}?${buildQueryString({
include_without_venue: 1,
...searchConditions,
latitude: currentLocation?.latitude,
longitude: currentLocation?.longitude,
page: pageNumber,
})}`);
setListedPamphlets((alreadyListedPamphlets) => (
pageNumber === 1 ? searchResponse.data : [...alreadyListedPamphlets, ...searchResponse.data]
));
setHeroPamphlets((currentHeroPamphlets) => (
currentHeroPamphlets.length > 0
? currentHeroPamphlets
// 開催中のものを先に、あとは開催が近い順(一覧の並び)で最大 5 件
: [...searchResponse.data]
.filter((pamphlet) => pamphlet.cover_image_url)
.sort((pamphletA, pamphletB) => Number(pamphletB.is_ongoing) - Number(pamphletA.is_ongoing))
.slice(0, 5)
));
if (pageNumber === 1) setPromotedPamphlets(searchResponse.promoted);
setPaginationMeta(searchResponse.meta);
} catch (caughtError) {
setLoadErrorMessage('読み込みに失敗しました。時間をおいて再度お試しください。');
} finally {
setIsLoadingPamphlets(false);
}
}, [searchConditions, currentLocation]);
useEffect(() => {
loadPamphlets(1);
}, [loadPamphlets]);
const handleSearchSubmit = useCallback((submitEvent) => {
submitEvent.preventDefault();
setSearchConditions((currentConditions) => ({ ...currentConditions, keyword: enteredKeyword }));
}, [enteredKeyword]);
const handleLocateClick = useCallback(() => {
if (currentLocation) {
setCurrentLocation(null);
return;
}
if (!navigator.geolocation) {
window.alert('このブラウザでは位置情報を利用できません。');
return;
}
setIsLocating(true);
navigator.geolocation.getCurrentPosition(
(position) => {
setCurrentLocation({
latitude: Number(position.coords.latitude.toFixed(5)),
longitude: Number(position.coords.longitude.toFixed(5)),
});
setIsLocating(false);
document.getElementById('catalog-results')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
},
() => {
window.alert('位置情報を取得できませんでした。ブラウザの設定で位置情報の利用を許可してください。');
setIsLocating(false);
},
{ enableHighAccuracy: false, timeout: 10000, maximumAge: 300000 },
);
}, [currentLocation]);
const hasMorePages = paginationMeta && paginationMeta.current_page < paginationMeta.last_page;
return (
<>
{/* 検索: 表にはキーワードと1行の操作(種類・近い順・絞り込み)だけ。都道府県とカテゴリは「絞り込み」の中 */}
{KIND_OPTIONS.map((kindOption) => (
setSearchConditions((currentConditions) => ({ ...currentConditions, kind: kindOption.key }))}>
{kindOption.label}
))}
{isLocating ? '取得中…' : '近い順'}
setIsFilterPanelOpen((isOpen) => !isOpen)} aria-expanded={isFilterPanelOpen}>
絞り込み{activeFilterCount > 0 && {activeFilterCount}}
{activeFilterCount > 0 && !isFilterPanelOpen && (
{searchConditions.prefecture && (
setSearchConditions((currentConditions) => ({ ...currentConditions, prefecture: '' }))}>
{searchConditions.prefecture}×
)}
{searchConditions.category && (
setSearchConditions((currentConditions) => ({ ...currentConditions, category: '' }))}>
{categoryLabelsByKey[searchConditions.category]}×
)}
)}
{isFilterPanelOpen && (
カテゴリ
setSearchConditions((currentConditions) => ({ ...currentConditions, category: '' }))}>すべて
{Object.entries(categoryLabelsByKey).map(([categoryKey, categoryLabel]) => (
setSearchConditions((currentConditions) => ({ ...currentConditions, category: categoryKey }))}>
{categoryLabel}
))}
setSearchConditions((currentConditions) => ({ ...currentConditions, prefecture: '', category: '' }))}>
条件をクリア
setIsFilterPanelOpen(false)}>閉じる
)}
setDisplayMode('list')}>リスト
setDisplayMode('map')}>地図
リール
{paginationMeta && displayMode === 'list' && (
{paginationMeta.total}件
)}
{displayMode === 'map' ? (
) : (
<>
{promotedPamphlets.length > 0 && (
近くのおすすめPICK UP
PR
{promotedPamphlets.map((promotedPamphlet) => (
))}
)}
{currentLocation ? '近い順' : '開催が近い順'}PAMPHLETS
{loadErrorMessage &&
{loadErrorMessage}
}
{!isLoadingPamphlets && listedPamphlets.length === 0 ? (
条件に合うパンフレット・ポスター・チラシはまだありません。
) : (
{listedPamphlets.map((listedPamphlet) => (
))}
)}
{isLoadingPamphlets && 読み込み中…}
{!isLoadingPamphlets && hasMorePages && (
loadPamphlets(paginationMeta.current_page + 1)}>
もっと見る
)}
>
)}
>
);
}
ReactDOM.createRoot(rootElement).render();
})();