From df59314b090765617c0d84ffb96f2d4e3ec99af8 Mon Sep 17 00:00:00 2001 From: Yash Raj Date: Sun, 19 Oct 2025 13:12:27 +0530 Subject: [PATCH 1/8] Refactor pagination to use buttons instead of links --- .../(public)/repos/_components/pagination.tsx | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/src/app/(public)/repos/_components/pagination.tsx b/src/app/(public)/repos/_components/pagination.tsx index 1329d7d..5d105a0 100644 --- a/src/app/(public)/repos/_components/pagination.tsx +++ b/src/app/(public)/repos/_components/pagination.tsx @@ -1,6 +1,9 @@ +'use client'; + import { Button } from '@/app/(public)/_components/button'; -import { ArrowLeft, ArrowRight } from 'lucide-react'; -import Link from 'next/link'; +import { ArrowLeft, ArrowRight, Loader2 } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useTransition } from 'react'; import type { SearchParams } from '@/types'; const MAX_PER_PAGE = 21; @@ -15,24 +18,60 @@ export function Pagination({ totalCount, searchParams }: PaginationProps) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + function changePage(delta: number) { + const params = new URLSearchParams( + Object.entries(searchParams).map(([k, v]) => [k, String(v)]) + ); + params.set('p', String(page + delta)); + + startTransition(() => { + router.push(`?${params.toString()}`); + }); + } + return (
{page > 1 && ( - - - + )} {totalCount >= MAX_PER_PAGE && page < Math.ceil(totalCount / MAX_PER_PAGE) && ( - - - + )}
); From f823b02659d96ca3ddbed327731b60b5fabde7b6 Mon Sep 17 00:00:00 2001 From: Yash Raj Date: Sun, 19 Oct 2025 13:39:54 +0530 Subject: [PATCH 2/8] Refactor search form to use nuqs for query state Replaces manual search query handling with nuqs's useQueryState for improved URL query management in the search form. Adds nuqs dependency to package.json. --- package.json | 1 + src/app/(public)/_components/search-form.tsx | 26 +++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index fd1c3f4..98d362e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "next-cloudinary": "^6.16.0", "next-seo": "^6.8.0", "nextjs-progressbar": "^0.0.16", + "nuqs": "^2.7.2", "postcss": "^8.5.6", "postgres": "^3.4.7", "prettier": "^3.6.2", 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 3a5c719fa6e8a964e15f2b541a12bb1b95688a48 Mon Sep 17 00:00:00 2001 From: Yash Raj Date: Sun, 19 Oct 2025 15:02:37 +0530 Subject: [PATCH 3/8] 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 4/8] 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 5/8] 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) && ( - - - + )}
); From 94b31311d2e1bee2abe7db418c6e2fb4944d709e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:05:35 +0000 Subject: [PATCH 6/8] Initial plan From e240d3e463b5505280f0f876977787ad36ed43a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:09:08 +0000 Subject: [PATCH 7/8] Add nuqs to pnpm lockfile Co-authored-by: offbeatjs <193760898+offbeatjs@users.noreply.github.com> --- pnpm-lock.yaml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea436d6..32430ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: nextjs-progressbar: specifier: ^0.0.16 version: 0.0.16(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + nuqs: + specifier: ^2.7.2 + version: 2.7.2(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) postcss: specifier: ^8.5.6 version: 8.5.6 @@ -846,6 +849,9 @@ packages: '@rushstack/eslint-patch@1.12.0': resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -2229,6 +2235,27 @@ packages: nprogress@0.2.0: resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + nuqs@2.7.2: + resolution: {integrity: sha512-wOPJoz5om7jMJQick9zU1S/Q+joL+B2DZTZxfCleHEcUzjUnPoujGod4+nAmUWb+G9TwZnyv+mfNqlyfEi8Zag==} + peerDependencies: + '@remix-run/react': '>=2' + '@tanstack/react-router': ^1 + next: '>=14.2.0' + react: '>=18.2.0 || ^19.0.0-0' + react-router: ^6 || ^7 + react-router-dom: ^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 + oauth4webapi@3.6.1: resolution: {integrity: sha512-b39+drVyA4aNUptFOhkkmGWnG/BE7dT29SW/8PVYElqp7j/DBqzm5SS1G+MUD07XlTcBOAG+6Cb/35Cx2kHIuQ==} @@ -3373,6 +3400,8 @@ snapshots: '@rushstack/eslint-patch@1.12.0': {} + '@standard-schema/spec@1.0.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -4858,6 +4887,13 @@ snapshots: nprogress@0.2.0: {} + nuqs@2.7.2(next@15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + dependencies: + '@standard-schema/spec': 1.0.0 + react: 19.1.1 + optionalDependencies: + next: 15.5.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + oauth4webapi@3.6.1: {} object-assign@4.1.1: {} From e3538388c55e43e32546d0935d0c584873a35796 Mon Sep 17 00:00:00 2001 From: Yash Raj Date: Fri, 31 Oct 2025 17:28:27 +0530 Subject: [PATCH 8/8] Remove searchParams from Pagination component Removed searchParams prop from Pagination component. --- src/app/(public)/repos/[language]/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/(public)/repos/[language]/page.tsx b/src/app/(public)/repos/[language]/page.tsx index 186eb78..cc9721c 100644 --- a/src/app/(public)/repos/[language]/page.tsx +++ b/src/app/(public)/repos/[language]/page.tsx @@ -64,7 +64,6 @@ export default async function ReposPage({