mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-07-29 04:42:23 +02:00
query-params: migrate search & map state to nuqs
Replace the hand-rolled useQueryParams hook for the search and map-position features with nuqs type-safe parsers. Adds the NuqsAdapter provider, rewrites useSearchQuery on useQueryStates, switches MapView lat/lng/zoom to nuqs, and fixes node-link URLs to emit exact=true/is_repeater=true so parseAsBoolean reads them. The generic useQueryParams hook stays for ChatBox. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+44
@@ -25,6 +25,7 @@
|
||||
"moment": "^2.30.1",
|
||||
"next": "15.3.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.8.9",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-d3-tree": "^3.6.6",
|
||||
@@ -1585,6 +1586,12 @@
|
||||
"integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -5767,6 +5774,43 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/nuqs": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.8.9.tgz",
|
||||
"integrity": "sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/franky47"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@remix-run/react": ">=2",
|
||||
"@tanstack/react-router": "^1",
|
||||
"next": ">=14.2.0",
|
||||
"react": ">=18.2.0 || ^19.0.0-0",
|
||||
"react-router": "^5 || ^6 || ^7",
|
||||
"react-router-dom": "^5 || ^6 || ^7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@remix-run/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@tanstack/react-router": {
|
||||
"optional": true
|
||||
},
|
||||
"next": {
|
||||
"optional": true
|
||||
},
|
||||
"react-router": {
|
||||
"optional": true
|
||||
},
|
||||
"react-router-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"moment": "^2.30.1",
|
||||
"next": "15.3.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.8.9",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-d3-tree": "^3.6.6",
|
||||
|
||||
@@ -8,7 +8,6 @@ import SearchResults from '@/components/SearchResults';
|
||||
import RegionSelect from '@/components/RegionSelect';
|
||||
import { LAST_SEEN_OPTIONS } from '@/components/ConfigContext';
|
||||
import { useState, Suspense, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
function SearchPageContent() {
|
||||
@@ -16,20 +15,14 @@ function SearchPageContent() {
|
||||
const { query, setQuery, setLimit, setExact, setIsRepeater } = useSearchQuery();
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
// Helper function to check if exact search is enabled
|
||||
const isExactEnabled = query.exact === true || (typeof query.exact === 'string' && (query.exact === 'true' || query.exact === ''));
|
||||
|
||||
// Helper function to check if is_repeater search is enabled
|
||||
const isRepeaterEnabled = query.is_repeater === true || (typeof query.is_repeater === 'string' && (query.is_repeater === 'true' || query.is_repeater === ''));
|
||||
|
||||
// Always use config values for region and lastSeen
|
||||
const searchParams = {
|
||||
query: query.q,
|
||||
region: config.selectedRegion,
|
||||
lastSeen: config.lastSeen,
|
||||
limit: query.limit || 50,
|
||||
exact: isExactEnabled,
|
||||
is_repeater: isRepeaterEnabled,
|
||||
exact: query.exact,
|
||||
is_repeater: query.is_repeater,
|
||||
};
|
||||
|
||||
const { data, isLoading, error } = useMeshcoreSearch({
|
||||
@@ -144,7 +137,7 @@ function SearchPageContent() {
|
||||
<input
|
||||
type="checkbox"
|
||||
id="exact-match"
|
||||
checked={isExactEnabled}
|
||||
checked={query.exact}
|
||||
onChange={(e) => setExact(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
@@ -163,7 +156,7 @@ function SearchPageContent() {
|
||||
<input
|
||||
type="checkbox"
|
||||
id="is-repeater"
|
||||
checked={isRepeaterEnabled}
|
||||
checked={query.is_repeater}
|
||||
onChange={(e) => setIsRepeater(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ConfigProvider } from "@/components/ConfigContext";
|
||||
import { QueryProvider } from "@/components/QueryProvider";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -42,11 +43,13 @@ export default function RootLayout({
|
||||
style={{ '--header-height': '64px' } as React.CSSProperties}
|
||||
>
|
||||
<div className="flex flex-col min-h-screen w-full">
|
||||
<QueryProvider>
|
||||
<ConfigProvider>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
</QueryProvider>
|
||||
<NuqsAdapter>
|
||||
<QueryProvider>
|
||||
<ConfigProvider>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
</QueryProvider>
|
||||
</NuqsAdapter>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
import React, { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
||||
import { MapContainer, TileLayer, useMapEvents, Marker, Popup, MapContainerProps, useMap, Polyline } from "react-leaflet";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L from "leaflet";
|
||||
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
|
||||
@@ -17,16 +16,9 @@ import { buildApiUrl } from "@/lib/api";
|
||||
import { NodePosition } from "@/types/map";
|
||||
import { useNeighbors, type Neighbor } from "@/hooks/useNeighbors";
|
||||
import { type AllNeighborsConnection } from "@/hooks/useAllNeighbors";
|
||||
import { useQueryParams } from "@/hooks/useQueryParams";
|
||||
import { useQueryStates, parseAsFloat, parseAsInteger } from "nuqs";
|
||||
import { useMapPosition } from "@/hooks/useMapPosition";
|
||||
|
||||
interface MapQuery {
|
||||
lat?: number;
|
||||
lng?: number;
|
||||
zoom?: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
type ClusteredMarkersProps = {
|
||||
nodes: NodePosition[];
|
||||
@@ -505,7 +497,11 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
const [mapPosition, setMapPosition] = useMapPosition();
|
||||
|
||||
// Use query params for map position (for sharing URLs)
|
||||
const { query: mapQuery, updateQuery: updateMapQuery } = useQueryParams<MapQuery>({});
|
||||
const [mapQuery, setMapQuery] = useQueryStates({
|
||||
lat: parseAsFloat,
|
||||
lng: parseAsFloat,
|
||||
zoom: parseAsInteger,
|
||||
});
|
||||
|
||||
// Determine map center and zoom: query params take priority over localStorage
|
||||
const mapCenter: [number, number] = [
|
||||
@@ -667,7 +663,7 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
});
|
||||
|
||||
// Update URL with new map position for sharing
|
||||
updateMapQuery({
|
||||
setMapQuery({
|
||||
lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
|
||||
lng: Math.round(center.lng * 100000) / 100000,
|
||||
zoom: zoom
|
||||
@@ -709,7 +705,7 @@ export default function MapView({ target = '_self' }: MapViewProps = {}) {
|
||||
});
|
||||
|
||||
// Update URL with new map position for sharing
|
||||
updateMapQuery({
|
||||
setMapQuery({
|
||||
lat: Math.round(center.lat * 100000) / 100000, // Round to 5 decimal places
|
||||
lng: Math.round(center.lng * 100000) / 100000,
|
||||
zoom: zoom
|
||||
|
||||
@@ -56,8 +56,8 @@ export default function NodeLinkWithHover({
|
||||
// If no results or multiple results, link to search page
|
||||
const searchUrl = `/search?q=${encodeURIComponent(nodeName)}`;
|
||||
const params = [];
|
||||
if (exact) params.push('exact');
|
||||
if (is_repeater) params.push('is_repeater');
|
||||
if (exact) params.push('exact=true');
|
||||
if (is_repeater) params.push('is_repeater=true');
|
||||
return searchUrl + (params.length > 0 ? '&' + params.join('&') : '');
|
||||
})();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
|
||||
import { useQueryStates, parseAsString, parseAsInteger, parseAsBoolean } from 'nuqs';
|
||||
|
||||
export function useQueryParams<T extends Record<string, any>>(defaultValues: T = {} as T) {
|
||||
const router = useRouter();
|
||||
@@ -104,27 +105,21 @@ export function useQueryParams<T extends Record<string, any>>(defaultValues: T =
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy hook for backward compatibility - now uses the generic hook
|
||||
export interface SearchQuery {
|
||||
q: string;
|
||||
limit?: number;
|
||||
exact?: boolean;
|
||||
is_repeater?: boolean;
|
||||
}
|
||||
|
||||
// Search query params, backed by nuqs for type-safe URL state.
|
||||
export function useSearchQuery() {
|
||||
const { query, setParam } = useQueryParams<SearchQuery>({ q: '', limit: 50, exact: false, is_repeater: false });
|
||||
|
||||
const [query, setQuery] = useQueryStates({
|
||||
q: parseAsString.withDefault(''),
|
||||
limit: parseAsInteger.withDefault(50),
|
||||
exact: parseAsBoolean.withDefault(false),
|
||||
is_repeater: parseAsBoolean.withDefault(false),
|
||||
});
|
||||
|
||||
return {
|
||||
query,
|
||||
setQuery: (q: string) => setParam('q', q),
|
||||
setLimit: (limit: number) => setParam('limit', limit),
|
||||
setExact: (exact: boolean) => setParam('exact', exact),
|
||||
setIsRepeater: (is_repeater: boolean) => setParam('is_repeater', is_repeater),
|
||||
updateQuery: (updates: Partial<SearchQuery>) => {
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
setParam(key as keyof SearchQuery, value as any);
|
||||
});
|
||||
},
|
||||
setQuery: (q: string) => setQuery({ q }),
|
||||
setLimit: (limit: number) => setQuery({ limit }),
|
||||
setExact: (exact: boolean) => setQuery({ exact }),
|
||||
setIsRepeater: (is_repeater: boolean) => setQuery({ is_repeater }),
|
||||
updateQuery: setQuery,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user