Components
Loading preview...
Here is Pagination component
@SubframeApp
npx shadcn@latest add https://21st.dev/r/SubframeApp/paginationimport { useMemo, useState } from "react"
export default function Pagination() {
// Константы под пример
const totalItems = 120
const pageSize = 10
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize))
const [currentPage, setCurrentPage] = useState(1)
// Диапазон "Showing X–Y of Z"
const start = (currentPage - 1) * pageSize + 1
const end = Math.min(currentPage * pageSize, totalItems)
// Утилита генерации страниц с многоточиями
const getPageItems = (
total: number,
current: number,
siblingCount = 1
): (number | "...")[] => {
const firstPage = 1
const lastPage = total
const leftSibling = Math.max(current - siblingCount, firstPage)
const rightSibling = Math.min(current + siblingCount, lastPage)
const items: (number | "...")[] = []
// Всегда показываем первую
items.push(firstPage)
// Левое многоточие
if (leftSibling > firstPage + 1) {
items.push("...")
}
// Средние страницы
for (let p = leftSibling; p <= rightSibling; p++) {
if (p !== firstPage && p !== lastPage) {
items.push(p)
}
}
// Правое многоточие
if (rightSibling < lastPage - 1) {
items.push("...")
}
// Всегда показываем последнюю (если не совпадает с первой)
if (lastPage !== firstPage) {
items.push(lastPage)
}
return items
}
const pages = useMemo(
() => getPageItems(totalPages, currentPage, 1),
[totalPages, currentPage]
)
const goTo = (p: number) => setCurrentPage(Math.min(totalPages, Math.max(1, p)))
return (
<div className="max-w-screen-xl mx-auto mt-12 px-4 text-gray-600 md:px-8">
{/* Desktop */}
<div className="hidden justify-between text-sm md:flex" aria-label="Pagination">
<div>Showing {start}-{end} of {totalItems}</div>
<div className="flex items-center gap-8">
<button
type="button"
onClick={() => goTo(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-1.5 rounded-md duration-150 hover:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed"
>
Previous
</button>
<ul className="flex items-center gap-1" role="list">
{pages.map((item, idx) => (
<li key={`${item}-${idx}`}>
{item === "..." ? (
<span className="px-2 text-gray-400 select-none">…</span>
) : (
<button
type="button"
onClick={() => goTo(item as number)}
aria-current={currentPage === item ? "page" : undefined}
className={`px-2.5 py-1.5 rounded-md duration-150 ${
currentPage === item
? "bg-indigo-600 text-white font-medium"
: "hover:bg-indigo-600 hover:text-white"
}`}
>
{item}
</button>
)}
</li>
))}
</ul>
<button
type="button"
onClick={() => goTo(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-1.5 rounded-md duration-150 hover:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
{/* Mobile */}
<div className="flex items-center justify-between text-sm text-gray-600 font-medium md:hidden">
<button
type="button"
onClick={() => goTo(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-1.5 border rounded-md duration-150 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed"
>
Previous
</button>
{/* добавлены px-3 для симметричных отступов */}
<div className="px-3 font-medium">
Showing {start}-{end} of {totalItems}
</div>
<button
type="button"
onClick={() => goTo(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-1.5 border rounded-md duration-150 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)
}