mirror of
https://github.com/ChiChou/entdb.git
synced 2026-09-23 13:10:42 +02:00
Sync text filter keyword to URL query param
Extract a useQueryFilter hook that debounces the filter keyword and mirrors it into the URL's q param via router.replace, so deep links open pre-filtered and rapid typing doesn't flood history. Apply across keys, files, find, and oslist, replacing their ad-hoc debounce logic.
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useDebounce } from "use-debounce";
|
|
||||||
import { Search, X, ChevronsUpDown, ChevronsDownUp } from "lucide-react";
|
import { Search, X, ChevronsUpDown, ChevronsDownUp } from "lucide-react";
|
||||||
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -10,6 +9,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import FileSystem from "@/components/filesystem";
|
import FileSystem from "@/components/filesystem";
|
||||||
import { HeaderPortal } from "@/components/header-portal";
|
import { HeaderPortal } from "@/components/header-portal";
|
||||||
import { createEngine } from "@/lib/engine";
|
import { createEngine } from "@/lib/engine";
|
||||||
|
import { useQueryFilter } from "@/hooks/use-query-filter";
|
||||||
|
|
||||||
export default function Files() {
|
export default function Files() {
|
||||||
const params = useSearchParams();
|
const params = useSearchParams();
|
||||||
@@ -18,10 +18,9 @@ export default function Files() {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [files, setFiles] = useState<string[]>([]);
|
const [files, setFiles] = useState<string[]>([]);
|
||||||
const [keyword, setKeyword] = useState("");
|
|
||||||
const [expandAll, setExpandAll] = useState<boolean | null>(null);
|
const [expandAll, setExpandAll] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const [debouncedKeyword] = useDebounce(keyword, 200);
|
const { keyword, setKeyword, debouncedKeyword } = useQueryFilter("q", 200);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import FileSystem from "@/components/filesystem";
|
import FileSystem from "@/components/filesystem";
|
||||||
import { HeaderPortal } from "@/components/header-portal";
|
import { HeaderPortal } from "@/components/header-portal";
|
||||||
import { createEngine } from "@/lib/engine";
|
import { createEngine } from "@/lib/engine";
|
||||||
|
import { useQueryFilter } from "@/hooks/use-query-filter";
|
||||||
|
|
||||||
export default function FindByKey() {
|
export default function FindByKey() {
|
||||||
const params = useSearchParams();
|
const params = useSearchParams();
|
||||||
@@ -29,17 +30,8 @@ export default function FindByKey() {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [paths, setPaths] = useState<string[]>([]);
|
const [paths, setPaths] = useState<string[]>([]);
|
||||||
const [keyword, setKeyword] = useState("");
|
|
||||||
const [debouncedKeyword, setDebouncedKeyword] = useState("");
|
|
||||||
const [expandAll, setExpandAll] = useState<boolean | null>(null);
|
const [expandAll, setExpandAll] = useState<boolean | null>(null);
|
||||||
|
const { keyword, setKeyword, debouncedKeyword } = useQueryFilter();
|
||||||
// Debounce keyword with 300ms delay
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDebouncedKeyword(keyword);
|
|
||||||
}, 300);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [keyword]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchPaths() {
|
async function fetchPaths() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { HeaderPortal } from "@/components/header-portal";
|
import { HeaderPortal } from "@/components/header-portal";
|
||||||
import { createEngine } from "@/lib/engine";
|
import { createEngine } from "@/lib/engine";
|
||||||
import { tokenizeKeys, getTopTokens } from "@/lib/tokenizer";
|
import { tokenizeKeys, getTopTokens } from "@/lib/tokenizer";
|
||||||
|
import { useQueryFilter } from "@/hooks/use-query-filter";
|
||||||
|
|
||||||
export default function Keys() {
|
export default function Keys() {
|
||||||
const params = useSearchParams();
|
const params = useSearchParams();
|
||||||
@@ -18,15 +19,7 @@ export default function Keys() {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [keys, setKeys] = useState<string[]>([]);
|
const [keys, setKeys] = useState<string[]>([]);
|
||||||
const [keyword, setKeyword] = useState("");
|
const { keyword, setKeyword, debouncedKeyword } = useQueryFilter();
|
||||||
const [debouncedKeyword, setDebouncedKeyword] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDebouncedKeyword(keyword);
|
|
||||||
}, 300);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [keyword]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
|||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { useQueryFilter } from "@/hooks/use-query-filter";
|
||||||
import { dataURL } from "@/lib/env";
|
import { dataURL } from "@/lib/env";
|
||||||
import type { Group, OS } from "@/lib/types";
|
import type { Group, OS } from "@/lib/types";
|
||||||
import { HeaderPortal } from "./header-portal";
|
import { HeaderPortal } from "./header-portal";
|
||||||
@@ -74,15 +75,7 @@ export default function OSList() {
|
|||||||
const [groups, setGroups] = useState<Group[]>([]);
|
const [groups, setGroups] = useState<Group[]>([]);
|
||||||
const [highlights, setHighlights] = useState<Set<string>>(new Set());
|
const [highlights, setHighlights] = useState<Set<string>>(new Set());
|
||||||
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
||||||
const [keyword, setKeyword] = useState("");
|
const { keyword, setKeyword, debouncedKeyword } = useQueryFilter("q", 200);
|
||||||
const [debouncedKeyword, setDebouncedKeyword] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDebouncedKeyword(keyword);
|
|
||||||
}, 200);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [keyword]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const set: Set<string> = new Set();
|
const set: Set<string> = new Set();
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||||
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
|
import { basePath } from "@/lib/env";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounced text filter that mirrors its value into a URL query param.
|
||||||
|
*
|
||||||
|
* - Initializes the keyword from the param so deep links open pre-filtered.
|
||||||
|
* - Debounces, so rapid typing ("c" → "co" → "com") only writes the URL once.
|
||||||
|
* - Uses router.replace (not push), so intermediate filter states don't
|
||||||
|
* pollute browser history.
|
||||||
|
*/
|
||||||
|
export function useQueryFilter(param = "q", delay = 300) {
|
||||||
|
const params = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const [keyword, setKeyword] = useState(() => params.get(param) ?? "");
|
||||||
|
const [debouncedKeyword] = useDebounce(keyword, delay);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = new URLSearchParams(params.toString());
|
||||||
|
if (debouncedKeyword) {
|
||||||
|
next.set(param, debouncedKeyword);
|
||||||
|
} else {
|
||||||
|
next.delete(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
// usePathname() may include the configured basePath, which router.replace
|
||||||
|
// re-applies — strip it to avoid doubling (mirrors version-switcher).
|
||||||
|
const path =
|
||||||
|
basePath && pathname.startsWith(basePath)
|
||||||
|
? pathname.slice(basePath.length)
|
||||||
|
: pathname;
|
||||||
|
const query = next.toString();
|
||||||
|
router.replace(query ? `${path}?${query}` : path, { scroll: false });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [debouncedKeyword]);
|
||||||
|
|
||||||
|
return { keyword, setKeyword, debouncedKeyword };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user