From 3a5c719fa6e8a964e15f2b541a12bb1b95688a48 Mon Sep 17 00:00:00 2001 From: Yash Raj Date: Sun, 19 Oct 2025 15:02:37 +0530 Subject: [PATCH 1/3] Refactor SearchParams to nuqs searchState --- src/app/(public)/_components/search-form.tsx | 26 +++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/app/(public)/_components/search-form.tsx b/src/app/(public)/_components/search-form.tsx index cb52e32..71e5d06 100644 --- a/src/app/(public)/_components/search-form.tsx +++ b/src/app/(public)/_components/search-form.tsx @@ -1,25 +1,29 @@ 'use client'; -import { usePathname, useSearchParams, useRouter } from 'next/navigation'; +import { usePathname } from 'next/navigation'; import { useForm } from 'react-hook-form'; import { GoX } from 'react-icons/go'; +import { useQueryState } from 'nuqs'; interface FormValues { searchQuery: string; } export function SearchForm() { - const router = useRouter(); const pathname = usePathname(); - const searchParams = useSearchParams(); + const [searchQuery, setSearchQuery] = useQueryState('q', { + defaultValue: '', + parse: (value: string) => value, + serialize: (value: string) => value || '' + }); const { register, handleSubmit, reset, watch } = useForm({ defaultValues: { - searchQuery: searchParams.get('q') as string + searchQuery: searchQuery } }); - const searchQuery = watch('searchQuery'); + const queryValue = watch('searchQuery'); if (!pathname.startsWith('/repos')) { return null; @@ -28,12 +32,9 @@ export function SearchForm() { function onSubmit({ searchQuery }: FormValues) { if (!pathname.startsWith('/repos')) return; - const reposPathname = pathname as `/repos/${string}`; const trimmedQuery = searchQuery.trim(); if (trimmedQuery !== '') { - const sp = new URLSearchParams(searchParams); - sp.set('q', trimmedQuery); - router.push(`${reposPathname}?${sp.toString()}`); + void setSearchQuery(trimmedQuery); } } @@ -47,11 +48,14 @@ export function SearchForm() { type="text" {...register('searchQuery', { required: true })} /> - {searchQuery && searchQuery.trim() !== '' && ( + {queryValue && queryValue.trim() !== '' && ( From 8112daa3a8b3c5bdfb9e48a7021de0cefbe0237e Mon Sep 17 00:00:00 2001 From: Yash Raj Date: Tue, 21 Oct 2025 07:30:01 +0530 Subject: [PATCH 2/3] Refactor repo filters to use nuqs query state Replaces manual URLSearchParams and next/navigation search param handling with nuqs useQueryState and useQueryStates in pagination, sorter, and stars-filter components. Simplifies state management and improves consistency for query parameter updates across repo listing UI. --- .../(public)/repos/_components/pagination.tsx | 27 ++++- src/app/(public)/repos/_components/sorter.tsx | 99 +++++++++---------- .../repos/_components/stars-filter.tsx | 38 +++---- src/app/(public)/repos/page.tsx | 1 - 4 files changed, 91 insertions(+), 74 deletions(-) diff --git a/src/app/(public)/repos/_components/pagination.tsx b/src/app/(public)/repos/_components/pagination.tsx index 1329d7d..2f091f2 100644 --- a/src/app/(public)/repos/_components/pagination.tsx +++ b/src/app/(public)/repos/_components/pagination.tsx @@ -1,20 +1,41 @@ import { Button } from '@/app/(public)/_components/button'; +<<<<<<< Updated upstream import { ArrowLeft, ArrowRight } from 'lucide-react'; import Link from 'next/link'; import type { SearchParams } from '@/types'; +======= +import { ArrowLeft, ArrowRight, Loader2 } from 'lucide-react'; +import { useTransition } from 'react'; +import { useQueryState } from 'nuqs'; +>>>>>>> Stashed changes const MAX_PER_PAGE = 21; interface PaginationProps { page: number; totalCount: number; - searchParams: SearchParams; } export function Pagination({ page, - totalCount, - searchParams + totalCount }: PaginationProps) { +<<<<<<< Updated upstream +======= + const [, setPageParam] = useQueryState('p', { + defaultValue: '1', + parse: (value: string) => value, + serialize: (value: string) => value + }); + const [isPending, startTransition] = useTransition(); + + function changePage(delta: number) { + const newPage = page + delta; + startTransition(() => { + void setPageParam(String(newPage)); + }); + } + +>>>>>>> Stashed changes return (
{page > 1 && ( diff --git a/src/app/(public)/repos/_components/sorter.tsx b/src/app/(public)/repos/_components/sorter.tsx index 545c35c..31f4718 100644 --- a/src/app/(public)/repos/_components/sorter.tsx +++ b/src/app/(public)/repos/_components/sorter.tsx @@ -4,8 +4,9 @@ import { Button } from '@/app/(public)/_components/button'; import { ArrowUpAZ, Code } from 'lucide-react'; import Link from 'next/link'; import languages from '@/assets/languages.json'; -import { usePathname, useSearchParams } from 'next/navigation'; +import { usePathname } from 'next/navigation'; import { sortByName } from '@/lib/utils'; +import { useQueryStates, useQueryState } from 'nuqs'; const { mainLanguages } = languages; @@ -23,88 +24,74 @@ enum SortTypes { type Pathname = '/repos' | `/repos/${string}`; export function Sorter() { - const searchParams = useSearchParams(); const pathname = usePathname() as Pathname; + const [sortField] = useQueryState('s', { defaultValue: '' }); + const [sortOrder] = useQueryState('o', { defaultValue: 'desc' }); + const [languages_] = useQueryState('l', { parse: (value: string) => value.split(',').filter(Boolean), serialize: (value: string[]) => value.join(',') }); const navigationItems = [ { name: 'Best match', - onSelect(sp: URLSearchParams) { - sp.delete('o'); - sp.delete('s'); - return sp; + onSelect(): { s: string | null; o: string | null } { + return { s: null, o: null }; } }, { name: 'Most stars', - onSelect(sp: URLSearchParams) { - sp.set('s', 'stars'); - sp.set('o', 'desc'); - return sp; + onSelect() { + return { s: 'stars', o: 'desc' }; } }, { name: 'Fewest stars', - onSelect(sp: URLSearchParams) { - sp.set('s', 'stars'); - sp.set('o', 'asc'); - return sp; + onSelect() { + return { s: 'stars', o: 'asc' }; } }, { name: 'Most forks', - onSelect(sp: URLSearchParams) { - sp.set('s', 'forks'); - sp.set('o', 'desc'); - return sp; + onSelect() { + return { s: 'forks', o: 'desc' }; } }, { name: 'Fewest forks', - onSelect(sp: URLSearchParams) { - sp.set('s', 'forks'); - sp.set('o', 'asc'); - return sp; + onSelect() { + return { s: 'forks', o: 'asc' }; } }, { name: 'Most help wanted issues', - onSelect(sp: URLSearchParams) { - sp.set('s', 'help-wanted-issues'); - sp.set('o', 'desc'); - return sp; + onSelect() { + return { s: 'help-wanted-issues', o: 'desc' }; } }, { name: 'Recently updated', - onSelect(sp: URLSearchParams) { - sp.set('s', 'updated'); - sp.set('o', 'desc'); - return sp; + onSelect() { + return { s: 'updated', o: 'desc' }; } }, { name: 'Least recently updated', - onSelect(sp: URLSearchParams) { - sp.set('s', 'updated'); - sp.set('o', 'asc'); - return sp; + onSelect() { + return { s: 'updated', o: 'asc' }; } } ]; function selectedSort(): SortTypes { - if (searchParams.get('o') === 'asc') { - if (searchParams.get('s') === 'stars') return SortTypes.FewestStars; - if (searchParams.get('s') === 'forks') return SortTypes.FewestForks; - if (searchParams.get('s') === 'updated') + if (sortOrder === 'asc') { + if (sortField === 'stars') return SortTypes.FewestStars; + if (sortField === 'forks') return SortTypes.FewestForks; + if (sortField === 'updated') return SortTypes.LeastRecentlyUpdated; return SortTypes.BestMatch; - } else if (searchParams.get('o') === 'desc') { - if (searchParams.get('s') === 'stars') return SortTypes.MostStars; - if (searchParams.get('s') === 'forks') return SortTypes.MostForks; - if (searchParams.get('s') === 'updated') return SortTypes.RecentlyUpdated; - if (searchParams.get('s') === 'help-wanted-issues') + } else if (sortOrder === 'desc') { + if (sortField === 'stars') return SortTypes.MostStars; + if (sortField === 'forks') return SortTypes.MostForks; + if (sortField === 'updated') return SortTypes.RecentlyUpdated; + if (sortField === 'help-wanted-issues') return SortTypes.MostHelpWantedIssues; return SortTypes.BestMatch; } else { @@ -127,12 +114,17 @@ export function Sorter() {
    {mainLanguages.sort(sortByName).map(language => { - const sp = new URLSearchParams(searchParams); - sp.delete('p'); + const languageParams = `l=${language.toLowerCase()}`; + const currentParams = new URLSearchParams(); + if (sortField) currentParams.set('s', sortField); + if (sortOrder) currentParams.set('o', sortOrder); + const queryString = currentParams.toString(); + const fullQuery = queryString ? `${languageParams}&${queryString}` : languageParams; + return (
  • {language} @@ -151,16 +143,19 @@ export function Sorter() {
      {navigationItems.map((item, index) => { - const sp = item.onSelect(new URLSearchParams(searchParams)); - sp.delete('p'); - if (item.name === SortTypes.BestMatch) { - sp.delete('o'); - sp.delete('s'); + const { s, o } = item.onSelect(); + const currentParams = new URLSearchParams(); + if (s) currentParams.set('s', s); + if (o) currentParams.set('o', o); + if (languages_ && languages_.length > 0) { + currentParams.set('l', languages_.join(',')); } + const queryString = currentParams.toString(); + return (
    • {item.name} diff --git a/src/app/(public)/repos/_components/stars-filter.tsx b/src/app/(public)/repos/_components/stars-filter.tsx index d2fcc1d..407a58f 100644 --- a/src/app/(public)/repos/_components/stars-filter.tsx +++ b/src/app/(public)/repos/_components/stars-filter.tsx @@ -2,12 +2,11 @@ import { useParams, - usePathname, - useRouter, - useSearchParams + usePathname } from 'next/navigation'; import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { useQueryStates } from 'nuqs'; interface FormValues { startStars: number | ''; @@ -17,18 +16,18 @@ interface FormValues { type Pathname = '/repos' | `/repos/${string}`; export function StarsFilter() { - const router = useRouter(); const pathname = usePathname() as Pathname; - const searchParams = useSearchParams(); const params = useParams(); + const [{ startStars, endStars, p }, setStarsParams] = useQueryStates({ + startStars: { parse: (value: string) => (value ? +value : ''), serialize: (value: number | '') => value === '' ? '' : String(value) }, + endStars: { parse: (value: string) => (value ? +value : ''), serialize: (value: number | '') => value === '' ? '' : String(value) }, + p: { parse: (value: string) => (value ? +value : 1), serialize: (value: number) => String(value) } + }); + const { handleSubmit, control, reset } = useForm({ defaultValues: { - startStars: !searchParams.get('startStars') - ? '' - : +(searchParams.get('startStars') as string), - endStars: !searchParams.get('endStars') - ? '' - : +(searchParams.get('endStars') as string) + startStars: startStars === '' ? '' : startStars || '', + endStars: endStars === '' ? '' : endStars || '' } }); @@ -36,21 +35,24 @@ export function StarsFilter() { useEffect(() => reset(), [params.language]); function onSubmit({ startStars, endStars }: FormValues) { - const sp = new URLSearchParams(searchParams); if ( typeof endStars === 'number' && typeof startStars === 'number' && endStars < startStars ) { reset({ startStars, endStars: '' }); - sp.delete('endStars'); - sp.set('startStars', startStars.toString()); + void setStarsParams({ + startStars, + endStars: '', + p: 1 + }); } else { - sp.set('startStars', startStars.toString()); - sp.set('endStars', endStars.toString()); + void setStarsParams({ + startStars: typeof startStars === 'number' ? startStars : '', + endStars: typeof endStars === 'number' ? endStars : '', + p: 1 + }); } - sp.delete('p'); - router.push(`${pathname}?${sp.toString()}`); } return ( diff --git a/src/app/(public)/repos/page.tsx b/src/app/(public)/repos/page.tsx index 3b03988..7b5466c 100644 --- a/src/app/(public)/repos/page.tsx +++ b/src/app/(public)/repos/page.tsx @@ -64,7 +64,6 @@ export default async function ReposPage({
From e4a985d5c2351f046f765de4e350b86be0ae9989 Mon Sep 17 00:00:00 2001 From: Yash Raj Date: Tue, 21 Oct 2025 07:34:10 +0530 Subject: [PATCH 3/3] Update pagination.tsx Resolve Conflicts --- .../(public)/repos/_components/pagination.tsx | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/app/(public)/repos/_components/pagination.tsx b/src/app/(public)/repos/_components/pagination.tsx index 2f091f2..07f3faa 100644 --- a/src/app/(public)/repos/_components/pagination.tsx +++ b/src/app/(public)/repos/_components/pagination.tsx @@ -1,13 +1,9 @@ +'use client'; + import { Button } from '@/app/(public)/_components/button'; -<<<<<<< Updated upstream -import { ArrowLeft, ArrowRight } from 'lucide-react'; -import Link from 'next/link'; -import type { SearchParams } from '@/types'; -======= import { ArrowLeft, ArrowRight, Loader2 } from 'lucide-react'; import { useTransition } from 'react'; import { useQueryState } from 'nuqs'; ->>>>>>> Stashed changes const MAX_PER_PAGE = 21; interface PaginationProps { @@ -19,8 +15,6 @@ export function Pagination({ page, totalCount }: PaginationProps) { -<<<<<<< Updated upstream -======= const [, setPageParam] = useQueryState('p', { defaultValue: '1', parse: (value: string) => value, @@ -35,25 +29,46 @@ export function Pagination({ }); } ->>>>>>> Stashed changes return (
{page > 1 && ( - - - + )} {totalCount >= MAX_PER_PAGE && page < Math.ceil(totalCount / MAX_PER_PAGE) && ( - - - + )}
);