feat(site-finder): deep-link tabs via ?tab=env|land|market URL param

Tab state now syncs with URL — можно поделиться ссылкой на конкретный
таб (например /site-finder?tab=market для тренда цен по последнему cad).

- useRouter + useSearchParams (Next 15 app router, client-component)
- router.replace (не push) чтобы tab switches не засоряли history
- useEffect on searchParams → back/forward работают
- overview = default, не пишется в URL (clean)
- Type-guard isTabId(): только valid values из URL переключают state
This commit is contained in:
lekss361 2026-05-12 00:22:02 +03:00
parent aadf47709b
commit c47e56b931

View file

@ -1,8 +1,9 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import type { FeatureCollection } from "geojson"; import type { FeatureCollection } from "geojson";
import { CadInput } from "@/components/site-finder/CadInput"; import { CadInput } from "@/components/site-finder/CadInput";
@ -47,6 +48,12 @@ const TABS: Array<{ id: TabId; label: string }> = [
{ id: "market", label: "Рынок" }, { id: "market", label: "Рынок" },
]; ];
const TAB_IDS = TABS.map((t) => t.id);
function isTabId(v: string | null | undefined): v is TabId {
return !!v && (TAB_IDS as readonly string[]).includes(v);
}
// ── KPI helpers ─────────────────────────────────────────────────────────────── // ── KPI helpers ───────────────────────────────────────────────────────────────
type KpiColor = "neutral" | "amber" | "green" | "red" | "blue"; type KpiColor = "neutral" | "amber" | "green" | "red" | "blue";
@ -74,11 +81,39 @@ function scoreKpiColor(score: number, max: number): KpiColor {
// ── Page ────────────────────────────────────────────────────────────────────── // ── Page ──────────────────────────────────────────────────────────────────────
export default function SiteFinderPage() { export default function SiteFinderPage() {
const router = useRouter();
const searchParams = useSearchParams();
const initialTab: TabId = (() => {
const t = searchParams.get("tab");
return isTabId(t) ? t : "overview";
})();
const { mutate, data, isPending, error, isIdle } = useSiteAnalysis(); const { mutate, data, isPending, error, isIdle } = useSiteAnalysis();
const [isochrones, setIsochrones] = useState<FeatureCollection | undefined>( const [isochrones, setIsochrones] = useState<FeatureCollection | undefined>(
undefined, undefined,
); );
const [tab, setTab] = useState<TabId>("overview"); const [tab, setTabState] = useState<TabId>(initialTab);
// Sync tab → URL query (?tab=env). overview = default, не пишем в URL.
function setTab(next: TabId) {
setTabState(next);
const params = new URLSearchParams(searchParams.toString());
if (next === "overview") {
params.delete("tab");
} else {
params.set("tab", next);
}
const qs = params.toString();
router.replace(qs ? `?${qs}` : "?", { scroll: false });
}
// Listen to back/forward navigation — reflect URL tab in state
useEffect(() => {
const urlTab = searchParams.get("tab");
const validTab: TabId = isTabId(urlTab) ? urlTab : "overview";
if (validTab !== tab) setTabState(validTab);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]);
function handleAnalyze(cadNum: string) { function handleAnalyze(cadNum: string) {
setIsochrones(undefined); setIsochrones(undefined);