feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { MapData } from './types';
|
||||
|
||||
// Leaflet CDN を動的にロードする
|
||||
let leafletLoaded = false;
|
||||
let leafletLoadPromise: Promise<void> | null = null;
|
||||
|
||||
function loadLeaflet(): Promise<void> {
|
||||
if (leafletLoaded) return Promise.resolve();
|
||||
if (leafletLoadPromise) return leafletLoadPromise;
|
||||
|
||||
leafletLoadPromise = new Promise<void>((resolve, reject) => {
|
||||
// CSS
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://unpkg.com/[email protected]/dist/leaflet.css';
|
||||
document.head.appendChild(link);
|
||||
|
||||
// JS
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://unpkg.com/[email protected]/dist/leaflet.js';
|
||||
script.onload = () => {
|
||||
leafletLoaded = true;
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => reject(new Error('Failed to load Leaflet'));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
return leafletLoadPromise;
|
||||
}
|
||||
|
||||
declare const L: typeof import('leaflet');
|
||||
|
||||
export function MapPlacesDetail({ data }: { data: MapData }) {
|
||||
const { query, places } = data;
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const mapInstanceRef = useRef<import('leaflet').Map | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapRef.current || places.length === 0) return;
|
||||
let cancelled = false;
|
||||
|
||||
loadLeaflet().then(() => {
|
||||
if (cancelled || !mapRef.current) return;
|
||||
|
||||
// 既存のマップがあれば破棄
|
||||
if (mapInstanceRef.current) {
|
||||
mapInstanceRef.current.remove();
|
||||
}
|
||||
|
||||
const center = { lat: places[0]!.lat, lon: places[0]!.lon };
|
||||
const map = L.map(mapRef.current).setView([center.lat, center.lon], 13);
|
||||
mapInstanceRef.current = map;
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19,
|
||||
}).addTo(map);
|
||||
|
||||
const markers = places.map((p) =>
|
||||
L.marker([p.lat, p.lon])
|
||||
.addTo(map)
|
||||
.bindPopup(`<b>${p.name}</b><br>${p.address}`),
|
||||
);
|
||||
|
||||
if (markers.length > 1) {
|
||||
const group = L.featureGroup(markers);
|
||||
map.fitBounds(group.getBounds().pad(0.15));
|
||||
}
|
||||
|
||||
// モーダルが開いた直後はサイズが確定していない場合がある
|
||||
setTimeout(() => map.invalidateSize(), 100);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (mapInstanceRef.current) {
|
||||
mapInstanceRef.current.remove();
|
||||
mapInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [places]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-bold text-slate-800 mb-4">
|
||||
📍 地図検索結果: 「{query}」
|
||||
</h2>
|
||||
|
||||
{/* Leaflet map */}
|
||||
<div
|
||||
ref={mapRef}
|
||||
style={{ height: 400 }}
|
||||
className="w-full rounded-xl overflow-hidden border border-slate-200 mb-6"
|
||||
/>
|
||||
|
||||
{/* Place list */}
|
||||
<div className="space-y-4">
|
||||
{places.map((p, i) => (
|
||||
<div key={`${p.lat}-${p.lon}`} className="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<div className="text-slate-400 mb-1" style={{ fontSize: 13 }}>#{i + 1}</div>
|
||||
<h3 className="text-sm font-semibold text-slate-800 leading-snug mb-2">{p.name}</h3>
|
||||
|
||||
<div className="text-xs text-slate-600 space-y-1 mb-3">
|
||||
<div>📍 {p.address}</div>
|
||||
<div>📌 {p.lat.toFixed(6)}, {p.lon.toFixed(6)}</div>
|
||||
{p.type && <div>🏷 {p.type}</div>}
|
||||
{p.details && <div>💬 {p.details}</div>}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={p.mapUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-semibold rounded-lg no-underline transition-colors"
|
||||
>
|
||||
OpenStreetMap で開く
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user