const { useState, useEffect } = React;
const { createRoot } = ReactDOM;
// 検索入力のデバウンス時間(ミリ秒)
const SEARCH_DEBOUNCE_MS = 300;
// 区分(kind)の表示順。サーバーの enum 定義順に合わせる
const KIND_ORDER = ['todofuken', 'tokubetsu_ku', 'seirei_shitei_toshi', 'chukaku_shi', 'shikouji_tokurei_shi', 'ippan_shi', 'cho', 'mura'];
// 主要区分(都道府県〜中核市)のセクションを区別するための border 色。
// その他の区分(一般市以下)は強調しない
const KIND_BORDER_COLORS = {
todofuken: '#0d6efd',
tokubetsu_ku: '#b8860b',
seirei_shitei_toshi: '#6f42c1',
shikouji_tokurei_shi: '#0f766e',
chukaku_shi: '#dc3545',
};
// 指標のアイコン。ホバーで CSS ツールチップ(data-label の中身)に指標名を表示する
const MetricIcon = ({ label, iconClass }) => (
);
// 比較モード中にカード下部へ出す操作エリア。
// カード全体が なので、preventDefault / stopPropagation でカルテへの遷移を止める
const CompareControl = ({ shichoson, compareFirstId, onToggleFirst }) => {
if (compareFirstId && compareFirstId !== shichoson.id) {
// 1つめが選ばれていれば、他のカードは「このまちと比較」で比較画面へ
return (
);
}
// 1つめの選択チェックボックス(選択済みカードはチェック解除で選び直せる)
return (
{
e.preventDefault();
e.stopPropagation();
onToggleFirst(shichoson.id);
}}
>
);
};
// 自治体カード。クリックで3分カルテへ遷移する
const ShichosonCard = ({ shichoson, compareMode, compareFirstId, onToggleFirst }) => (
{shichoson.name}
{shichoson.top_type_name && (
{shichoson.top_type_name}
)}
{Object.entries(shichoson.metrics).map(([key, metric]) => (
{metric.text === null ? '—' : metric.text}
))}
{shichoson.summary_50 &&
{shichoson.summary_50}
}
{compareMode && (
)}
);
// 区分(kind)ごとのサブ見出しを KIND_ORDER の順で作る
const groupByKind = (shichosons) =>
KIND_ORDER.flatMap((kind) => {
const items = shichosons.filter((shichoson) => shichoson.kind === kind);
return items.length > 0 ? [{ kind, kindName: items[0].kind_name, shichosons: items }] : [];
});
// 地域ブロックごとのアコーディオン。開いたときだけ中の自治体カードを描画する
const RegionAccordionItem = ({ group, isOpen, onToggle, compareMode, compareFirstId, onToggleFirst }) => (
{groupByKind(group.shichosons).map((kindGroup) => {
const borderColor = KIND_BORDER_COLORS[kindGroup.kind];
return (
{kindGroup.kindName}
{kindGroup.shichosons.length}
{kindGroup.shichosons.map((shichoson) => (
))}
);
})}
);
// サーバーの並び順(都道府県コード順・名前順)を保ったまま地域ブロックごとにまとめる。
// 都道府県コード順では同じ地域の都道府県が隣り合わないことがあるため、連続でなくても
// 同じ region は同じグループにまとめる(例: 北関東甲信の ibaraki..gunma と yamanashi..nagano)
const groupByRegion = (shichosons) => {
const groups = [];
const groupByRegion = new Map();
shichosons.forEach((shichoson) => {
const group = groupByRegion.get(shichoson.region);
if (group) {
group.shichosons.push(shichoson);
} else {
const newGroup = {
region: shichoson.region,
regionName: shichoson.region_name,
shichosons: [shichoson],
};
groupByRegion.set(shichoson.region, newGroup);
groups.push(newGroup);
}
});
return groups;
};
// URL の ?compare= から比較モードの初期状態を読み取る。
// 「compare=1」はトグルONのみ、「compare=」は1つめのまちも選択済みにする(3分カルテの「くらべる」導線)
const initialCompareParam = () => new URLSearchParams(window.location.search).get('compare');
// URL の ?name= から検索ワードを読み取る(LPトップの検索フォームからの遷移を受ける)
const initialNameParam = () => new URLSearchParams(window.location.search).get('name') || '';
// 自治体一覧。名前・データ種別での絞り込みはリアクティブに /api/shichosons を叩く
const ShichosonIndex = ({ apiUrl }) => {
const [name, setName] = useState(initialNameParam);
const [checkedFeatures, setCheckedFeatures] = useState({});
const compareParam = initialCompareParam();
const [compareMode, setCompareMode] = useState(compareParam !== null);
const [compareFirstId, setCompareFirstId] = useState(compareParam && compareParam !== '1' ? compareParam : null);
const [shichosons, setShichosons] = useState([]);
const [featureLabels, setFeatureLabels] = useState({});
const [openRegions, setOpenRegions] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const activeFeatures = Object.keys(checkedFeatures).filter((feature) => checkedFeatures[feature]);
const isFiltering = name.trim() !== '' || activeFeatures.length > 0;
// 入力からデバウンスして検索する。マウント時も空クエリで初期一覧を取得する
useEffect(() => {
let isCancelled = false;
const timerId = setTimeout(async () => {
setIsLoading(true);
setError('');
try {
const query = new URLSearchParams({ name: name });
activeFeatures.forEach((feature) => query.set(`feature_${feature}`, 'true'));
const response = await fetch(`${apiUrl}?${query}`);
if (isCancelled) return;
if (response.ok) {
const data = await response.json();
setShichosons(data.shichosons);
setFeatureLabels(data.feature_labels);
// 絞り込み中は該当地域ブロックをすべて開き、絞り込み解除で閉じる
const allOpen = {};
if (isFiltering) data.shichosons.forEach((s) => { allOpen[s.region] = true; });
setOpenRegions(allOpen);
} else {
setError('検索に失敗しました');
}
} catch (e) {
if (isCancelled) return;
console.error('Error:', e);
setError('ネットワークエラーが発生しました');
} finally {
if (!isCancelled) setIsLoading(false);
}
}, SEARCH_DEBOUNCE_MS);
// 入力が続いた場合は前回のリクエスト結果を破棄する
return () => {
isCancelled = true;
clearTimeout(timerId);
};
}, [name, JSON.stringify(activeFeatures), apiUrl]);
const toggleFeature = (feature) => {
setCheckedFeatures((prev) => ({ ...prev, [feature]: !prev[feature] }));
};
const toggleRegion = (region) => {
setOpenRegions((prev) => ({ ...prev, [region]: !prev[region] }));
};
// 比較モードのON/OFF。OFFにしたら選択もリセットする
const toggleCompareMode = () => {
setCompareMode((prev) => !prev);
setCompareFirstId(null);
};
// 1つめのまちの選択/解除(同じカードをもう一度押すと解除)
const onToggleFirst = (id) => {
setCompareFirstId((prev) => (prev === id ? null : id));
};
const groups = groupByRegion(shichosons);
const compareFirstShichoson = shichosons.find((shichoson) => shichoson.id === compareFirstId);
return (
{compareMode && (
{compareFirstId
? `「${compareFirstShichoson ? compareFirstShichoson.name : '選択中のまち'}」と比較するまちの「このまちと比較」を押してください。`
: 'まず1つめのまちをカードのチェックボックスで選んでください。'}
)}
{error && (
{error}
)}
{!error && !isLoading && shichosons.length === 0 ? (
条件に合う自治体が見つかりませんでした。検索条件を変えてお試しください。
) : (
{groups.map((group) => (
toggleRegion(group.region)}
compareMode={compareMode}
compareFirstId={compareFirstId}
onToggleFirst={onToggleFirst}
/>
))}
)}
);
};
function initializeApp() {
const container = document.getElementById('shichosons-root');
if (container) {
try {
const { apiUrl } = container.dataset;
const root = createRoot(container);
root.render();
} catch (error) {
console.error('Error rendering ShichosonIndex component:', error);
}
} else {
console.error('shichosons-root element not found!');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
}