mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-08-08 01:22:43 +02:00
query-params: migrate ChatBox to nuqs and delete hand-rolled hook
Move ChatBox's selectedTab to nuqs useQueryState, inline the search params directly in the search page, and remove the now-unused generic useQueryParams hook (and its file). The entire app's URL query-param state is now nuqs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useConfig } from '@/components/ConfigContext';
|
||||
import { useSearchQuery } from '@/hooks/useQueryParams';
|
||||
import { useQueryStates, parseAsString, parseAsInteger, parseAsBoolean } from 'nuqs';
|
||||
import { useMeshcoreSearch } from '@/hooks/useMeshcoreSearch';
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import SearchResults from '@/components/SearchResults';
|
||||
@@ -12,7 +12,12 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
function SearchPageContent() {
|
||||
const { config } = useConfig();
|
||||
const { query, setQuery, setLimit, setExact, setIsRepeater } = useSearchQuery();
|
||||
const [query, setQuery] = useQueryStates({
|
||||
q: parseAsString.withDefault(''),
|
||||
limit: parseAsInteger.withDefault(50),
|
||||
exact: parseAsBoolean.withDefault(false),
|
||||
is_repeater: parseAsBoolean.withDefault(false),
|
||||
});
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
// Always use config values for region and lastSeen
|
||||
@@ -41,7 +46,7 @@ function SearchPageContent() {
|
||||
};
|
||||
|
||||
const handleLimitChange = (limit: number) => {
|
||||
setLimit(limit);
|
||||
setQuery({ limit });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -61,7 +66,7 @@ function SearchPageContent() {
|
||||
<div className="mb-6">
|
||||
<SearchInput
|
||||
value={query.q}
|
||||
onChange={setQuery}
|
||||
onChange={(q) => setQuery({ q })}
|
||||
placeholder="Search by node name or public key..."
|
||||
autoFocus
|
||||
/>
|
||||
@@ -138,7 +143,7 @@ function SearchPageContent() {
|
||||
type="checkbox"
|
||||
id="exact-match"
|
||||
checked={query.exact}
|
||||
onChange={(e) => setExact(e.target.checked)}
|
||||
onChange={(e) => setQuery({ exact: e.target.checked })}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="exact-match" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
@@ -157,7 +162,7 @@ function SearchPageContent() {
|
||||
type="checkbox"
|
||||
id="is-repeater"
|
||||
checked={query.is_repeater}
|
||||
onChange={(e) => setIsRepeater(e.target.checked)}
|
||||
onChange={(e) => setQuery({ is_repeater: e.target.checked })}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="is-repeater" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -10,7 +10,7 @@ import { selectorLabel } from "@/lib/regions";
|
||||
import { useRegionGroups } from "@/hooks/useRegions";
|
||||
import { useChatMessages } from "@/hooks/useChatMessages";
|
||||
import { useIntersectionObserver } from "@/hooks/useIntersectionObserver";
|
||||
import { useQueryParams } from "@/hooks/useQueryParams";
|
||||
import { useQueryState, parseAsInteger } from "nuqs";
|
||||
|
||||
|
||||
interface ChatBoxProps {
|
||||
@@ -25,10 +25,6 @@ interface TabItem {
|
||||
isAllMessages?: boolean;
|
||||
}
|
||||
|
||||
interface ChatBoxQuery {
|
||||
selectedTab?: number;
|
||||
}
|
||||
|
||||
export default function ChatBox({
|
||||
showAllMessagesTab = false,
|
||||
className = "",
|
||||
@@ -47,16 +43,15 @@ export default function ChatBox({
|
||||
: meshcoreKeys;
|
||||
|
||||
// Use query params to persist selected tab across navigation
|
||||
const { query, setParam } = useQueryParams<ChatBoxQuery>({
|
||||
selectedTab: showAllMessagesTab ? 1 : 0,
|
||||
});
|
||||
|
||||
const [rawSelectedTab, setSelectedTab] = useQueryState(
|
||||
'selectedTab',
|
||||
parseAsInteger.withDefault(showAllMessagesTab ? 1 : 0)
|
||||
);
|
||||
|
||||
// Ensure selectedTab is within bounds of available tabs
|
||||
const rawSelectedTab = query.selectedTab ?? (showAllMessagesTab ? 1 : 0);
|
||||
const selectedTab = rawSelectedTab >= 0 && rawSelectedTab < allTabs.length
|
||||
? rawSelectedTab
|
||||
const selectedTab = rawSelectedTab >= 0 && rawSelectedTab < allTabs.length
|
||||
? rawSelectedTab
|
||||
: (showAllMessagesTab ? 1 : 0);
|
||||
const setSelectedTab = (tabIndex: number) => setParam('selectedTab', tabIndex);
|
||||
|
||||
const [minimized, setMinimized] = useState(!startExpanded); // Use startExpanded as default for minimized state
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
"use client";
|
||||
|
||||
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();
|
||||
const searchParams = useSearchParams();
|
||||
const [internalState, setInternalState] = useState<T>(() => {
|
||||
const result = { ...defaultValues };
|
||||
|
||||
// Initialize from search params on mount
|
||||
if (typeof window !== 'undefined') {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
urlParams.forEach((value, key) => {
|
||||
// Don't auto-convert 'q' (query) parameter to number since it should always be a string
|
||||
if (key !== 'q' && !isNaN(Number(value)) && value !== '') {
|
||||
result[key as keyof T] = Number(value) as T[keyof T];
|
||||
} else {
|
||||
result[key as keyof T] = value as T[keyof T];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const internalStateRef = useRef(internalState);
|
||||
internalStateRef.current = internalState;
|
||||
|
||||
// Only update internal state when searchParams change from external navigation
|
||||
// (not from our own updates)
|
||||
useEffect(() => {
|
||||
const newState = { ...defaultValues };
|
||||
|
||||
searchParams.forEach((value, key) => {
|
||||
// Don't auto-convert 'q' (query) parameter to number since it should always be a string
|
||||
if (key !== 'q' && !isNaN(Number(value)) && value !== '') {
|
||||
newState[key as keyof T] = Number(value) as T[keyof T];
|
||||
} else {
|
||||
newState[key as keyof T] = value as T[keyof T];
|
||||
}
|
||||
});
|
||||
|
||||
// Only update if the state is actually different
|
||||
const stateChanged = JSON.stringify(newState) !== JSON.stringify(internalStateRef.current);
|
||||
if (stateChanged) {
|
||||
setInternalState(newState);
|
||||
}
|
||||
}, [searchParams, defaultValues]);
|
||||
|
||||
const query = internalState;
|
||||
|
||||
const updateQuery = useCallback((updates: Partial<T>) => {
|
||||
const newState = { ...internalState, ...updates };
|
||||
setInternalState(newState);
|
||||
|
||||
const newSearchParams = new URLSearchParams();
|
||||
|
||||
Object.entries(newState).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '' && value !== defaultValues[key as keyof T]) {
|
||||
newSearchParams.set(key, value.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const newUrl = `${window.location.pathname}${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ''}`;
|
||||
|
||||
// Use native History API for shallow-like routing
|
||||
window.history.replaceState(null, '', newUrl);
|
||||
}, [internalState, defaultValues]);
|
||||
|
||||
const setParam = useCallback(<K extends keyof T>(key: K, value: T[K]) => {
|
||||
updateQuery({ [key]: value } as unknown as Partial<T>);
|
||||
}, [updateQuery]);
|
||||
|
||||
const clearParam = useCallback((key: keyof T) => {
|
||||
const newState = { ...internalState };
|
||||
delete newState[key];
|
||||
setInternalState(newState);
|
||||
|
||||
const newSearchParams = new URLSearchParams();
|
||||
|
||||
Object.entries(newState).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null && v !== '' && v !== defaultValues[k as keyof T]) {
|
||||
newSearchParams.set(k, v.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const newUrl = `${window.location.pathname}${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ''}`;
|
||||
window.history.replaceState(null, '', newUrl);
|
||||
}, [internalState, defaultValues]);
|
||||
|
||||
const clearAll = useCallback(() => {
|
||||
setInternalState({ ...defaultValues });
|
||||
window.history.replaceState(null, '', window.location.pathname);
|
||||
}, [defaultValues]);
|
||||
|
||||
return {
|
||||
query,
|
||||
updateQuery,
|
||||
setParam,
|
||||
clearParam,
|
||||
clearAll,
|
||||
};
|
||||
}
|
||||
|
||||
// Search query params, backed by nuqs for type-safe URL state.
|
||||
export function useSearchQuery() {
|
||||
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) => 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