برنامه نویسی

مدیریت API Next.js: پیاده سازی ویژگی های جستجو، صفحه بندی، فیلتر، مرتب سازی و محدودیت

Summarize this content to 400 words in Persian Lang
در این مقاله، می‌خواهم نحوه مدیریت APIها را در a به اشتراک بگذارم Next.js کاربرد، از جمله واکشی داده‌ها، نمایش آن‌ها و اجرای ویژگی‌هایی مانند جستجو، صفحه‌بندی، فیلتر کردن، مرتب‌سازی و محدود کردن نتایج. بیایید مستقیماً وارد جزئیات شویم.

الزامات:

Next.js 14.2.18
Axios 1.7.8
از debounce 10.0.4 استفاده کنید
API، مطمئن شوید که API مورد استفاده شما از ویژگی‌هایی مانند جستجو، صفحه‌بندی، فیلتر کردن، مرتب‌سازی و محدود کردن داده‌ها پشتیبانی می‌کند. در این مقاله، استفاده از API Jikan را نشان می‌دهم که داده‌هایی را برای انیمه و محتوای مرتبط ارائه می‌کند.

راه اندازی پروژه

Next.js و Required Packages را نصب کنید
ایجاد ساختار فایل و پوشه پروژه خود را با ساختار زیر سازماندهی کنید:

واکشی و نمایش داده ها

//src/lib/actions.ts

import axios from ‘axios’;

const axiosInstance = axios.create({
baseURL: ‘https://api.jikan.moe/v4’,
});

export const getAllAnime = async (
query?: string,
currentPage?: number,
orderBy?: string,
type?: string,
status?: string,
sort?: string,
limit?: number
) => {
try {
const res = await axiosInstance.get(
`/anime?q=${query}&page=${currentPage}&order_by=${orderBy}&type=${type}&status=${status}&sort=${sort}&limit=${limit}`
);

const totalPages = res.data.pagination.last_visible_page;

return {
data: res.data.data,
totalPages,
};
} catch (error) {
console.log(error);
return null;
}
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

//src/components/AnimeList.tsx

import { getAllAnime } from ‘@/lib/actions’;
import Image from ‘next/image’;

type AnimeProps = {
mal_id: number;
images: {
webp: {
image_url: string;
};
};
title: string;
};

export default async function AnimeList({
query,
currentPage,
orderBy,
type,
status,
sort,
limit,
}: {
query?: string;
currentPage?: number;
orderBy?: string;
type?: string;
status?: string;
sort?: string;
limit?: number;
}) {
const animes = await getAllAnime(
query,
currentPage,
orderBy,
type,
status,
sort,
limit
);

return (
<>
<div className=”grid grid-cols-6 gap-4 pt-4″>
{animes?.data.map((anime: AnimeProps) => (
<div key={anime.mal_id}>
<Image
src={anime.images.webp.image_url}
alt={anime.title}
width={500}
height={500}
className=”w-full h-[250px] object-cover rounded-md”
/>
<h3>
{anime.title.length > 15
? `${anime.title.slice(0, 15)}…`
: anime.title}
h3>
div>
))}
div>
>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جستجو را پیاده سازی کنید

//src/components/Search.tsx

‘use client’;

import { usePathname, useRouter, useSearchParams } from ‘next/navigation’;
import { useDebouncedCallback } from ‘use-debounce’;

export default function Search() {
const pathname = usePathname();
const { replace } = useRouter();
const searchParams = useSearchParams();

const handleSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParams);
params.set(‘page’, ‘1’);
if (term) {
params.set(‘query’, term);
} else {
params.delete(‘query’);
}
replace(`${pathname}?${params.toString()}`);
}, 500);

return (
<div className=”w-[50%]”>
<input
type=”text”
placeholder=”Search…”
className=”w-full bg-neutral-200 px-2 py-1 rounded-md”
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get(‘query’)?.toString()}
/>
div>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پیاده سازی صفحه بندی

//src/components/Pagination.tsx

‘use client’;

import { usePathname, useRouter, useSearchParams } from ‘next/navigation’;

export default function Pagination({ totalPages }: { totalPages: number }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get(‘page’)) || 1;
const router = useRouter();

const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set(‘page’, pageNumber.toString());
return `${pathname}?${params.toString()}`;
};

