Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions packages/examples/next-app/src/app/_components/client.tsx

This file was deleted.

47 changes: 47 additions & 0 deletions packages/examples/next-app/src/app/_components/filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client'

import { useQueryStates } from 'nuqs'
import { searchParams } from '../searchParams'

export function Filter() {
const [filter, setFilter] = useQueryStates(searchParams, {
shallow: false
})
return (
<div className="flex flex-col gap-4">
<input
className="border border-gray-300 dark:border-gray-600 rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
type="text"
value={filter.search}
onChange={e => setFilter({ search: e.target.value, page: 1 })}
placeholder="Search users"
/>

<div className="flex gap-2">
<button
onClick={() =>
setFilter(prev => ({
order: prev.order === 'asc' ? 'desc' : 'asc',
page: 1
}))
}
className="rounded px-4 flex-1 py-2 text-sm border border-gray-300 dark:border-gray-600 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 transition-colors"
>
Order:{' '}
{filter.order === 'asc'
? 'Ascending'
: filter.order === 'desc'
? 'Descending'
: 'None'}
</button>

<button
onClick={() => setFilter(null)}
className="rounded px-4 py-2 text-sm border border-red-400 bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-800 dark:text-red-100 dark:hover:bg-red-700 transition-colors"
>
Reset
</button>
</div>
</div>
)
}
32 changes: 32 additions & 0 deletions packages/examples/next-app/src/app/_components/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { useQueryStates } from 'nuqs'
import { searchParams, serialize } from '../searchParams'

export function Pagination({ total }: { total: number }) {
const [{ page, limit }] = useQueryStates(searchParams)
const totalPages = Math.ceil(total / limit)
const currentSearchParams = useSearchParams()
return (
<nav className="mt-2 flex items-center gap-2 mx-auto">
{Array.from({ length: totalPages }, (_, i) => {
const p = i + 1
const isActive = p === page
return (
<Link
key={p}
href={serialize(currentSearchParams, { page: p })}
className={`rounded border px-3 py-1 text-sm transition-colors ${
isActive
? 'bg-gray-800 text-white dark:bg-gray-200 dark:text-black'
: 'hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
>
{p}
</Link>
)
})}
</nav>
)
}
18 changes: 0 additions & 18 deletions packages/examples/next-app/src/app/_components/server.tsx

This file was deleted.

21 changes: 21 additions & 0 deletions packages/examples/next-app/src/app/_components/users-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getUsers } from '@/data'
import { searchParamsCache } from '../searchParams'
import { Pagination } from './pagination'

export async function UsersList() {
const params = searchParamsCache.all()
const users = await getUsers(params)

return (
<div className="w-full flex flex-col gap-4">
<ul className="w-full max-w-sm divide-y divide-gray-200 dark:divide-gray-700 text-center sm:text-left">
{users.data.map(user => (
<li key={user.id} className="py-2">
{user.name}
</li>
))}
</ul>
<Pagination total={users.total} />
</div>
)
}
10 changes: 6 additions & 4 deletions packages/examples/next-app/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { SearchParams } from 'next/dist/server/request/search-params'
import Image from 'next/image'
import { Suspense } from 'react'
import { Client } from './_components/client'
import { Server } from './_components/server'
import { Filter } from './_components/filter'
import { UsersList } from './_components/users-list'
import { searchParamsCache } from './searchParams'

type PageProps = {
searchParams: Promise<SearchParams>
}

export default async function Home({ searchParams }: PageProps) {
await searchParamsCache.parse(searchParams)
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
Expand All @@ -21,10 +23,10 @@ export default async function Home({ searchParams }: PageProps) {
priority
/>
<Suspense>
<Server searchParams={searchParams} />
<Filter />
</Suspense>
<Suspense>
<Client />
<UsersList />
</Suspense>
<a
className="w-full rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
Expand Down
18 changes: 14 additions & 4 deletions packages/examples/next-app/src/app/searchParams.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { createLoader, parseAsInteger, parseAsString } from 'nuqs/server'
import {
createSearchParamsCache,
createSerializer,
parseAsInteger,
parseAsString,
parseAsStringEnum
} from 'nuqs/server'

export const searchParams = {
name: parseAsString.withDefault(''),
count: parseAsInteger.withDefault(0)
page: parseAsInteger.withDefault(1),
limit: parseAsInteger.withDefault(5),
order: parseAsStringEnum(['asc', 'desc']),
search: parseAsString.withDefault('')
}

export const loadSearchParams = createLoader(searchParams)
export const searchParamsCache = createSearchParamsCache(searchParams)

export const serialize = createSerializer(searchParams)
122 changes: 122 additions & 0 deletions packages/examples/next-app/src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
export const users = [
{
id: 1,
name: 'John Doe'
},
{
id: 2,
name: 'Jane Doe'
},
{
id: 3,
name: 'Ziad El-Sayed'
},
{
id: 4,
name: 'Alice Johnson'
},
{
id: 5,
name: 'Bob Brown'
},
{
id: 6,
name: 'Charlie Davis'
},
{
id: 7,
name: 'Diana Evans'
},
{
id: 8,
name: 'Ethan Harris'
},
{
id: 9,
name: 'Fiona Clark'
},
{
id: 10,
name: 'George Miller'
},
{
id: 11,
name: 'Hannah Wilson'
},
{
id: 12,
name: 'Ian Taylor'
},
{
id: 13,
name: 'Karen Anderson'
},
{
id: 14,
name: 'Liam Thomas'
},
{
id: 15,
name: 'Mia Rodriguez'
},
{
id: 16,
name: 'Noah Martinez'
},
{
id: 17,
name: 'Olivia Lee'
},
{
id: 18,
name: 'Peter Walker'
},
{
id: 19,
name: 'Quinn Young'
},
{
id: 20,
name: 'Riley King'
}
]

export interface GetUsersOptions {
page: number
limit: number
order: 'asc' | 'desc' | null
search: string
}

export async function getUsers({
page,
limit,
order,
search
}: GetUsersOptions) {
let records = users

if (search.trim()) {
const term = search.toLowerCase()
records = records.filter(item => item.name.toLowerCase().includes(term))
}

if (order) {
records = [...records].sort((a, b) => {
const valueA = a.name
const valueB = b.name
if (valueA < valueB) return order === 'asc' ? -1 : 1
if (valueA > valueB) return order === 'asc' ? 1 : -1
return 0
})
}

const total = records.length
const start = (page - 1) * limit
const paginated = records.slice(start, start + limit)

return {
total,
data: paginated
}
}