diff --git a/app/dashboard/music/tracks/page.tsx b/app/dashboard/music/tracks/page.tsx index b75c540..8597adf 100644 --- a/app/dashboard/music/tracks/page.tsx +++ b/app/dashboard/music/tracks/page.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { apiFetch, ApiError, toFormData } from "@/lib/api"; -import { useList } from "@/lib/useResource"; +import { useList, usePaginated } from "@/lib/useResource"; import { useToast } from "@/components/toast"; import { DataTable, type Column } from "@/components/DataTable"; import { @@ -13,6 +13,7 @@ import { Select, Modal, PageHeader, + Pagination, } from "@/components/ui"; import { EditIcon, PlusIcon, TrashIcon } from "@/components/icons"; import { formatMinutes, fileNameToTitle } from "@/lib/utils"; @@ -45,7 +46,8 @@ interface Track { } export default function MusicTracksPage() { - const { data, loading, error, reload } = useList("/music"); + const { data, meta, page, setPage, loading, error, reload } = + usePaginated("/music", { perPage: 20 }); const { data: playlists } = useList("/music-playlists"); const toast = useToast(); @@ -256,6 +258,15 @@ export default function MusicTracksPage() { )} /> + {meta && ( + + )} + setOpen(false)} diff --git a/components/ui.tsx b/components/ui.tsx index a849d3a..da024bb 100644 --- a/components/ui.tsx +++ b/components/ui.tsx @@ -8,7 +8,7 @@ import { type TextareaHTMLAttributes, useEffect, } from "react"; -import { cn } from "@/lib/utils"; +import { cn, toFa } from "@/lib/utils"; import { CloseIcon, SpinnerIcon } from "./icons"; /* ------------------------------- Button -------------------------------- */ @@ -192,6 +192,44 @@ export function Badge({ ); } +export function Pagination({ + page, + lastPage, + total, + onChange, +}: { + page: number; + lastPage: number; + total?: number; + onChange: (p: number) => void; +}) { + if (lastPage <= 1) return null; + return ( +
+ + {total != null && `${toFa(total)} مورد · `} + صفحه {toFa(page)} از {toFa(lastPage)} + +
+ + +
+
+ ); +} + export function EmptyState({ title, message, diff --git a/lib/useResource.ts b/lib/useResource.ts index e9dc06b..c9fa348 100644 --- a/lib/useResource.ts +++ b/lib/useResource.ts @@ -51,6 +51,73 @@ export function useList( return { data, loading, error, reload }; } +export interface PageMeta { + current_page: number; + last_page: number; + per_page: number; + total: number; +} + +// Paginated GET-list hook for Laravel paginator responses +// ({ data, current_page, last_page, per_page, total }). +export function usePaginated( + path: string | null, + options?: { perPage?: number; query?: Query }, +): { + data: T[]; + meta: PageMeta | null; + page: number; + setPage: (p: number) => void; + loading: boolean; + error: string | null; + reload: () => void; +} { + const perPage = options?.perPage ?? 20; + const query = options?.query; + const [page, setPage] = useState(1); + const [data, setData] = useState([]); + const [meta, setMeta] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const queryKey = JSON.stringify(query ?? {}); + + const reload = useCallback(() => { + if (!path) { + setData([]); + setMeta(null); + setLoading(false); + setError(null); + return; + } + setLoading(true); + setError(null); + apiFetch(path, { query: { ...query, page, per_page: perPage } }) + .then((res) => { + const r = (res ?? {}) as Record; + const rows = Array.isArray(r.data) ? (r.data as T[]) : []; + setData(rows); + setMeta({ + current_page: Number(r.current_page ?? page), + last_page: Number(r.last_page ?? 1), + per_page: Number(r.per_page ?? perPage), + total: Number(r.total ?? rows.length), + }); + }) + .catch((e: unknown) => { + setError(e instanceof ApiError ? e.message : "خطا در دریافت اطلاعات"); + setData([]); + }) + .finally(() => setLoading(false)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [path, page, perPage, queryKey]); + + useEffect(() => { + reload(); + }, [reload]); + + return { data, meta, page, setPage, loading, error, reload }; +} + // Single-record GET hook. export function useItem(path: string | null): { data: T | null;