diff --git a/app/assets/images/onsen.svg b/app/assets/images/onsen.svg
new file mode 100644
index 0000000..4e68ede
--- /dev/null
+++ b/app/assets/images/onsen.svg
@@ -0,0 +1,75 @@
+
+
diff --git a/app/javascript/controllers/form_map_controller.js b/app/javascript/controllers/form_map_controller.js
new file mode 100644
index 0000000..361dfc3
--- /dev/null
+++ b/app/javascript/controllers/form_map_controller.js
@@ -0,0 +1,90 @@
+import { Controller } from "@hotwired/stimulus"
+import L from "leaflet"
+
+// Connects to data-controller="form-map"
+export default class extends Controller {
+ static targets = ["container", "latitudeInput", "longitudeInput"]
+
+ connect() {
+ this.currentMarker = null;
+
+ // 地図の初期化
+ this.map = L.map(this.containerTarget).setView([35.468, 133.0483], 11.5);
+ L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 19,
+ attribution: '© OpenStreetMap'
+ }).addTo(this.map);
+
+ // フォームに初期値があればピンを立てる
+ if (this.hasLatitudeInputTarget && this.hasLongitudeInputTarget && this.latitudeInputTarget.value && this.longitudeInputTarget.value) {
+ this.updateMarkerAndInputs(this.latitudeInputTarget.value, this.longitudeInputTarget.value);
+ }
+
+ // マップクリック時のイベントリスナー
+ this.map.on("click", (e) => {
+ const { lat, lng } = e.latlng;
+ this.updateMarkerAndInputs(lat, lng);
+ });
+ }
+
+ // マーカーを更新し、フォームの値を自動入力
+ updateMarkerAndInputs(lat, lng) {
+ this.removeMarker(); // 既存のピンを削除
+
+ const latlng = [lat, lng];
+ this.currentMarker = L.marker(latlng).addTo(this.map)
+ .bindPopup(`緯度: ${lat.toFixed(5)}
経度: ${lng.toFixed(5)}`)
+ .openPopup();
+
+ this.map.setView(latlng, this.map.getZoom());
+ this.latitudeInputTarget.value = lat.toFixed(5);
+ this.longitudeInputTarget.value = lng.toFixed(5);
+ }
+
+ // 既存のピンを削除する
+ removeMarker() {
+ if (this.currentMarker) {
+ this.map.removeLayer(this.currentMarker);
+ }
+ }
+
+ // フォームの入力値からマーカーを更新
+ updateMarkerFromInput() {
+ const lat = parseFloat(this.latitudeInputTarget.value);
+ const lng = parseFloat(this.longitudeInputTarget.value);
+
+ // 有効な数値であることを確認
+ if (!isNaN(lat) && !isNaN(lng)) {
+ this.updateMarkerAndInputs(lat, lng);
+ }
+ }
+
+ /**
+ * 現在地を取得し、地図とフォームを更新する
+ */
+ locate() {
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ const lat = position.coords.latitude;
+ const lng = position.coords.longitude;
+ this.updateMarkerAndInputs(lat, lng);
+ },
+ (error) => {
+ console.error("現在地の取得に失敗しました: ", error);
+ alert("現在地の取得に失敗しました。ブラウザの設定をご確認ください。");
+ }
+ );
+ } else {
+ alert("お使いのブラウザは現在地の取得に対応していません。");
+ }
+ }
+
+ // 切断時の処理
+ disconnect() {
+ if (this.map) {
+ this.map.remove();
+ this.map = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/controllers/index_map_controller.js b/app/javascript/controllers/index_map_controller.js
new file mode 100644
index 0000000..e425f71
--- /dev/null
+++ b/app/javascript/controllers/index_map_controller.js
@@ -0,0 +1,99 @@
+import { Controller } from "@hotwired/stimulus"
+import L from "leaflet"
+
+// Connects to data-controller="index-map"
+export default class extends Controller {
+ static targets = ["container"]
+ static values = { photoSpots: Array }
+
+ connect() {
+ console.log(this.photoSpotsValue)
+
+ // 地図の初期化
+ this.map = L.map(this.containerTarget).setView([35.474, 133.050], 13)
+ L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 19,
+ attribution: '© OpenStreetMap'
+ }).addTo(this.map)
+
+ // フォトスポットのマーカー
+ this.photoSpotsValue.forEach(photo_spot => {
+ if (photo_spot.latitude && photo_spot.longitude) {
+ const marker = L.marker([photo_spot.latitude, photo_spot.longitude])
+ .addTo(this.map)
+ .bindPopup(photo_spot.name)
+
+ // マーカークリック時に対応カードをハイライト
+ marker.on("click", () => {
+ this.highlight(null, photo_spot.id)
+ })
+ }
+ })
+
+ // イベントリスナーを登録
+ this.map.on("locationfound", this._onLocationFound.bind(this))
+ this.map.on("locationerror", this._onLocationError.bind(this))
+
+ // 初回ロード時にも現在地を取得
+ this.locate()
+ }
+
+ locate() {
+ if (navigator.geolocation) {
+ this.map.locate({ setView: true, maxZoom: 16 })
+ } else {
+ alert("お使いのブラウザは現在地の取得に対応していません。")
+ }
+ }
+
+ _onLocationFound(e) {
+ const redIcon = new L.Icon({
+ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
+ shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+ popupAnchor: [1, -34],
+ shadowSize: [41, 41]
+ })
+
+ L.marker(e.latlng, { icon: redIcon })
+ .addTo(this.map)
+ .bindPopup("現在地")
+ .openPopup()
+ }
+
+ _onLocationError(e) {
+ alert("現在地を取得できませんでした: " + e.message)
+ }
+
+ highlight(event, spotId = null) {
+ // イベントから spotId を取得
+ const targetSpotId = spotId || event?.currentTarget.dataset.spotId
+ console.log("ハイライト対象ID:", targetSpotId)
+
+ // 既存ハイライトを削除
+ document.querySelectorAll("[data-spot-id]").forEach(el => {
+ el.classList.remove("ring-2", "ring-blue-500", "bg-blue-50", "animate-pulse")
+ })
+
+ // 対象カードを取得
+ const card = document.querySelector(`[data-spot-id='${targetSpotId}']`)
+ console.log("見つかったカード:", card)
+
+ if (card) {
+ card.classList.add("ring-2", "ring-blue-500", "bg-blue-50", "animate-pulse")
+ card.scrollIntoView({ behavior: "smooth", block: "center" })
+ setTimeout(() => card.classList.remove("animate-pulse"), 2000)
+ }
+ }
+
+
+ disconnect() {
+ if (this.map) {
+ this.map.off("locationfound")
+ this.map.off("locationerror")
+ this.map.remove()
+ this.map = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/controllers/map_controller.js b/app/javascript/controllers/map_controller.js
deleted file mode 100644
index 02dacd4..0000000
--- a/app/javascript/controllers/map_controller.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { Controller } from "@hotwired/stimulus"
-import L from "leaflet"
-
-// Connects to data-controller="map"
-export default class extends Controller {
- // ターゲット名を変更しました
- static targets = ["container", "latitudeInput", "longitudeInput"]
- static values = { photo_spots: Array }
-
- connect() {
- this.currentMarker = null;
- this.photo_spots = this._parsePhotoSpotsData();
-
- // 地図の初期化
- this.map = L.map(this.containerTarget).setView([35.468, 133.0483], 11.5);
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
- maxZoom: 19,
- attribution: '© OpenStreetMap'
- }).addTo(this.map);
-
- // 既存のスポットのマーカーを設置
- const photo_spotIcon = L.icon({
- iconUrl: '/photo_spot.svg',
- iconSize: [32, 32],
- iconAnchor: [16, 32],
- });
- this.photo_spots.forEach(photo_spot => {
- // プロパティ名を変更しました
- L.marker([photo_spot.latitude, photo_spot.longitude], { icon: photo_spotIcon })
- .addTo(this.map)
- .bindPopup(photo_spot.name);
- });
-
- // フォームに初期値があればピンを立てる
- if (this.latitudeInputTarget.value && this.longitudeInputTarget.value) {
- this.updateMarker(this.latitudeInputTarget.value, this.longitudeInputTarget.value);
- }
-
- // マップクリック時のイベントリスナー
- this.map.on("click", (e) => {
- const { lat, lng } = e.latlng;
- this.updateMarker(lat, lng);
- // テキストボックスの値を更新
- this.latitudeInputTarget.value = lat.toFixed(5);
- this.longitudeInputTarget.value = lng.toFixed(5);
- });
- }
-
- // マーカーを更新し、マップの中心を移動する
- updateMarker(lat, lng) {
- if (this.currentMarker) {
- this.map.removeLayer(this.currentMarker);
- }
- const latlng = [lat, lng];
- this.currentMarker = L.marker(latlng).addTo(this.map);
- this.map.setView(latlng, this.map.getZoom());
- }
-
- // フォームの入力値からマーカーを更新する
- updateMarkerFromInput() {
- const lat = parseFloat(this.latitudeInputTarget.value);
- const lng = parseFloat(this.longitudeInputTarget.value);
-
- if (!isNaN(lat) && !isNaN(lng)) {
- this.updateMarker(lat, lng);
- }
- }
-
- // 現在地を取得するメソッドを追加
- locate() {
- if (navigator.geolocation) {
- navigator.geolocation.getCurrentPosition(
- (position) => {
- const lat = position.coords.latitude;
- const lng = position.coords.longitude;
- this.updateMarker(lat, lng);
- this.latitudeInputTarget.value = lat.toFixed(5);
- this.longitudeInputTarget.value = lng.toFixed(5);
- },
- (error) => {
- console.error("現在地の取得に失敗しました: ", error);
- alert("現在地の取得に失敗しました。ブラウザの設定をご確認ください。");
- }
- );
- } else {
- alert("お使いのブラウザは現在地の取得に対応していません。");
- }
- }
-
- // === プライベートメソッド(内部処理用) ===
-
- _parsePhotoSpotsData() {
- try {
- const rawData = this.element.dataset.mapPhotoSpots || "[]";
- // パース後のデータが{id:..., name:..., geo_lat:..., geo_lng:...}のような構造であれば
- // 必要に応じてキーをリマップする必要があります
- return JSON.parse(rawData);
- } catch (error) {
- console.warn("写真スポットデータのパースに失敗:", error);
- return [];
- }
- }
-}
\ No newline at end of file
diff --git a/app/views/admin/photo_spots/_form.html.erb b/app/views/admin/photo_spots/_form.html.erb
index 741ca3c..ef80bfd 100644
--- a/app/views/admin/photo_spots/_form.html.erb
+++ b/app/views/admin/photo_spots/_form.html.erb
@@ -20,69 +20,71 @@