Skip to content
Open
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
141 changes: 141 additions & 0 deletions app/components/ui/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'use client';

import React, { useRef, useEffect, useState } from 'react';
import { motion } from 'framer-motion';

interface PaginationProps {
totalPages: number;
currentPage: number;
onPageChange: (page: number) => void;
className?: string;
maxVisiblePages?: number;
}

export default function Pagination({
totalPages,
currentPage,
onPageChange,
className = '',
maxVisiblePages = 7,
}: PaginationProps) {
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
const [underlineStyle, setUnderlineStyle] = useState<{ left: number; width: number }>({
left: 0,
width: 0,
});
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
setIsMounted(true);
}, []);

useEffect(() => {
// Reset refs array when total pages change
buttonRefs.current = buttonRefs.current.slice(0, totalPages);
}, [totalPages]);

useEffect(() => {
if (!isMounted) return;
const currentBtn = buttonRefs.current[currentPage - 1];
if (currentBtn && currentBtn.parentElement) {
const rect = currentBtn.getBoundingClientRect();
const parentRect = currentBtn.parentElement.getBoundingClientRect();
setUnderlineStyle({
left: rect.left - parentRect.left,
width: rect.width,
});
}
}, [currentPage, totalPages, isMounted]);

const generatePages = () => {
if (totalPages <= maxVisiblePages) return Array.from({ length: totalPages }, (_, i) => i + 1);

const pages: (number | -1)[] = [];
const first = 1;
const last = totalPages;
const sideCount = 1;
const middleCount = maxVisiblePages - 2 * sideCount - 2;

pages.push(first);

let left = Math.max(currentPage - Math.floor(middleCount / 2), sideCount + 1);
let right = Math.min(currentPage + Math.floor(middleCount / 2), totalPages - sideCount);

if (left > sideCount + 1) pages.push(-1);
else left = sideCount + 1;

for (let i = left; i <= right; i++) pages.push(i);

if (right < totalPages - sideCount) pages.push(-1);

pages.push(last);

return pages;
};

if (totalPages <= 1) return null;

const pagesToShow = generatePages();

return (
<div className={`flex items-center justify-center space-x-2 mt-8 mb-4 ${className}`}>
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="p-2 rounded-lg text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white disabled:opacity-30 disabled:hover:text-black/70 dark:disabled:hover:text-white/70 transition-colors"
aria-label="Previous Page"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m15 18-6-6 6-6"/>
</svg>
</button>

<div className="relative inline-flex items-center gap-1">
{pagesToShow.map((pageNum, i) =>
pageNum === -1 ? (
<span key={`dots-${i}`} className="px-2 text-black/40 dark:text-white/40">…</span>
) : (
<button
key={pageNum}
ref={(el) => {
if (el) buttonRefs.current[pageNum - 1] = el;
}}
onClick={() => onPageChange(pageNum)}
className={`relative z-10 px-4 py-2 text-sm transition-colors rounded-lg ${
pageNum === currentPage
? 'text-black font-semibold'
: 'text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5'
}`}
>
{pageNum}
</button>
)
)}

{isMounted && (
<motion.div
layout
initial={false}
animate={{
left: underlineStyle.left,
width: underlineStyle.width,
}}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
className="absolute h-full bg-primary rounded-lg z-0"
/>
)}
</div>

<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="p-2 rounded-lg text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white disabled:opacity-30 disabled:hover:text-black/70 dark:disabled:hover:text-white/70 transition-colors"
aria-label="Next Page"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m9 18 6-6-6-6"/>
</svg>
</button>
</div>
);
}
41 changes: 36 additions & 5 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import ConvertXIcon from './components/theme/Icon/convertXIcon';
import GenieIcon from './components/theme/Icon/genieIcon';
import DevUtilsIcon from './components/theme/Icon/devUtilsIcon';
import { useTheme } from './contexts/themeContext';
import Pagination from './components/ui/Pagination';
import { motion } from 'framer-motion';

const CATEGORY_GROUPS = [
'Text Lab',
Expand Down Expand Up @@ -134,6 +136,11 @@ const Page = () => {
const [selectedBasis, setSelectedBasis] = useState<BasisType>('All');
const [favorites, setFavorites] = useState<string[]>([]);
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
const [currentPage, setCurrentPage] = useState(1);

useEffect(() => {
setCurrentPage(1);
}, [selectedCategory, selectedBasis, showFavoritesOnly]);

useEffect(() => {
try {
Expand Down Expand Up @@ -178,11 +185,13 @@ const Page = () => {
setValue('txtSearch', '');
setSearchTerm('');
setIsSearch(false);
setCurrentPage(1);
};

const handleSearchChange = (event: any) => {
setSearchTerm(event.target.value);
setIsSearch(event.target.value.length > 0);
setCurrentPage(1);
};

const allItems = Object.entries(
Expand Down Expand Up @@ -243,6 +252,13 @@ const Page = () => {
{} as Record<BasisType, number>
);

const pageSize = isSearch ? 9 : 30;
const totalPages = Math.ceil(filteredItems.length / pageSize);
const paginatedItems = filteredItems.slice(
(currentPage - 1) * pageSize,
currentPage * pageSize
);

return (
<>
<SEOComponent
Expand Down Expand Up @@ -485,9 +501,16 @@ const Page = () => {
<span className="mt-2">No tools found</span>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredItems.map((item: any, index: number) => (
<Link
<>
<motion.div
key={`${currentPage}-${selectedCategory}-${selectedBasis}-${searchTerm}-${showFavoritesOnly}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"
>
{paginatedItems.map((item: any, index: number) => (
<Link
key={index}
href={`${item?.url}`}
className={`bg-white/5 rounded-lg p-8 w-full ${DevelopmentToolsStyles.contentCardHoverEffect} ${DevelopmentToolsStyles.toolCard} group md:min-h-[160px] relative`}
Expand Down Expand Up @@ -565,8 +588,16 @@ const Page = () => {
{item?.__group} • {item?.__basis}
</div>
</Link>
))}
</div>
))}
</motion.div>
{filteredItems.length > pageSize && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
)}
</>
)}
</main>
</div>
Expand Down