|
3 | 3 | import { useEffect, useRef, useState } from "react"; |
4 | 4 | import { api } from "@/lib/api-client"; |
5 | 5 | import { formatPlanDateRange } from "@/lib/planListDraft"; |
| 6 | +import { |
| 7 | + buildSpotWeatherAskUrl, |
| 8 | + planWeatherDate, |
| 9 | + weatherLinkLabel, |
| 10 | +} from "@/lib/weather"; |
6 | 11 | import { useDragReorder, REORDER_HANDLE_CLASS } from "@/lib/useDragReorder"; |
7 | 12 | import { formatSpotMeta } from "@/lib/spotMeta"; |
8 | 13 | import type { Spot, VisitPlanList } from "@/lib/types"; |
9 | 14 | import type { SeriesStyleDefinition } from "@/lib/seriesStyle"; |
10 | 15 | import SpotBadge from "@/components/SpotBadge"; |
11 | 16 | import HelpTip from "@/components/HelpTip"; |
12 | 17 | import GoogleMapsRouteLink from "@/components/GoogleMapsRouteLink"; |
| 18 | +import { useCurrentSpotTypeKey } from "@/lib/useSpotTypeKey"; |
| 19 | +import { usePathname } from "next/navigation"; |
| 20 | + |
| 21 | +/** 天気アイコン(太陽)。外部の天気ページへ開くリンクの印 */ |
| 22 | +function SunIcon({ className }: { className?: string }) { |
| 23 | + return ( |
| 24 | + <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden="true"> |
| 25 | + <path d="M12 17a5 5 0 100-10 5 5 0 000 10z" /> |
| 26 | + <path d="M12 1.5a1 1 0 011 1V4a1 1 0 11-2 0V2.5a1 1 0 011-1zm0 17a1 1 0 011 1v1.5a1 1 0 11-2 0V19.5a1 1 0 011-1zM22.5 12a1 1 0 01-1 1H20a1 1 0 110-2h1.5a1 1 0 011 1zm-17 0a1 1 0 01-1 1H3a1 1 0 110-2h1.5a1 1 0 011 1zm13.6-6.6a1 1 0 010 1.42l-1.06 1.06a1 1 0 11-1.42-1.42l1.06-1.06a1 1 0 011.42 0zM7.38 16.62a1 1 0 010 1.41l-1.06 1.06a1 1 0 11-1.41-1.41l1.06-1.06a1 1 0 011.41 0zm11.24 2.47a1 1 0 01-1.42 0l-1.06-1.06a1 1 0 111.42-1.41l1.06 1.06a1 1 0 010 1.41zM7.38 7.38a1 1 0 01-1.41 0L4.91 6.32A1 1 0 016.32 4.9l1.06 1.06a1 1 0 010 1.42z" /> |
| 27 | + </svg> |
| 28 | + ); |
| 29 | +} |
13 | 30 |
|
14 | 31 | /** |
15 | 32 | * 訪問予定リスト(旅程)の詳細モーダル。タイトル・説明・訪問予定期間と、 |
@@ -43,6 +60,10 @@ export default function VisitPlanListDetailModal({ |
43 | 60 | }) { |
44 | 61 | const [list, setList] = useState<VisitPlanList | null>(null); |
45 | 62 | const [loading, setLoading] = useState(true); |
| 63 | + // 「地図で見る」の遷移先。地図そのものから開いたときは出さない(今いる画面へのリンクになるため) |
| 64 | + const typeKey = useCurrentSpotTypeKey(); |
| 65 | + const pathname = usePathname(); |
| 66 | + const onMapPage = pathname.endsWith("/map"); |
46 | 67 | const [deleting, setDeleting] = useState(false); |
47 | 68 | const [error, setError] = useState<string | null>(null); |
48 | 69 | // 呼び出し側の spotsById に無い(=別スポット種別を重ねて追加した)スポットを |
@@ -80,6 +101,8 @@ export default function VisitPlanListDetailModal({ |
80 | 101 | }, [list, spotsById]); |
81 | 102 |
|
82 | 103 | const visitedIds = new Set(list?.visited_spot_ids ?? []); |
| 104 | + // 天気を見る日(開始日→終了日→今日)。リストが読めるまでは今日として扱う |
| 105 | + const weatherDate = planWeatherDate(list ?? {}); |
83 | 106 |
|
84 | 107 | // 経由スポットの並び替え。ドラッグ中は手元の並びだけを入れ替え(画面がその場で |
85 | 108 | // 追従する)、指を離した時点で1回だけPATCHする —— 動かすたびに保存すると |
@@ -273,6 +296,23 @@ export default function VisitPlanListDetailModal({ |
273 | 296 | </HelpTip> |
274 | 297 | </div> |
275 | 298 | )} |
| 299 | + {/* そのスポットの、予定の日の天気。**天気サービスは日付を指定して |
| 300 | + 開けない**ので、日付を添えてAIに聞く形にしてある(lib/weather.ts)。 |
| 301 | + 日は開始日→終了日→今日の順。**行のボタンの中には置けない** |
| 302 | + (ボタンの入れ子になるため)ので、独立したリンクとして隣に並べる */} |
| 303 | + {spot && ( |
| 304 | + <a |
| 305 | + href={buildSpotWeatherAskUrl(spot, weatherDate)} |
| 306 | + target="_blank" |
| 307 | + rel="noopener noreferrer" |
| 308 | + title={weatherLinkLabel(spot.name, weatherDate)} |
| 309 | + aria-label={weatherLinkLabel(spot.name, weatherDate)} |
| 310 | + onClick={(e) => e.stopPropagation()} |
| 311 | + className="mr-1 shrink-0 rounded-full p-1.5 text-amber-500 hover:bg-amber-50" |
| 312 | + > |
| 313 | + <SunIcon className="size-5" /> |
| 314 | + </a> |
| 315 | + )} |
276 | 316 | {/* 訪問済みの付け外し。訪問記録を付ければ自動で付くが、ここでも直せる |
277 | 317 | (訪問済みは経路から外れるだけで、リストからは消えない) */} |
278 | 318 | <button |
@@ -311,6 +351,18 @@ export default function VisitPlanListDetailModal({ |
311 | 351 | </p> |
312 | 352 | )} |
313 | 353 |
|
| 354 | + {/* このリストだけを地図で見る。地図側は`?planList=`を受け取ると、そのリストを |
| 355 | + 経路の対象に選び「これだけを表示」にして全体が入るよう移動する |
| 356 | + (MapView)。地図から開いたときは出さない —— 今いる画面へのリンクになるため */} |
| 357 | + {!onMapPage && typeKey && ( |
| 358 | + <a |
| 359 | + href={`/${typeKey}/map?planList=${encodeURIComponent(list.id)}`} |
| 360 | + className="mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg border border-blue-600 py-2 text-sm font-medium text-blue-600" |
| 361 | + > |
| 362 | + 🗺️ このリストだけを地図で表示 |
| 363 | + </a> |
| 364 | + )} |
| 365 | + |
314 | 366 | {/* 残りのスポットをGoogle マップの経路検索で開く(途中のスポットは経由地、 |
315 | 367 | 最後のスポットは目的地になる)。読み込めていないスポットは飛ばし、 |
316 | 368 | 訪問済みも外す —— 地図の経路と同じで、これから回る先だけを繋ぐ */} |
|
0 commit comments