Skip to content

Commit e4036c7

Browse files
rtcode337claude
andauthored
訪問予定リストの経路表示中、現在地からリスト先頭のスポットまで線で結ぶ (#50)
* 訪問予定リストの経路表示中、現在地からリスト先頭のスポットまで線で結ぶ 現在地(GeolocateControlの青丸)を表示しているときは、訪問予定リストの 紫の経路の先頭に現在地を繋ぎ、現在地→最初のスポットの線・矢印を描く。 - buildRouteGeoJSONのextraPathsに始点`start`([lng, lat])を追加し、 経路のスポットが1件以上あるときだけ先頭に繋ぐ - 現在地はgeolocateイベントでcurrentLocation stateに保持 (約1m未満の移動は再レンダー抑制のため無視) - trackuserlocationendはBACKGROUND(青丸は残る)でも発火するため、 青丸のDOM要素の有無でOFFを見分けて現在地を忘れる Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KqNK9cR5KJRyuKHcLcaQG * 訪問予定リスト作成パネルをスクロール可能にし、シリーズ表示を色玉に変更 - 並び替えを「行のどこでも長押し→ドラッグ」から左端の三本線ハンドルに変更。 全行にtouch-action: noneが当たっていて一覧をタッチスクロールできず、 画面外のスポットを編集する術がなかったため、ハンドルにだけ当てて 行本体は通常のスクロールに任せる(長押し判定も不要になり削除) - シリーズ名のバッジをやめ、シリーズの色+縁取りの色玉で示す (シリーズ名が長いとスポット名の幅を食って何行にも折り返すため。 非公開スポットの破線縁取り・シリーズ未設定=マイスポット扱いは バッジと同じ規則で、名前はtitle属性で確認できる) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KqNK9cR5KJRyuKHcLcaQG * 訪問状況の既定を未訪問のみにし、リセットをセクションごとに分割 - 訪問状況の絞り込みは「すべて」チップを廃止し、既定を「未訪問」のみにする (両方選択=全件表示。空選択は「何も選んでいない見た目で全件表示」に なって分かりにくいため最後の1つは外せない)。hasActiveFiltersも 「空でない」から「既定と違う」の判定に変更。旧保存データの空配列 (「すべて」だった頃の値)は読み込み時に既定へ倒す - 絞り込みモーダル見出しのリセットは絞り込み(シリーズ・カテゴリ・ 訪問状況)だけを既定に戻すようにし(FilterResetButtonに一本化)、 訪問日・訪問予定リスト・別の種別を重ねて表示の各セクションに 個別のリセットボタン(SectionResetButton)を追加。それぞれ 今日/表示しない/重ねないへ戻し、訪問日・訪問予定リストの リセットはそのセクションの「これだけを表示」も解除する Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KqNK9cR5KJRyuKHcLcaQG --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b57df60 commit e4036c7

4 files changed

Lines changed: 284 additions & 171 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ GitHub Actions(`.github/workflows/docker-publish.yml`)がビルド時に`<JST日
109109
2. `/[type]/spots`の地域タブの名称(`regionFieldLabel`: 都道府県/州・県/国)と並び順(`compareRegions`: `'jp'`はJIS順・リスト外の値も末尾に表示、他は五十音順)
110110
3. 地名検索`/api/geocode``countrycodes`(`scope`クエリパラメータで渡す。`'world'`は絞り込みなし)
111111
4. 逆ジオ`/api/geocode/reverse`の地域解決(`'jp'`=ISO3166-2コード→都道府県、国指定=state/province/county、`'world'`=国名。レスポンスのキーは`region`)
112-
5. `/[type]/map`初回表示(`'jp'`=従来どおり現在地取得、他=登録スポット全体へfitBounds・スポット0件時は世界全体表示)。地図の表示位置の記憶(`MapView`の`lastViews`。メモリ上のみ)と絞り込み条件の記憶(同`loadSavedFilters`/`saveFilters`。localStorage保存のためアプリを完全に落としても残る。絞り込み中(シリーズ・カテゴリ・訪問状況のいずれか。`hasActiveFilters`)は地図の絞り込みボタンが青くなる。「リセット」ボタンは絞り込みモーダルの見出し行の✕の隣にあり、**地図では絞り込みに加えて訪問日を今日に・別の種別を重ねて表示を「重ねない」に戻す**(`MapView`が独自に描画。押せる=青くなる条件も`hasActiveFilters` ‖ 訪問日が今日でない ‖ 重ね表示が選択中、のいずれか。`showRoutes`は表示切り替えのため対象外で現在値を維持)。スポット一覧側は`FilterBar.tsx`の`FilterResetButton`(`DEFAULT_FILTERS`に戻すだけ。`showReset` propで`FilterBar`先頭に内蔵表示)を使う)も種別ごとに分けている。現在地追跡モード(GeolocateControlのカメラ追従)中に他画面へ遷移して戻った場合は、位置の復元に加えて追跡モード自体を再開する(`MapView`の`lastTrackingActive`)。なお、PWA(スタンドアロン起動)でアプリを切り替えて戻った場合はページが再読み込みされないため初回表示の現在地取得は走らず、意図的に「開いていた位置をそのまま表示」の挙動にしている(バックグラウンド中に調べ物などで見ていた位置を失わないため)
112+
5. `/[type]/map`初回表示(`'jp'`=従来どおり現在地取得、他=登録スポット全体へfitBounds・スポット0件時は世界全体表示)。地図の表示位置の記憶(`MapView`の`lastViews`。メモリ上のみ)と絞り込み条件の記憶(同`loadSavedFilters`/`saveFilters`。localStorage保存のためアプリを完全に落としても残る。絞り込み中(シリーズ・カテゴリ・訪問状況が既定と違うとき。`hasActiveFilters`。**訪問状況の既定は「未訪問」のみで「すべて」チップは無い** — 両方選択=全件表示、空選択はUIから作れず、旧保存データの空配列=「すべて」は読み込み時に既定へ倒す)は地図の絞り込みボタンが青くなる。「リセット」ボタンは絞り込みモーダルの見出し行の✕の隣にあり、**地図・スポット一覧とも`FilterBar.tsx`の`FilterResetButton`=絞り込み(シリーズ・カテゴリ・訪問状況)だけを既定に戻す**(`showRoutes`・訪問日・訪問予定リスト・「これだけを表示」・重ね表示は対象外で現在値を維持。スポット一覧は`showReset` propで`FilterBar`先頭に内蔵表示)。地図の訪問日・訪問予定リスト・別の種別を重ねて表示の各セクションの見出し行には個別のリセットボタン(`MapView`の`SectionResetButton`)があり、それぞれ今日/表示しない/重ねないへ戻す(訪問日・訪問予定リストのリセットは、そのセクションの「これだけを表示」も解除する))も種別ごとに分けている。現在地追跡モード(GeolocateControlのカメラ追従)中に他画面へ遷移して戻った場合は、位置の復元に加えて追跡モード自体を再開する(`MapView`の`lastTrackingActive`)。なお、PWA(スタンドアロン起動)でアプリを切り替えて戻った場合はページが再読み込みされないため初回表示の現在地取得は走らず、意図的に「開いていた位置をそのまま表示」の挙動にしている(バックグラウンド中に調べ物などで見ていた位置を失わないため)
113113

114114
あわせて`wikipedia_lang`キー(既定`'ja'``resolveWikipediaLang`)でスポット詳細のWikipedia検索(`SpotInfoModal`)の言語版サブドメインを種別ごとに切り替えられる。どちらも`/[type]/admin`「スポット種別の設定」の「対象地域とWikipedia言語」フォーム(admin専用)から変更し、PATCH `/api/spot-types/[id]`が値の妥当性を検証する。`PREFECTURES`(47都道府県ハードコード)を「地域の全集合」として使ってよいのは`'jp'`スコープの文脈だけ、という点に注意。
115115

@@ -136,7 +136,7 @@ GitHub Actions(`.github/workflows/docker-publish.yml`)がビルド時に`<JST日
136136

137137
**1スポットは複数のカテゴリを持てる**(`spots.categories``text[]`。かつては単数の`spots.category text`だった)。絞り込みはOR条件で、選択中のカテゴリのいずれかを持つスポットが通る(`FilterBar``passesFilters`)。CSV・訪問記録エクスポートでは`categories`という1列にパイプ区切り(`CATEGORY_SEPARATOR`)で書く — カンマだとCSVの区切りと衝突して値全体の引用が要るため(`parseCategoryList`/`formatCategoryList`)。CSVに`categories`列自体が無い場合は、`key`列と同じく既存スポットのカテゴリを変更しない(全消しを防ぐため)。PATCH `/api/spots/[id]`も同じ理由で、ボディに`categories`が含まれるときだけ更新する(「カテゴリなし」にするには空配列を明示的に送る)。
138138

139-
カテゴリの一覧(種別ごとに使える値の定義)も同じパターンでスポット種別ごとに持つ(`lib/category.ts`)。`spot_type_settings`の`categories`キー(`CATEGORIES_SETTING_KEY`)にJSON文字列(`string[]`。見た目は持たない)を保存し、行が無い・parse失敗時は`DEFAULT_CATEGORIES`(観光地の現行カテゴリ、旧`lib/types.ts`の`CATEGORIES`ハードコードの後継)にフォールバックする(`resolveCategories`)。明示的に空配列`"[]"`を保存した種別は「定義済みカテゴリなし」の扱い。配列の並び順がカテゴリの並び順(`getCategoryOrder`)で、地図・スポット一覧の絞り込みチップ(`components/FilterBar.tsx`の`SpotFilters.categories`。シリーズ・訪問状況と同じ複数選択+「すべて」チップ、選択肢は実データに存在する値から作る)と、スポット追加・編集フォーム(`AddSpotModal`)の選択チップ(複数選択のトグル。設定の一覧を先頭に、設定外の既存値を後ろに合成し、一覧に無い値は下の入力欄から足せる)がこの並びを使う。取得は`useCategories(typeKey)`フック(`useSeriesStyles`のカテゴリ版)。管理画面`/[type]/admin`「スポット種別の設定」のカテゴリ欄(カンマ・読点区切りで入力、admin専用)と、スポット種別JSON作成の`categories`フィールド(文字列配列)から設定でき、PATCH `/api/spot-types/[id]`が`parseCategories`で妥当性を検証する。`categories`列自体は従来どおり自由入力で、一覧に無い値も動く(並びは末尾)。
139+
カテゴリの一覧(種別ごとに使える値の定義)も同じパターンでスポット種別ごとに持つ(`lib/category.ts`)。`spot_type_settings`の`categories`キー(`CATEGORIES_SETTING_KEY`)にJSON文字列(`string[]`。見た目は持たない)を保存し、行が無い・parse失敗時は`DEFAULT_CATEGORIES`(観光地の現行カテゴリ、旧`lib/types.ts`の`CATEGORIES`ハードコードの後継)にフォールバックする(`resolveCategories`)。明示的に空配列`"[]"`を保存した種別は「定義済みカテゴリなし」の扱い。配列の並び順がカテゴリの並び順(`getCategoryOrder`)で、地図・スポット一覧の絞り込みチップ(`components/FilterBar.tsx`の`SpotFilters.categories`。シリーズと同じ複数選択+「すべて」チップ(訪問状況は「すべて」チップ無しで既定=未訪問のみ)、選択肢は実データに存在する値から作る)と、スポット追加・編集フォーム(`AddSpotModal`)の選択チップ(複数選択のトグル。設定の一覧を先頭に、設定外の既存値を後ろに合成し、一覧に無い値は下の入力欄から足せる)がこの並びを使う。取得は`useCategories(typeKey)`フック(`useSeriesStyles`のカテゴリ版)。管理画面`/[type]/admin`「スポット種別の設定」のカテゴリ欄(カンマ・読点区切りで入力、admin専用)と、スポット種別JSON作成の`categories`フィールド(文字列配列)から設定でき、PATCH `/api/spot-types/[id]`が`parseCategories`で妥当性を検証する。`categories`列自体は従来どおり自由入力で、一覧に無い値も動く(並びは末尾)。
140140

141141
### ルート(`spot_routes`/`spot_route_points`)とスポット参照キー(`spots.key`)
142142

@@ -156,7 +156,7 @@ GitHub Actions(`.github/workflows/docker-publish.yml`)がビルド時に`<JST日
156156

157157
訪問日が選ばれているとき、`MapView`は**その日の訪問記録を訪問時刻の昇順に矢印で結んだ「訪問順の経路」**を描く(`buildVisitPath`)。ルートCSVのルートと同じ`spot-routes`ソース・同じ線/矢印レイヤーに載せるだけなので描画コードは共用で、色だけ`VISIT_PATH_COLOR`(緑`#16a34a`=訪問済みピンの塗りと同じ)にしてルートと区別する。同じスポットへの再訪はそのまま複数回経由地として現れる(行って戻る線になる)が、連続する同じスポットへの訪問は長さ0の線分になり矢印の向きが定まらないためまとめる。他の種別の訪問記録・日時不明の訪問は除外する。**この経路上のスポット(`pathIds`)は、シリーズ・カテゴリ・訪問状況の絞り込みで外れていても必ずピンを表示する**(絞り込みではなくその日の訪問を辿るための表示のため全条件を免除。ルートCSVの経由地はシリーズ・カテゴリのみ免除で訪問状況は適用する点と対照的)。
158158

159-
同じ仕組みで、絞り込みモーダルの訪問日の下に**「訪問予定リスト」セレクト**があり、選んだリスト(旅程)のスポットを**リスト順に矢印(紫`PLAN_LIST_PATH_COLOR`=`#9333ea`)で結んだ経路**を描く(`filters.planListId``buildPlanListPath`)。訪問日の経路と同じ`spot-routes`ソース/レイヤーに色違いで重ねるだけで(`buildRouteGeoJSON`は色付き経路の配列`extraPaths`を受け取る)、選んだときに`handleSelectPlanList`が経路全体へ`fitBounds`し、経路上のスポットは`pathIds`で絞り込みから免除する点も訪問日と同じ。リストは`MapView``api.visitPlanLists.list`で読み(1件以上あるときだけセレクトを出す)、選択は`loadSavedFilters`/`saveFilters`で保存する(削除済み等で見つからないIDは描画側で無視)。地図のリセットは`planListId`も含めて`null`に戻す。
159+
同じ仕組みで、絞り込みモーダルの訪問日の下に**「訪問予定リスト」セレクト**があり、選んだリスト(旅程)のスポットを**リスト順に矢印(紫`PLAN_LIST_PATH_COLOR`=`#9333ea`)で結んだ経路**を描く(`filters.planListId`・`buildPlanListPath`)。訪問日の経路と同じ`spot-routes`ソース/レイヤーに色違いで重ねるだけで(`buildRouteGeoJSON`は色付き経路の配列`extraPaths`を受け取る)、選んだときに`handleSelectPlanList`が経路全体へ`fitBounds`し、経路上のスポットは`pathIds`で絞り込みから免除する点も訪問日と同じ。**現在地(GeolocateControlの青丸)を表示中は、現在地からリスト先頭のスポットまでも同じ紫の線・矢印で結ぶ**(`buildRouteGeoJSON`の`start`に`currentLocation` stateを渡して経路の先頭に繋ぐ。現在地は`geolocate`イベントで更新し(約1m未満の移動は再レンダー抑制のため無視)、青丸ごと消えるOFFへの遷移で忘れる — `trackuserlocationend`はBACKGROUND=青丸が残る遷移でも発火するため、青丸のDOM要素の有無でOFFを見分ける)。リストは`MapView`が`api.visitPlanLists.list`で読み(1件以上あるときだけセレクトを出す)、選択は`loadSavedFilters`/`saveFilters`で保存する(削除済み等で見つからないIDは描画側で無視)。訪問予定リストのセクションのリセットボタンが`planListId``null`に戻す(見出し行のリセットは絞り込みのみで触らない)
160160

161161
### 別スポット種別の重ね表示(地図)
162162

components/FilterBar.tsx

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ export interface SpotFilters {
1818
series: Series[];
1919
/** 空配列 = カテゴリによる絞り込みなし(「すべて」選択中、全件表示) */
2020
categories: Category[];
21-
/** 空配列 = 訪問状況による絞り込みなし(「すべて」選択中、全件表示) */
21+
/**
22+
* 訪問状況の絞り込み。既定は`["unvisited"]`(未訪問のみ)で、「すべて」チップは
23+
* 無い(両方選択=全件表示)。UI上は空選択を作れない(見た目が「何も表示しない」に
24+
* 見えるのに全件表示になるため)が、判定(`passesFilters`)は旧保存データ互換の
25+
* ため空配列=絞り込みなし(全件)としても動く
26+
*/
2227
visited: VisitedValue[];
2328
/**
2429
* 訪問順の経路を描く対象日(`YYYY-MM-DD`のローカル日付)。絞り込みではなく、
@@ -51,13 +56,18 @@ export interface SpotFilters {
5156
export const DEFAULT_FILTERS: SpotFilters = {
5257
series: [],
5358
categories: [],
54-
visited: [],
59+
visited: ["unvisited"],
5560
visitedDate: null,
5661
planListId: null,
5762
showRoutes: true,
5863
isolate: null,
5964
};
6065

66+
/** 訪問状況が既定(未訪問のみ)のままか(絞り込み中とみなさない条件) */
67+
export function isDefaultVisited(visited: VisitedValue[]): boolean {
68+
return visited.length === 1 && visited[0] === "unvisited";
69+
}
70+
6171
/**
6272
* 訪問日時(`visits.visited_on`のISO文字列)を絞り込み・選択肢のキーに使う
6373
* ローカル日付(`YYYY-MM-DD`)にする。日時不明(null)・不正値は`null`。
@@ -107,22 +117,27 @@ export function passesFilters(
107117
}
108118

109119
/**
110-
* 何らかの絞り込みが掛かっているか(リセットボタンと地図の絞り込みボタンの
111-
* 見た目の条件)。`showRoutes`は表示の切り替え、`visitedDate`は訪問順の経路の
112-
* 対象日(絞り込みではない)であって、どちらも絞り込みではないため含めない。
120+
* 何らかの絞り込みが掛かっているか=既定と違う絞り込み条件か(リセットボタンと
121+
* 地図の絞り込みボタンの見た目の条件)。訪問状況は既定が「未訪問のみ」のため、
122+
* 空ではなく既定と違うかどうかで見る。`showRoutes`は表示の切り替え、
123+
* `visitedDate`は訪問順の経路の対象日(絞り込みではない)であって、
124+
* どちらも絞り込みではないため含めない。
113125
*/
114126
export function hasActiveFilters(filters: SpotFilters): boolean {
115127
return (
116128
filters.series.length > 0 ||
117129
filters.categories.length > 0 ||
118-
filters.visited.length > 0
130+
!isDefaultVisited(filters.visited)
119131
);
120132
}
121133

122134
/**
123-
* 全条件を「すべて」に戻すボタン(条件を1つずつ戻す手間を省く)。常に出しておき、
124-
* 戻す対象があるとき(`hasActiveFilters`)だけチップと同じ青にして知らせる
135+
* 絞り込み(シリーズ・カテゴリ・訪問状況)だけを既定に戻すボタン
136+
* (条件を1つずつ戻す手間を省く)。常に出しておき、戻す対象があるとき
137+
* (`hasActiveFilters`)だけチップと同じ青にして知らせる
125138
* (出し入れするとボタンの位置が動くため)。
139+
* 絞り込みではないもの(`showRoutes`・訪問日・訪問予定リスト・「これだけを表示」)は
140+
* 対象外で現在値を維持する(地図ではそれぞれのセクションの個別リセットで戻す)。
126141
* 置き場所が呼び出し側で異なる(地図は絞り込みモーダルの見出し行、
127142
* スポット一覧は`FilterBar`の先頭)ため、`FilterBar`から切り出してある
128143
*/
@@ -138,8 +153,14 @@ export function FilterResetButton({
138153
<button
139154
type="button"
140155
disabled={!active}
141-
// showRoutesは絞り込みではないためリセットの対象外(現在の値を維持する)
142-
onClick={() => onChange({ ...DEFAULT_FILTERS, showRoutes: filters.showRoutes })}
156+
onClick={() =>
157+
onChange({
158+
...filters,
159+
series: [],
160+
categories: [],
161+
visited: [...DEFAULT_FILTERS.visited],
162+
})
163+
}
143164
className={`rounded-full border px-3 py-1 text-xs font-medium ${
144165
active ? ALL_CHIP_ACTIVE_CLASS : "border-gray-300 bg-white text-gray-400"
145166
}`}
@@ -298,27 +319,22 @@ export default function FilterBar({
298319
<span className="mb-1 block text-xs font-medium text-gray-500">
299320
訪問状況
300321
</span>
322+
{/* 「すべて」チップは無し(両方選択=全件表示)。空選択は「何も選んでいない
323+
見た目なのに全件表示」になって分かりにくいため、最後の1つは外せない */}
301324
<div className="flex flex-wrap gap-1.5">
302325
{VISITED_OPTIONS.map((opt) => (
303326
<Chip
304327
key={opt.value}
305328
label={opt.label}
306329
active={filters.visited.includes(opt.value)}
307330
activeClassName={ALL_CHIP_ACTIVE_CLASS}
308-
onClick={() =>
309-
onChange({
310-
...filters,
311-
visited: toggleSelection(filters.visited, opt.value),
312-
})
313-
}
331+
onClick={() => {
332+
const visited = toggleSelection(filters.visited, opt.value);
333+
if (visited.length === 0) return;
334+
onChange({ ...filters, visited });
335+
}}
314336
/>
315337
))}
316-
<Chip
317-
label="すべて"
318-
active={filters.visited.length === 0}
319-
activeClassName={ALL_CHIP_ACTIVE_CLASS}
320-
onClick={() => onChange({ ...filters, visited: [] })}
321-
/>
322338
</div>
323339
</div>
324340

0 commit comments

Comments
 (0)