return (
<>
<div className=”w-full flex items-center justify-center gap-x-8″>
<button
onClick={() => router.push(createPageURL(currentPage – 1))}
disabled={currentPage === 1}
className=”bg-neutral-600 px-2 py-1 rounded-md text-neutral-100 disabled:bg-neutral-400 disabled:cursor-not-allowed”
>
Prev
button>
<p className=”text-[1rem] text-neutral-700″>
Page {currentPage} of {totalPages}
p>
<button
onClick={() => router.push(createPageURL(currentPage + 1))}
disabled={currentPage === totalPages}
className=”bg-neutral-600 px-2 py-1 rounded-md text-neutral-100 disabled:bg-neutral-400 disabled:cursor-not-allowed”
>
Next
button>
div>
>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پیاده سازی فیلتر و مرتب سازی

//src/components/FilterByType.tsx

‘use client’;

import { usePathname, useRouter, useSearchParams } from ‘next/navigation’;

type TypeProps = { value: string; name: string };

const TYPE = [
{ value: ‘tv’, name: ‘TV’ },
{ value: ‘movie’, name: ‘Movie’ },
{ value: ‘ova’, name: ‘OVA’ },
{ value: ‘special’, name: ‘Special’ },
{ value: ‘ona’, name: ‘ONA’ },
{ value: ‘music’, name: ‘Music’ },
{ value: ‘cm’, name: ‘CM’ },
{ value: ‘pv’, name: ‘PV’ },
{ value: ‘tv_special’, name: ‘TV Special’ },
];

export default function FilterByType() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleFilterByType = (type: string) => {
const params = new URLSearchParams(searchParams);
params.set(‘page’, ‘1’);
params.set(‘type’, type);
router.replace(`${pathname}?${params.toString()}`);
};

return (
<>
<form className=”w-[10%] bg-neutral-400 px-2 py-1 rounded-md”>
<select
className=”bg-neutral-400 w-full”
onChange={(e) => handleFilterByType(e.target.value)}
defaultValue={searchParams.get(‘type’)?.toString()}
>
<option value={”} defaultValue={”}>
Type:
option>
{TYPE.map((type: TypeProps, index) => (
<option key={index} value={type.value}>
{type.name}
option>
))}
select>
form>
>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

//src/components/FilterByStatus.tsx

‘use client’;

import { usePathname, useRouter, useSearchParams } from ‘next/navigation’;

type StatusProps = { value: string; name: string };

const STATUS = [
{ value: ‘airing’, name: ‘Airing’ },
{ value: ‘upcoming’, name: ‘Upcoming’ },
{ value: ‘complete’, name: ‘Complete’ },
];

export default function FilterByStatus() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleFilterByStatus = (status: string) => {
const params = new URLSearchParams(searchParams);
params.set(‘page’, ‘1’);
params.set(‘status’, status);
router.replace(`${pathname}?${params.toString()}`);
};
return (
<>
<form className=”w-[10%] bg-neutral-400 px-2 py-1 rounded-md”>
<select
className=”bg-neutral-400 w-full”
onChange={(e) => handleFilterByStatus(e.target.value)}
defaultValue={searchParams.get(‘status’)?.toString()}
>
<option value={”} defaultValue={”}>
Status:
option>
{STATUS.map((status: StatusProps, index) => (
<option key={index} value={status.value}>
{status.name}
option>
))}
select>
form>
>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

//src/components/SortByOrder

‘use client’;

import { usePathname, useRouter, useSearchParams } from ‘next/navigation’;

type OrderByProps = { value: string; name: string };

const ORDER_BY = [
{ value: ‘mal_id’, name: ‘Mal id’ },
{ value: ‘title’, name: ‘Title’ },
{ value: ‘start_date’, name: ‘Start date’ },
{ value: ‘end_date’, name: ‘End date’ },
{ value: ‘episodes’, name: ‘Episodes’ },
{ value: ‘score’, name: ‘Score’ },
{ value: ‘scored_by’, name: ‘Scored by’ },
{ value: ‘rank’, name: ‘Rank’ },
{ value: ‘popularity’, name: ‘Popularity’ },
{ value: ‘members’, name: ‘Members’ },
{ value: ‘favorites’, name: ‘Favorites’ },
];

export default function SortByOrder() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleSortByOrder = (orderBy: string) => {
const params = new URLSearchParams(searchParams);
params.set(‘page’, ‘1’);
params.set(‘order_by’, orderBy);
router.replace(`${pathname}?${params.toString()}`);
};
return (
<>
<form className=”w-[10%] bg-neutral-400 px-2 py-1 rounded-md”>
<select
className=”bg-neutral-400 w-full”
onChange={(e) => handleSortByOrder(e.target.value)}
defaultValue={searchParams.get(‘order_by’)?.toString()}
>
<option value={”} defaultValue={”}>
Order by:
option>
{ORDER_BY.map((orderBy: OrderByProps, index: number) => (
<option key={index} value={orderBy.value}>
{orderBy.name}
option>
))}
select>
form>
>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

//src/components/SortDirection.tsx

‘use client’;

import { usePathname, useRouter, useSearchParams } from ‘next/navigation’;

type SortDirectionProps = { value: string; name: string };

const SORT_DIRECTION = [
{ value: ‘asc’, name: ‘Ascending’ },
{ value: ‘desc’, name: ‘Descending’ },
];

export default function SortDirection() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleSortDirection = (sort: string) => {
const params = new URLSearchParams(searchParams);
params.set(‘page’, ‘1’);
params.set(‘sort’, sort);
router.replace(`${pathname}?${params.toString()}`);
};
return (
<>
<form className=”w-[10%] bg-neutral-400 px-2 py-1 rounded-md”>
<select
className=”bg-neutral-400 w-full”
onChange={(e) => handleSortDirection(e.target.value)}
defaultValue={searchParams.get(‘sort’)?.toString()}
>
<option value={”} defaultValue={”}>
Sort:
option>
{SORT_DIRECTION.map((orderBy: SortDirectionProps, index: number) => (
<option key={index} value={orderBy.value}>
{orderBy.name}
option>
))}
select>
form>
>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

محدود کردن نتایج

//src/components/Limit.tsx

‘use client’;

import { usePathname, useRouter, useSearchParams } from ‘next/navigation’;

export default function Limit() {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleLimitChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const params = new URLSearchParams(searchParams);

if (!isNaN(Number(value)) && Number(value) > 0) {
params.set(‘page’, ‘1’);
params.set(‘limit’, value);
router.replace(`${pathname}?${params.toString()}`);
}

if (
Number(value) === 0 ||
isNaN(Number(value)) ||
value === ” ||
Number(value) > 25
) {
params.delete(‘limit’);
router.replace(`${pathname}?${params.toString()}`);
}
};

return (
<form className=”w-[10%] bg-neutral-400 px-2 py-1 rounded-md flex items-center gap-x-2″>
<label htmlFor=”limit”>Limit:label>
<input
id=”limit”
type=”string”
className=”bg-neutral-400 w-full placeholder-neutral-700″
min=”1″
placeholder=”1-25″
defaultValue={searchParams.get(‘limit’) || ”}
onChange={handleLimitChange}
/>
form>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

صفحه اصلی

//src/app/page

import AnimeList from ‘@/components/AnimeList’;
import FilterByStatus from ‘@/components/FilterByStatus’;
import FilterByType from ‘@/components/FilterByType’;
import Limit from ‘@/components/Limit’;
import Pagination from ‘@/components/Pagination’;
import Search from ‘@/components/Search’;
import SortByOrder from ‘@/components/SortByOrder’;
import SortDirection from ‘@/components/SortDirection’;
import { getAllAnime } from ‘@/lib/actions’;
import { Suspense } from ‘react’;

export default async function Home({
searchParams,
}: {
searchParams?: {
query?: string;
page?: string;
order_by?: string;
type?: string;
status?: string;
sort?: string;
limit?: string;
};
}) {
const query = searchParams?.query || ”;
const currentPage = Number(searchParams?.page) || 1;
const orderBy = searchParams?.order_by || ”;
const type = searchParams?.type || ”;
const status = searchParams?.status || ”;
const sort = searchParams?.sort || ”;
const limit = Number(searchParams?.limit) || 25;

const pages = await getAllAnime(
query,
currentPage,
orderBy,
type,
status,
sort,
limit
);
const totalPages = pages?.totalPages;

return (
<main className=”w-[1020px] mx-auto”>
<div className=”flex items-center gap-x-4 py-4″>
<Search />
<FilterByType />
<FilterByStatus />
<SortByOrder />
<SortDirection />
<Limit />
div>
<Pagination totalPages={totalPages} />
<Suspense
key={query + currentPage + orderBy + type + status + sort + limit}
fallback={<div>Loading…div>}
>
<AnimeList
query={query}
currentPage={currentPage}
orderBy={orderBy}
type={type}
status={status}
sort={sort}
limit={limit}
/>
Suspense>
main>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اجزای Refactor

برای اینکه کد را مختصرتر و قابل استفاده‌تر کنیم، می‌توانیم فیلتر را اصلاح کنیم و اجزا را مرتب کنیم تا افزونگی را کاهش دهیم. استفاده کنید FilterSelect.tsx مؤلفه ای که می تواند هم عملکرد فیلتر کردن و هم مرتب سازی را انجام دهد. در اینجا نحوه بازسازی اجزای خود آورده شده است:

//src/components/FilterSelect.tsx

‘use client’;

import { usePathname, useRouter, useSearchParams } from ‘next/navigation’;
import React from ‘react’;

type FilterSelectProps = {
options: { value: string; name: string }[];
paramKey: string;
label: string;
};

const FilterSelect: React.FC<FilterSelectProps> = ({
options,
paramKey,
label,
}) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const router = useRouter();

const handleChange = (value: string) => {
const params = new URLSearchParams(searchParams);
params.set(‘page’, ‘1’);
params.set(paramKey, value);
router.replace(`${pathname}?${params.toString()}`);
};

return (
<form className=”w-[10%] bg-neutral-400 px-2 py-1 rounded-md”>
<select
className=”bg-neutral-400 w-full”
onChange={(e) => handleChange(e.target.value)}
defaultValue={searchParams.get(paramKey) || ”}
>
<option value=””>{label}:option>
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.name}
option>
))}
select>
form>
);
};

const STATUS = [
{ value: ‘airing’, name: ‘Airing’ },
{ value: ‘upcoming’, name: ‘Upcoming’ },
{ value: ‘complete’, name: ‘Complete’ },
];

const TYPE = [
{ value: ‘tv’, name: ‘TV’ },
{ value: ‘movie’, name: ‘Movie’ },
{ value: ‘ova’, name: ‘OVA’ },
{ value: ‘special’, name: ‘Special’ },
{ value: ‘ona’, name: ‘ONA’ },
{ value: ‘music’, name: ‘Music’ },
{ value: ‘cm’, name: ‘CM’ },
{ value: ‘pv’, name: ‘PV’ },
{ value: ‘tv_special’, name: ‘TV Special’ },
];

const ORDER_BY = [
{ value: ‘mal_id’, name: ‘Mal id’ },
{ value: ‘title’, name: ‘Title’ },
{ value: ‘start_date’, name: ‘Start date’ },
{ value: ‘end_date’, name: ‘End date’ },
{ value: ‘episodes’, name: ‘Episodes’ },
{ value: ‘score’, name: ‘Score’ },
{ value: ‘scored_by’, name: ‘Scored by’ },
{ value: ‘rank’, name: ‘Rank’ },
{ value: ‘popularity’, name: ‘Popularity’ },
{ value: ‘members’, name: ‘Members’ },
{ value: ‘favorites’, name: ‘Favorites’ },
];

const SORT_DIRECTION = [
{ value: ‘asc’, name: ‘Ascending’ },
{ value: ‘desc’, name: ‘Descending’ },
];

export function FilterByStatus() {
return <FilterSelect options={STATUS} paramKey=”status” label=”Status” />;
}

export function FilterByType() {
return <FilterSelect options={TYPE} paramKey=”type” label=”Type” />;
}

export function SortByOrder() {
return (
<FilterSelect options={ORDER_BY} paramKey=”order_by” label=”Order by” />
);
}

export function SortDirection() {
return <FilterSelect options={SORT_DIRECTION} paramKey=”sort” label=”Sort” />;
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

صفحه اصلی را تغییر دهید و فیلتر را وارد کنید و اجزای آن را مرتب کنید FilterSelect

توضیح

usePathname، useSearchParams، و useRouterHooksاین قلاب‌ها از next/navigation برای مدیریت وضعیت‌های URL ضروری هستند:

usePathname: مسیر فعلی URL را بازیابی می کند.

useSearchParams: به پارامترهای پرس و جو دسترسی و دستکاری می کند.

useRouter: توابع مسیریابی را برای پیمایش یا به‌روزرسانی برنامه‌ای URLها (جایگزین یا فشار) ارائه می‌کند.

برگشت به تماس در مؤلفه جستجورا useDebouncedCallback تابع اجرای درخواست جستجو را به تاخیر می اندازد:

با انتظار برای مدت زمان مشخص (500 میلی ثانیه) از تماس های بیش از حد API جلوگیری می کند.
عملکرد بهتر و تجربه کاربری روان تر را تضمین می کند.

به روز رسانی پویا URLبه‌روزرسانی‌های پویا برای پارامترهای URL، ناوبری روان و در سمت مشتری را تضمین می‌کند:

replace() URL را بدون بارگیری مجدد صفحه به روز می کند.
تاریخچه مرورگر را در حالی که منعکس می کند، تمیز نگه می دارد filter، sort، و pagination تغییر می کند.

کامپوننت AnimeList و واکشی داده هاجزء AnimeList شامل موارد زیر است:

واکشی داده های ناهمزمان از API Jikan (getAllAnime()).

کامپوننت FilterSelect قابل استفاده مجددرا FilterSelectجزء:

عملکرد کشویی چکیده برای فیلترهایی مانند status، type، order_by، و sort.
کد را با کاهش افزونگی در سراسر ساده می کند filterو sortاجزاء

مدیریت صفحه بندیجزء صفحه بندی:

به صورت پویا لینک های ناوبری (Prev و Next) بر اساس currentPage و totalPages.
دکمه ها را هنگام رسیدن به صفحه اول یا آخر غیرفعال می کند.

اعتبار سنجی ورودی را محدود کنیدمؤلفه Limit ورودی کاربر را برای محدود کردن تعداد نتایج تأیید می کند:

اطمینان حاصل می کند که مقدار بین 1 و 25 است.
URL را به صورت پویا به روز می کند یا اگر ورودی نامعتبر باشد، بازنشانی می شود.

تعلیق برای واکشی داده هاجزء Suspense برای مدیریت رندر ناهمزمان استفاده می شود:

نشانگر بارگیری مجدد را در حین انتظار برای داده نشان می دهد.
اطمینان حاصل می کند که UI در طول واکشی داده ها پاسخگو باقی می ماند.

و این برنامه به این صورت است:

نتیجه گیریامیدوارم این مقاله راهنمای جامعی در مورد نحوه پیاده‌سازی ویژگی‌های ضروری ارائه کرده باشد جستجو کنید، صفحه بندی، فیلتر کردن، مرتب سازی، و محدود کردن نتیجه در یک Next.js کاربرد.

برای کاوش، شبیه سازی، یا مشارکت در پروژه در github راحت باشید. من مشتاقانه منتظر نظرات شما هستم و امیدوارم این پروژه به شما در سفر توسعه شما کمک کند!

مراجع:https://nextjs.org/learn/dashboard-app/adding-search-and-paginationhttps://nextjs.org/docs/app/api-reference/functions/use-search-paramshttps://docs.api.jikan.moe/

در این مقاله، می‌خواهم نحوه مدیریت APIها را در a به اشتراک بگذارم Next.js کاربرد، از جمله واکشی داده‌ها، نمایش آن‌ها و اجرای ویژگی‌هایی مانند جستجو، صفحه‌بندی، فیلتر کردن، مرتب‌سازی و محدود کردن نتایج. بیایید مستقیماً وارد جزئیات شویم.

الزامات:

  • Next.js 14.2.18
  • Axios 1.7.8
  • از debounce 10.0.4 استفاده کنید
  • API، مطمئن شوید که API مورد استفاده شما از ویژگی‌هایی مانند جستجو، صفحه‌بندی، فیلتر کردن، مرتب‌سازی و محدود کردن داده‌ها پشتیبانی می‌کند. در این مقاله، استفاده از API Jikan را نشان می‌دهم که داده‌هایی را برای انیمه و محتوای مرتبط ارائه می‌کند.

راه اندازی پروژه

  1. Next.js و Required Packages را نصب کنید
  2. ایجاد ساختار فایل و پوشه پروژه خود را با ساختار زیر سازماندهی کنید:

ساختار پروژه

واکشی و نمایش داده ها

//src/lib/actions.ts

import axios from 'axios';

const axiosInstance = axios.create({
  baseURL: 'https://api.jikan.moe/v4',
});

export const getAllAnime = async (
  query?: string,
  currentPage?: number,
  orderBy?: string,
  type?: string,
  status?: string,
  sort?: string,
  limit?: number
) => {
  try {
    const res = await axiosInstance.get(
      `/anime?q=${query}&page=${currentPage}&order_by=${orderBy}&type=${type}&status=${status}&sort=${sort}&limit=${limit}`
    );

    const totalPages = res.data.pagination.last_visible_page;

    return {
      data: res.data.data,
      totalPages,
    };
  } catch (error) {
    console.log(error);
    return null;
  }
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

//src/components/AnimeList.tsx

import { getAllAnime } from '@/lib/actions';
import Image from 'next/image';

type AnimeProps = {
  mal_id: number;
  images: {
    webp: {
      image_url: string;
    };
  };
  title: string;
};

export default async function AnimeList({
  query,
  currentPage,
  orderBy,
  type,
  status,
  sort,
  limit,
}: {
  query?: string;
  currentPage?: number;
  orderBy?: string;
  type?: string;
  status?: string;
  sort?: string;
  limit?: number;
}) {
  const animes = await getAllAnime(
    query,
    currentPage,
    orderBy,
    type,
    status,
    sort,
    limit
  );

  return (
    <>
      <div className="grid grid-cols-6 gap-4 pt-4">
        {animes?.data.map((anime: AnimeProps) => (
          <div key={anime.mal_id}>
            <Image
              src={anime.images.webp.image_url}
              alt={anime.title}
              width={500}
              height={500}
              className="w-full h-[250px] object-cover rounded-md"
            />
            <h3>
              {anime.title.length > 15
                ? `${anime.title.slice(0, 15)}...`
                : anime.title}
            h3>
          div>
        ))}
      div>
    >
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جستجو را پیاده سازی کنید

//src/components/Search.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';

export default function Search() {
  const pathname = usePathname();
  const { replace } = useRouter();
  const searchParams = useSearchParams();

  const handleSearch = useDebouncedCallback((term: string) => {
    const params = new URLSearchParams(searchParams);
    params.set('page', '1');
    if (term) {
      params.set('query', term);
    } else {
      params.delete('query');
    }
    replace(`${pathname}?${params.toString()}`);
  }, 500);

  return (
    <div className="w-[50%]">
      <input
        type="text"
        placeholder="Search..."
        className="w-full bg-neutral-200 px-2 py-1 rounded-md"
        onChange={(e) => {
          handleSearch(e.target.value);
        }}
        defaultValue={searchParams.get('query')?.toString()}
      />
    div>
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پیاده سازی صفحه بندی

//src/components/Pagination.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

export default function Pagination({ totalPages }: { totalPages: number }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const currentPage = Number(searchParams.get('page')) || 1;
  const router = useRouter();

  const createPageURL = (pageNumber: number | string) => {
    const params = new URLSearchParams(searchParams);
    params.set('page', pageNumber.toString());
    return `${pathname}?${params.toString()}`;
  };

  return (
    <>
      <div className="w-full flex items-center justify-center gap-x-8">
        <button
          onClick={() => router.push(createPageURL(currentPage - 1))}
          disabled={currentPage === 1}
          className="bg-neutral-600 px-2 py-1 rounded-md text-neutral-100 disabled:bg-neutral-400 disabled:cursor-not-allowed"
        >
          Prev
        button>
        <p className="text-[1rem] text-neutral-700">
          Page {currentPage} of {totalPages}
        p>
        <button
          onClick={() => router.push(createPageURL(currentPage + 1))}
          disabled={currentPage === totalPages}
          className="bg-neutral-600 px-2 py-1 rounded-md text-neutral-100 disabled:bg-neutral-400 disabled:cursor-not-allowed"
        >
          Next
        button>
      div>
    >
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پیاده سازی فیلتر و مرتب سازی

//src/components/FilterByType.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

type TypeProps = { value: string; name: string };

const TYPE = [
  { value: 'tv', name: 'TV' },
  { value: 'movie', name: 'Movie' },
  { value: 'ova', name: 'OVA' },
  { value: 'special', name: 'Special' },
  { value: 'ona', name: 'ONA' },
  { value: 'music', name: 'Music' },
  { value: 'cm', name: 'CM' },
  { value: 'pv', name: 'PV' },
  { value: 'tv_special', name: 'TV Special' },
];

export default function FilterByType() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const router = useRouter();

  const handleFilterByType = (type: string) => {
    const params = new URLSearchParams(searchParams);
    params.set('page', '1');
    params.set('type', type);
    router.replace(`${pathname}?${params.toString()}`);
  };

  return (
    <>
      <form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
        <select
          className="bg-neutral-400 w-full"
          onChange={(e) => handleFilterByType(e.target.value)}
          defaultValue={searchParams.get('type')?.toString()}
        >
          <option value={''} defaultValue={''}>
            Type:
          option>
          {TYPE.map((type: TypeProps, index) => (
            <option key={index} value={type.value}>
              {type.name}
            option>
          ))}
        select>
      form>
    >
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

//src/components/FilterByStatus.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

type StatusProps = { value: string; name: string };

const STATUS = [
  { value: 'airing', name: 'Airing' },
  { value: 'upcoming', name: 'Upcoming' },
  { value: 'complete', name: 'Complete' },
];

export default function FilterByStatus() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const router = useRouter();

  const handleFilterByStatus = (status: string) => {
    const params = new URLSearchParams(searchParams);
    params.set('page', '1');
    params.set('status', status);
    router.replace(`${pathname}?${params.toString()}`);
  };
  return (
    <>
      <form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
        <select
          className="bg-neutral-400 w-full"
          onChange={(e) => handleFilterByStatus(e.target.value)}
          defaultValue={searchParams.get('status')?.toString()}
        >
          <option value={''} defaultValue={''}>
            Status:
          option>
          {STATUS.map((status: StatusProps, index) => (
            <option key={index} value={status.value}>
              {status.name}
            option>
          ))}
        select>
      form>
    >
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

//src/components/SortByOrder

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

type OrderByProps = { value: string; name: string };

const ORDER_BY = [
  { value: 'mal_id', name: 'Mal id' },
  { value: 'title', name: 'Title' },
  { value: 'start_date', name: 'Start date' },
  { value: 'end_date', name: 'End date' },
  { value: 'episodes', name: 'Episodes' },
  { value: 'score', name: 'Score' },
  { value: 'scored_by', name: 'Scored by' },
  { value: 'rank', name: 'Rank' },
  { value: 'popularity', name: 'Popularity' },
  { value: 'members', name: 'Members' },
  { value: 'favorites', name: 'Favorites' },
];

export default function SortByOrder() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const router = useRouter();

  const handleSortByOrder = (orderBy: string) => {
    const params = new URLSearchParams(searchParams);
    params.set('page', '1');
    params.set('order_by', orderBy);
    router.replace(`${pathname}?${params.toString()}`);
  };
  return (
    <>
      <form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
        <select
          className="bg-neutral-400 w-full"
          onChange={(e) => handleSortByOrder(e.target.value)}
          defaultValue={searchParams.get('order_by')?.toString()}
        >
          <option value={''} defaultValue={''}>
            Order by:
          option>
          {ORDER_BY.map((orderBy: OrderByProps, index: number) => (
            <option key={index} value={orderBy.value}>
              {orderBy.name}
            option>
          ))}
        select>
      form>
    >
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

//src/components/SortDirection.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

type SortDirectionProps = { value: string; name: string };

const SORT_DIRECTION = [
  { value: 'asc', name: 'Ascending' },
  { value: 'desc', name: 'Descending' },
];

export default function SortDirection() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const router = useRouter();

  const handleSortDirection = (sort: string) => {
    const params = new URLSearchParams(searchParams);
    params.set('page', '1');
    params.set('sort', sort);
    router.replace(`${pathname}?${params.toString()}`);
  };
  return (
    <>
      <form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
        <select
          className="bg-neutral-400 w-full"
          onChange={(e) => handleSortDirection(e.target.value)}
          defaultValue={searchParams.get('sort')?.toString()}
        >
          <option value={''} defaultValue={''}>
            Sort:
          option>
          {SORT_DIRECTION.map((orderBy: SortDirectionProps, index: number) => (
            <option key={index} value={orderBy.value}>
              {orderBy.name}
            option>
          ))}
        select>
      form>
    >
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

محدود کردن نتایج

//src/components/Limit.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';

export default function Limit() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const router = useRouter();

  const handleLimitChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value;
    const params = new URLSearchParams(searchParams);

    if (!isNaN(Number(value)) && Number(value) > 0) {
      params.set('page', '1');
      params.set('limit', value);
      router.replace(`${pathname}?${params.toString()}`);
    }

    if (
      Number(value) === 0 ||
      isNaN(Number(value)) ||
      value === '' ||
      Number(value) > 25
    ) {
      params.delete('limit');
      router.replace(`${pathname}?${params.toString()}`);
    }
  };

  return (
    <form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md flex items-center gap-x-2">
      <label htmlFor="limit">Limit:label>
      <input
        id="limit"
        type="string"
        className="bg-neutral-400 w-full placeholder-neutral-700"
        min="1"
        placeholder="1-25"
        defaultValue={searchParams.get('limit') || ''}
        onChange={handleLimitChange}
      />
    form>
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

صفحه اصلی

//src/app/page

import AnimeList from '@/components/AnimeList';
import FilterByStatus from '@/components/FilterByStatus';
import FilterByType from '@/components/FilterByType';
import Limit from '@/components/Limit';
import Pagination from '@/components/Pagination';
import Search from '@/components/Search';
import SortByOrder from '@/components/SortByOrder';
import SortDirection from '@/components/SortDirection';
import { getAllAnime } from '@/lib/actions';
import { Suspense } from 'react';

export default async function Home({
  searchParams,
}: {
  searchParams?: {
    query?: string;
    page?: string;
    order_by?: string;
    type?: string;
    status?: string;
    sort?: string;
    limit?: string;
  };
}) {
  const query = searchParams?.query || '';
  const currentPage = Number(searchParams?.page) || 1;
  const orderBy = searchParams?.order_by || '';
  const type = searchParams?.type || '';
  const status = searchParams?.status || '';
  const sort = searchParams?.sort || '';
  const limit = Number(searchParams?.limit) || 25;

  const pages = await getAllAnime(
    query,
    currentPage,
    orderBy,
    type,
    status,
    sort,
    limit
  );
  const totalPages = pages?.totalPages;

  return (
    <main className="w-[1020px] mx-auto">
      <div className="flex items-center gap-x-4 py-4">
        <Search />
        <FilterByType />
        <FilterByStatus />
        <SortByOrder />
        <SortDirection />
        <Limit />
      div>
      <Pagination totalPages={totalPages} />
      <Suspense
        key={query + currentPage + orderBy + type + status + sort + limit}
        fallback={<div>Loading...div>}
      >
        <AnimeList
          query={query}
          currentPage={currentPage}
          orderBy={orderBy}
          type={type}
          status={status}
          sort={sort}
          limit={limit}
        />
      Suspense>
    main>
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اجزای Refactor

برای اینکه کد را مختصرتر و قابل استفاده‌تر کنیم، می‌توانیم فیلتر را اصلاح کنیم و اجزا را مرتب کنیم تا افزونگی را کاهش دهیم. استفاده کنید FilterSelect.tsx مؤلفه ای که می تواند هم عملکرد فیلتر کردن و هم مرتب سازی را انجام دهد. در اینجا نحوه بازسازی اجزای خود آورده شده است:

//src/components/FilterSelect.tsx

'use client';

import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React from 'react';

type FilterSelectProps = {
  options: { value: string; name: string }[];
  paramKey: string;
  label: string;
};

const FilterSelect: React.FC<FilterSelectProps> = ({
  options,
  paramKey,
  label,
}) => {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const router = useRouter();

  const handleChange = (value: string) => {
    const params = new URLSearchParams(searchParams);
    params.set('page', '1');
    params.set(paramKey, value);
    router.replace(`${pathname}?${params.toString()}`);
  };

  return (
    <form className="w-[10%] bg-neutral-400 px-2 py-1 rounded-md">
      <select
        className="bg-neutral-400 w-full"
        onChange={(e) => handleChange(e.target.value)}
        defaultValue={searchParams.get(paramKey) || ''}
      >
        <option value="">{label}:option>
        {options.map((option, index) => (
          <option key={index} value={option.value}>
            {option.name}
          option>
        ))}
      select>
    form>
  );
};

const STATUS = [
  { value: 'airing', name: 'Airing' },
  { value: 'upcoming', name: 'Upcoming' },
  { value: 'complete', name: 'Complete' },
];

const TYPE = [
  { value: 'tv', name: 'TV' },
  { value: 'movie', name: 'Movie' },
  { value: 'ova', name: 'OVA' },
  { value: 'special', name: 'Special' },
  { value: 'ona', name: 'ONA' },
  { value: 'music', name: 'Music' },
  { value: 'cm', name: 'CM' },
  { value: 'pv', name: 'PV' },
  { value: 'tv_special', name: 'TV Special' },
];

const ORDER_BY = [
  { value: 'mal_id', name: 'Mal id' },
  { value: 'title', name: 'Title' },
  { value: 'start_date', name: 'Start date' },
  { value: 'end_date', name: 'End date' },
  { value: 'episodes', name: 'Episodes' },
  { value: 'score', name: 'Score' },
  { value: 'scored_by', name: 'Scored by' },
  { value: 'rank', name: 'Rank' },
  { value: 'popularity', name: 'Popularity' },
  { value: 'members', name: 'Members' },
  { value: 'favorites', name: 'Favorites' },
];

const SORT_DIRECTION = [
  { value: 'asc', name: 'Ascending' },
  { value: 'desc', name: 'Descending' },
];

export function FilterByStatus() {
  return <FilterSelect options={STATUS} paramKey="status" label="Status" />;
}

export function FilterByType() {
  return <FilterSelect options={TYPE} paramKey="type" label="Type" />;
}

export function SortByOrder() {
  return (
    <FilterSelect options={ORDER_BY} paramKey="order_by" label="Order by" />
  );
}

export function SortDirection() {
  return <FilterSelect options={SORT_DIRECTION} paramKey="sort" label="Sort" />;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

صفحه اصلی را تغییر دهید و فیلتر را وارد کنید و اجزای آن را مرتب کنید FilterSelect

توضیح

usePathname، useSearchParams، و useRouterHooks
این قلاب‌ها از next/navigation برای مدیریت وضعیت‌های URL ضروری هستند:

  • usePathname: مسیر فعلی URL را بازیابی می کند.
  • useSearchParams: به پارامترهای پرس و جو دسترسی و دستکاری می کند.
  • useRouter: توابع مسیریابی را برای پیمایش یا به‌روزرسانی برنامه‌ای URLها (جایگزین یا فشار) ارائه می‌کند.

برگشت به تماس در مؤلفه جستجو
را useDebouncedCallback تابع اجرای درخواست جستجو را به تاخیر می اندازد:

  • با انتظار برای مدت زمان مشخص (500 میلی ثانیه) از تماس های بیش از حد API جلوگیری می کند.
  • عملکرد بهتر و تجربه کاربری روان تر را تضمین می کند.

به روز رسانی پویا URL
به‌روزرسانی‌های پویا برای پارامترهای URL، ناوبری روان و در سمت مشتری را تضمین می‌کند:

  • replace() URL را بدون بارگیری مجدد صفحه به روز می کند.
  • تاریخچه مرورگر را در حالی که منعکس می کند، تمیز نگه می دارد filter، sort، و pagination تغییر می کند.

کامپوننت AnimeList و واکشی داده ها
جزء AnimeList شامل موارد زیر است:

  • واکشی داده های ناهمزمان از API Jikan (getAllAnime()).

کامپوننت FilterSelect قابل استفاده مجدد
را FilterSelectجزء:

  • عملکرد کشویی چکیده برای فیلترهایی مانند status، type، order_by، و sort.
  • کد را با کاهش افزونگی در سراسر ساده می کند filterو sortاجزاء

مدیریت صفحه بندی
جزء صفحه بندی:

  • به صورت پویا لینک های ناوبری (Prev و Next) بر اساس currentPage و totalPages.
  • دکمه ها را هنگام رسیدن به صفحه اول یا آخر غیرفعال می کند.

اعتبار سنجی ورودی را محدود کنید
مؤلفه Limit ورودی کاربر را برای محدود کردن تعداد نتایج تأیید می کند:

  • اطمینان حاصل می کند که مقدار بین 1 و 25 است.
  • URL را به صورت پویا به روز می کند یا اگر ورودی نامعتبر باشد، بازنشانی می شود.

تعلیق برای واکشی داده ها
جزء Suspense برای مدیریت رندر ناهمزمان استفاده می شود:

  • نشانگر بارگیری مجدد را در حین انتظار برای داده نشان می دهد.
  • اطمینان حاصل می کند که UI در طول واکشی داده ها پاسخگو باقی می ماند.

و این برنامه به این صورت است:

برنامه شبیه 1 است

برنامه شبیه 2 است

نتیجه گیری
امیدوارم این مقاله راهنمای جامعی در مورد نحوه پیاده‌سازی ویژگی‌های ضروری ارائه کرده باشد جستجو کنید، صفحه بندی، فیلتر کردن، مرتب سازی، و محدود کردن نتیجه در یک Next.js کاربرد.

برای کاوش، شبیه سازی، یا مشارکت در پروژه در github راحت باشید. من مشتاقانه منتظر نظرات شما هستم و امیدوارم این پروژه به شما در سفر توسعه شما کمک کند!

مراجع:
https://nextjs.org/learn/dashboard-app/adding-search-and-pagination
https://nextjs.org/docs/app/api-reference/functions/use-search-params
https://docs.api.jikan.moe/

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا