feat: add pagination to Clients and Products pages to improve performance with large datasets
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m9s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m9s
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import { Link, useOutletContext } from 'react-router-dom';
|
import { Link, useOutletContext } from 'react-router-dom';
|
||||||
import { Search, ChevronRight, Filter } from 'lucide-react';
|
import { Search, ChevronRight, Filter, ChevronLeft } from 'lucide-react';
|
||||||
import type { OrderData } from '../types';
|
import type { OrderData } from '../types';
|
||||||
import { parseOrderDate } from '../dataService';
|
import { parseOrderDate } from '../dataService';
|
||||||
|
|
||||||
@@ -10,6 +10,15 @@ const Clients = () => {
|
|||||||
const { ordersData } = useOutletContext<{ ordersData: OrderData[] }>();
|
const { ordersData } = useOutletContext<{ ordersData: OrderData[] }>();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [sortBy, setSortBy] = useState<SortOption>('recent');
|
const [sortBy, setSortBy] = useState<SortOption>('recent');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
|
||||||
|
// Reset to first page when search or sort changes
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [searchTerm, sortBy]);
|
||||||
|
|
||||||
const clientsData = useMemo(() => {
|
const clientsData = useMemo(() => {
|
||||||
const orders = ordersData;
|
const orders = ordersData;
|
||||||
@@ -58,6 +67,11 @@ const Clients = () => {
|
|||||||
});
|
});
|
||||||
}, [searchTerm, sortBy, ordersData]);
|
}, [searchTerm, sortBy, ordersData]);
|
||||||
|
|
||||||
|
// Pagination logic
|
||||||
|
const totalPages = Math.ceil(clientsData.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const paginatedData = clientsData.slice(startIndex, startIndex + itemsPerPage);
|
||||||
|
|
||||||
const formatCurrency = (value: number) => {
|
const formatCurrency = (value: number) => {
|
||||||
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
|
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
|
||||||
};
|
};
|
||||||
@@ -112,11 +126,11 @@ const Clients = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-zinc-100 dark:divide-dark-border">
|
<tbody className="divide-y divide-zinc-100 dark:divide-dark-border">
|
||||||
{clientsData.map((client, index) => (
|
{paginatedData.map((client, index) => (
|
||||||
<tr key={client.name} className="hover:bg-zinc-50/80 dark:hover:bg-dark-input/50 transition-colors group">
|
<tr key={client.name} className="hover:bg-zinc-50/80 dark:hover:bg-dark-input/50 transition-colors group">
|
||||||
<td className="px-6 py-2.5">
|
<td className="px-6 py-2.5">
|
||||||
<span className="inline-flex items-center justify-center w-7 h-7 rounded-full text-xs font-bold bg-zinc-100 dark:bg-dark-border text-zinc-500 dark:text-dark-muted">
|
<span className="inline-flex items-center justify-center w-7 h-7 rounded-full text-xs font-bold bg-zinc-100 dark:bg-dark-border text-zinc-500 dark:text-dark-muted">
|
||||||
{index + 1}
|
{startIndex + index + 1}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-2.5 font-semibold text-zinc-900 dark:text-dark-text">{client.name}</td>
|
<td className="px-6 py-2.5 font-semibold text-zinc-900 dark:text-dark-text">{client.name}</td>
|
||||||
@@ -138,6 +152,49 @@ const Clients = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
<div className="px-6 py-4 border-t border-zinc-100 dark:border-dark-border flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-zinc-500 dark:text-dark-muted">
|
||||||
|
<span>Mostrar</span>
|
||||||
|
<select
|
||||||
|
value={itemsPerPage}
|
||||||
|
onChange={(e) => {
|
||||||
|
setItemsPerPage(Number(e.target.value));
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="bg-zinc-50 dark:bg-dark-input border border-zinc-200 dark:border-dark-border rounded-lg px-2 py-1 focus:outline-none focus:border-brand-primary cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
<option value={100}>100</option>
|
||||||
|
</select>
|
||||||
|
<span>itens por página</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<span className="text-zinc-500 dark:text-dark-muted">
|
||||||
|
Mostrando {clientsData.length > 0 ? startIndex + 1 : 0} a {Math.min(startIndex + itemsPerPage, clientsData.length)} de {clientsData.length} clientes
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="p-1 rounded-lg border border-zinc-200 dark:border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:bg-zinc-50 dark:hover:bg-dark-input transition-colors text-zinc-600 dark:text-dark-text"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
|
disabled={currentPage === totalPages || totalPages === 0}
|
||||||
|
className="p-1 rounded-lg border border-zinc-200 dark:border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:bg-zinc-50 dark:hover:bg-dark-input transition-colors text-zinc-600 dark:text-dark-text"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import { Link, useOutletContext } from 'react-router-dom';
|
import { Link, useOutletContext } from 'react-router-dom';
|
||||||
import { Search, Package, TrendingUp } from 'lucide-react';
|
import { Search, Package, TrendingUp, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import DateRangePicker from '../components/DateRangePicker';
|
import DateRangePicker from '../components/DateRangePicker';
|
||||||
import type { OrderData, DateRange } from '../types';
|
import type { OrderData, DateRange } from '../types';
|
||||||
import { parseOrderDate } from '../dataService';
|
import { parseOrderDate } from '../dataService';
|
||||||
@@ -9,6 +9,15 @@ const Products = () => {
|
|||||||
const { dateRange, setDateRange, ordersData } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void, ordersData: OrderData[] }>();
|
const { dateRange, setDateRange, ordersData } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void, ordersData: OrderData[] }>();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
|
||||||
|
// Reset to first page when search or date range changes
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [searchTerm, dateRange]);
|
||||||
|
|
||||||
const productsData = useMemo(() => {
|
const productsData = useMemo(() => {
|
||||||
const orders = ordersData;
|
const orders = ordersData;
|
||||||
const productMap: Record<string, { id: string, name: string, totalSold: number, revenue: number, lastPrice: number }> = {};
|
const productMap: Record<string, { id: string, name: string, totalSold: number, revenue: number, lastPrice: number }> = {};
|
||||||
@@ -40,6 +49,11 @@ const Products = () => {
|
|||||||
return result.sort((a, b) => b.totalSold - a.totalSold);
|
return result.sort((a, b) => b.totalSold - a.totalSold);
|
||||||
}, [dateRange, searchTerm, ordersData]);
|
}, [dateRange, searchTerm, ordersData]);
|
||||||
|
|
||||||
|
// Pagination logic
|
||||||
|
const totalPages = Math.ceil(productsData.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const paginatedData = productsData.slice(startIndex, startIndex + itemsPerPage);
|
||||||
|
|
||||||
const formatCurrency = (value: number) => {
|
const formatCurrency = (value: number) => {
|
||||||
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
|
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
|
||||||
};
|
};
|
||||||
@@ -81,7 +95,7 @@ const Products = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-zinc-100 dark:divide-dark-border">
|
<tbody className="divide-y divide-zinc-100 dark:divide-dark-border">
|
||||||
{productsData.map((product) => (
|
{paginatedData.map((product) => (
|
||||||
<tr key={product.id} className="hover:bg-zinc-50/80 dark:hover:bg-dark-input/50 transition-colors group">
|
<tr key={product.id} className="hover:bg-zinc-50/80 dark:hover:bg-dark-input/50 transition-colors group">
|
||||||
<td className="px-6 py-2.5 font-mono text-[11px] text-zinc-400 dark:text-dark-muted">#{product.id}</td>
|
<td className="px-6 py-2.5 font-mono text-[11px] text-zinc-400 dark:text-dark-muted">#{product.id}</td>
|
||||||
<td className="px-6 py-2.5">
|
<td className="px-6 py-2.5">
|
||||||
@@ -109,6 +123,49 @@ const Products = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
<div className="px-6 py-4 border-t border-zinc-100 dark:border-dark-border flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-zinc-500 dark:text-dark-muted">
|
||||||
|
<span>Mostrar</span>
|
||||||
|
<select
|
||||||
|
value={itemsPerPage}
|
||||||
|
onChange={(e) => {
|
||||||
|
setItemsPerPage(Number(e.target.value));
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="bg-zinc-50 dark:bg-dark-input border border-zinc-200 dark:border-dark-border rounded-lg px-2 py-1 focus:outline-none focus:border-brand-primary cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
<option value={100}>100</option>
|
||||||
|
</select>
|
||||||
|
<span>itens por página</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<span className="text-zinc-500 dark:text-dark-muted">
|
||||||
|
Mostrando {productsData.length > 0 ? startIndex + 1 : 0} a {Math.min(startIndex + itemsPerPage, productsData.length)} de {productsData.length} produtos
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="p-1 rounded-lg border border-zinc-200 dark:border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:bg-zinc-50 dark:hover:bg-dark-input transition-colors text-zinc-600 dark:text-dark-text"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
|
disabled={currentPage === totalPages || totalPages === 0}
|
||||||
|
className="p-1 rounded-lg border border-zinc-200 dark:border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:bg-zinc-50 dark:hover:bg-dark-input transition-colors text-zinc-600 dark:text-dark-text"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user