v1.43.3 - Mobile: Transações + CategorySelector corrigido
This commit is contained in:
parent
be7bed5c99
commit
9800f987df
31
CHANGELOG.md
31
CHANGELOG.md
@ -5,6 +5,37 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
|
||||
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
|
||||
|
||||
|
||||
## [1.43.3] - 2025-12-16
|
||||
|
||||
### Improved
|
||||
- **Página Transações - Mobile Completo** - Lista de transações otimizada para mobile
|
||||
- Cards individuais: data + badges (tipo/status) + descrição + conta + categoria + valor
|
||||
- Menu dropdown com todas as ações (completar, editar, duplicar, dividir, etc.)
|
||||
- Descrição original do banco visível quando diferente
|
||||
- Fontes: 0.65-1rem, padding p-3, borders secundários
|
||||
- Desktop mantém tabela completa
|
||||
|
||||
- **Paginação Mobile** - Controles otimizados
|
||||
- Mobile: Mostra apenas 3 botões (anterior, atual, próxima)
|
||||
- Indicador "/ total" para contexto
|
||||
- Botões maiores (36px) para facilitar toque
|
||||
- Desktop: Mantém visualização de até 7 páginas
|
||||
|
||||
### Fixed
|
||||
- **CategorySelector - Scroll e Visualização** - Problemas em modais corrigidos
|
||||
- Position fixed com z-index 100000 (acima de modais)
|
||||
- Scroll sempre visível com scrollbar customizada (8px)
|
||||
- Altura fixa garantindo funcionamento: 280px (mobile) / 350px (desktop)
|
||||
- Largura mínima inteligente: 280px (mobile) / 320px (desktop)
|
||||
- Ajuste automático de posição se ultrapassar viewport
|
||||
- Margens de segurança: 10-20px das bordas
|
||||
- Cores da scrollbar harmonizadas com tema escuro
|
||||
- `-webkit-overflow-scrolling: touch` para iOS
|
||||
- Modal com `overflow: auto` para scroll adequado
|
||||
- Cálculo simplificado e robusto de posição
|
||||
- Funciona perfeitamente em todos os lugares (modais e páginas)
|
||||
|
||||
|
||||
## [1.43.2] - 2025-12-16
|
||||
|
||||
### Improved
|
||||
|
||||
@ -30,24 +30,46 @@ export default function CategorySelector({
|
||||
const dropdownRef = useRef(null);
|
||||
const searchInputRef = useRef(null);
|
||||
|
||||
// Calcular posición del dropdown (arriba o abajo según espacio disponible)
|
||||
// Calcular posición del dropdown
|
||||
const updateDropdownPosition = () => {
|
||||
if (buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
const dropdownHeight = 350; // altura aproximada del dropdown
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom - 10; // 10px margen
|
||||
|
||||
// Altura máxima do dropdown
|
||||
const maxDropdownHeight = isMobile ? 280 : 350;
|
||||
|
||||
// Largura mínima para exibir conteúdo completo
|
||||
const minWidth = Math.max(rect.width, isMobile ? 280 : 320);
|
||||
|
||||
// Ajustar left se ultrapassar viewport
|
||||
let leftPosition = rect.left;
|
||||
const viewportWidth = window.innerWidth;
|
||||
if (leftPosition + minWidth > viewportWidth - 20) {
|
||||
leftPosition = viewportWidth - minWidth - 20;
|
||||
}
|
||||
if (leftPosition < 10) {
|
||||
leftPosition = 10;
|
||||
}
|
||||
|
||||
// Espaço disponível abaixo e acima
|
||||
const spaceBelow = viewportHeight - rect.bottom - 10;
|
||||
const spaceAbove = rect.top - 10;
|
||||
|
||||
// Decidir si mostrar arriba o abajo
|
||||
const showAbove = spaceBelow < dropdownHeight && spaceAbove > spaceBelow;
|
||||
// Decidir se mostra acima ou abaixo
|
||||
const showAbove = spaceBelow < maxDropdownHeight && spaceAbove > spaceBelow;
|
||||
|
||||
// Calcular altura real disponível
|
||||
const availableSpace = showAbove ? spaceAbove : spaceBelow;
|
||||
const dropdownHeight = Math.min(maxDropdownHeight, Math.max(200, availableSpace));
|
||||
|
||||
setDropdownPosition({
|
||||
top: showAbove ? rect.top - Math.min(dropdownHeight, spaceAbove) : rect.bottom,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
top: showAbove ? rect.top - dropdownHeight : rect.bottom,
|
||||
left: leftPosition,
|
||||
width: minWidth,
|
||||
showAbove: showAbove,
|
||||
maxHeight: showAbove ? Math.min(dropdownHeight, spaceAbove) : Math.min(dropdownHeight, spaceBelow)
|
||||
maxHeight: dropdownHeight
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -196,18 +218,24 @@ export default function CategorySelector({
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="dropdown-menu show"
|
||||
className="dropdown-menu show category-dropdown-scroll"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: dropdownPosition.top,
|
||||
left: dropdownPosition.left,
|
||||
width: dropdownPosition.width,
|
||||
zIndex: 99999,
|
||||
maxHeight: dropdownPosition.maxHeight || 350,
|
||||
minHeight: '150px',
|
||||
overflowY: 'auto',
|
||||
position: 'fixed',
|
||||
top: `${dropdownPosition.top}px`,
|
||||
left: `${dropdownPosition.left}px`,
|
||||
width: `${dropdownPosition.width}px`,
|
||||
zIndex: 100000,
|
||||
height: `${dropdownPosition.maxHeight || 280}px`,
|
||||
maxHeight: `${dropdownPosition.maxHeight || 280}px`,
|
||||
minHeight: '200px',
|
||||
overflowY: 'scroll',
|
||||
overflowX: 'hidden',
|
||||
padding: '0.5rem 0',
|
||||
boxShadow: '0 6px 20px rgba(0,0,0,0.4)'
|
||||
margin: 0,
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
display: 'block'
|
||||
}}
|
||||
>
|
||||
{/* Búsqueda */}
|
||||
|
||||
@ -66,7 +66,7 @@ const Modal = ({
|
||||
className="modal show d-block"
|
||||
tabIndex="-1"
|
||||
onClick={handleBackdropClick}
|
||||
style={{ zIndex: 1055 }}
|
||||
style={{ zIndex: 1055, overflow: 'auto' }}
|
||||
>
|
||||
<div className={`modal-dialog ${sizeClass} ${centered ? 'modal-dialog-centered' : ''}`}>
|
||||
<div className="modal-content" style={{ background: '#1e293b', color: '#f1f5f9' }}>
|
||||
@ -83,7 +83,7 @@ const Modal = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="modal-body">
|
||||
<div className="modal-body" style={{ maxHeight: 'calc(100vh - 210px)', overflowY: 'auto' }}>
|
||||
{children}
|
||||
</div>
|
||||
{footer && (
|
||||
|
||||
@ -1837,6 +1837,7 @@ input[type="color"]::-webkit-color-swatch {
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
margin-top: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.txn-pagination button {
|
||||
@ -1869,6 +1870,23 @@ input[type="color"]::-webkit-color-swatch {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Mobile pagination */
|
||||
.txn-pagination.mobile {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.txn-pagination.mobile button {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.txn-pagination .page-info {
|
||||
color: #94a3b8;
|
||||
font-size: 0.75rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.txn-empty {
|
||||
text-align: center;
|
||||
@ -2822,3 +2840,41 @@ a,
|
||||
min-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Category Selector - garantir que funcione em modais */
|
||||
.category-selector {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.modal-body .category-selector {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.category-selector .dropdown-menu {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Scroll customizado para dropdown de categoria */
|
||||
.category-dropdown-scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(100, 116, 139, 0.5) rgba(30, 41, 59, 0.3);
|
||||
}
|
||||
|
||||
.category-dropdown-scroll::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.category-dropdown-scroll::-webkit-scrollbar-track {
|
||||
background: rgba(30, 41, 59, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.category-dropdown-scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 116, 139, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.category-dropdown-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 116, 139, 0.7);
|
||||
}
|
||||
|
||||
@ -22,6 +22,9 @@ export default function Transactions() {
|
||||
// Estado para destacar transação
|
||||
const [highlightedTransactionId, setHighlightedTransactionId] = useState(null);
|
||||
|
||||
// Mobile detection
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||
|
||||
// Estados principais
|
||||
const [weeklyData, setWeeklyData] = useState(null);
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
@ -252,6 +255,13 @@ export default function Transactions() {
|
||||
loadWeeklyData();
|
||||
}, [filters, page, selectedCurrency]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Mobile resize detection
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth < 768);
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// Efeito para destacar transação via URL
|
||||
useEffect(() => {
|
||||
const highlightId = searchParams.get('highlight');
|
||||
@ -1339,6 +1349,219 @@ export default function Transactions() {
|
||||
{/* Week Transactions */}
|
||||
{expandedWeeks[week.year_week] && (
|
||||
<div className="txn-week-body">
|
||||
{isMobile ? (
|
||||
// Mobile: Cards Layout
|
||||
<div className="d-flex flex-column gap-2 p-2">
|
||||
{week.transactions.map(transaction => (
|
||||
<div
|
||||
key={transaction.id}
|
||||
ref={transaction.id === highlightedTransactionId ? highlightedRef : null}
|
||||
className={`card border-secondary ${transaction.is_overdue ? 'border-danger' : ''} ${transaction.id === highlightedTransactionId ? 'border-primary' : ''} ${hasActiveFilters && selectedTransactionIds.has(transaction.id) ? 'bg-primary bg-opacity-10' : ''}`}
|
||||
style={{ background: '#0f172a', cursor: 'pointer' }}
|
||||
>
|
||||
<div className="card-body p-3">
|
||||
{/* Header: Date + Type Badge + Status */}
|
||||
<div className="d-flex justify-content-between align-items-start mb-2">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{hasActiveFilters && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={selectedTransactionIds.has(transaction.id)}
|
||||
onChange={() => handleToggleTransaction(transaction.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
<span className="text-slate-400" style={{ fontSize: '0.75rem' }}>
|
||||
{formatDate(transaction.effective_date || transaction.planned_date)}
|
||||
{transaction.is_overdue && <i className="bi bi-exclamation-triangle text-danger ms-1"></i>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex gap-1">
|
||||
<span className={`badge ${transaction.type === 'credit' ? 'bg-success' : 'bg-danger'} bg-opacity-25 ${transaction.type === 'credit' ? 'text-success' : 'text-danger'}`} style={{ fontSize: '0.65rem' }}>
|
||||
{transaction.type === 'credit' ? t('transactions.type.credit') : t('transactions.type.debit')}
|
||||
</span>
|
||||
<span className={`badge ${
|
||||
transaction.status === 'completed' ? 'bg-success' :
|
||||
transaction.status === 'pending' ? 'bg-warning' :
|
||||
transaction.status === 'cancelled' ? 'bg-secondary' : 'bg-info'
|
||||
} bg-opacity-25 text-${
|
||||
transaction.status === 'completed' ? 'success' :
|
||||
transaction.status === 'pending' ? 'warning' :
|
||||
transaction.status === 'cancelled' ? 'secondary' : 'info'
|
||||
}`} style={{ fontSize: '0.65rem' }}>
|
||||
{t(`transactions.status.${transaction.status}`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description + Transfer/Reconciled badges */}
|
||||
<div className="mb-2" onClick={() => openDetailModal(transaction)}>
|
||||
<div className="d-flex align-items-center gap-1 mb-1">
|
||||
{transaction.is_transfer && <i className="bi bi-arrow-left-right text-info"></i>}
|
||||
{transaction.is_reconciled && <i className="bi bi-link-45deg text-success" title={t('transactions.reconciled')}></i>}
|
||||
<span className="text-white fw-medium" style={{ fontSize: '0.85rem' }}>
|
||||
{transaction.description}
|
||||
</span>
|
||||
</div>
|
||||
{transaction.original_description && transaction.original_description !== transaction.description && (
|
||||
<div className="text-slate-400" style={{ fontSize: '0.65rem' }}>
|
||||
<i className="bi bi-bank me-1"></i>
|
||||
{transaction.original_description.substring(0, 60)}{transaction.original_description.length > 60 ? '...' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Account + Category */}
|
||||
<div className="d-flex flex-wrap gap-2 mb-2">
|
||||
<span className="badge bg-secondary bg-opacity-25 text-slate-300" style={{ fontSize: '0.7rem' }}>
|
||||
<i className="bi bi-wallet2 me-1"></i>
|
||||
{transaction.account?.name}
|
||||
</span>
|
||||
{transaction.category && (
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
backgroundColor: transaction.category.color + '20',
|
||||
color: transaction.category.color,
|
||||
fontSize: '0.7rem'
|
||||
}}
|
||||
>
|
||||
<i className={`bi ${transaction.category.icon} me-1`}></i>
|
||||
{transaction.category.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Amount + Actions */}
|
||||
<div className="d-flex justify-content-between align-items-center pt-2" style={{ borderTop: '1px solid #334155' }}>
|
||||
<div className={`fw-bold ${transaction.type === 'credit' ? 'text-success' : 'text-danger'}`} style={{ fontSize: '1rem' }}>
|
||||
{transaction.type === 'credit' ? '+' : '-'}
|
||||
{formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)}
|
||||
</div>
|
||||
<div className="dropdown" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i className="bi bi-three-dots-vertical"></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end shadow-sm">
|
||||
{/* Ações de Status */}
|
||||
{transaction.status === 'pending' && (
|
||||
<>
|
||||
<li className="dropdown-header small text-muted">{t('transactions.status.label')}</li>
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => openCompleteModal(transaction)}>
|
||||
<i className="bi bi-check-circle text-success me-2"></i>
|
||||
{t('transactions.markComplete')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleQuickComplete(transaction)}>
|
||||
<i className="bi bi-lightning-fill text-warning me-2"></i>
|
||||
{t('transactions.quickComplete')}
|
||||
</button>
|
||||
</li>
|
||||
<li><hr className="dropdown-divider" /></li>
|
||||
</>
|
||||
)}
|
||||
{transaction.status === 'completed' && (
|
||||
<>
|
||||
<li className="dropdown-header small text-muted">{t('transactions.status.label')}</li>
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleRevert(transaction)}>
|
||||
<i className="bi bi-arrow-counterclockwise text-warning me-2"></i>
|
||||
{t('transactions.revert')}
|
||||
</button>
|
||||
</li>
|
||||
<li><hr className="dropdown-divider" /></li>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Ações Principais */}
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => openQuickCategorizeModal(transaction)}>
|
||||
<i className="bi bi-tags text-success me-2"></i>
|
||||
{t('transactions.quickCategorize')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => openEditModal(transaction)}>
|
||||
<i className="bi bi-pencil text-primary me-2"></i>
|
||||
{t('common.edit')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleDuplicate(transaction)}>
|
||||
<i className="bi bi-copy text-info me-2"></i>
|
||||
{t('transactions.duplicate')}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{/* Dividir transação */}
|
||||
{!transaction.is_transfer && !transaction.split_parent_id && (
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => openSplitModal(transaction)}>
|
||||
<i className="bi bi-diagram-3 text-secondary me-2"></i>
|
||||
{t('transactions.split')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* Converter em transferência */}
|
||||
{!transaction.is_transfer && !transaction.is_split_child && (
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => openConvertTransferModal(transaction)}>
|
||||
<i className="bi bi-arrow-left-right text-purple me-2"></i>
|
||||
{t('transactions.convertToTransfer')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* Conciliar com passivo */}
|
||||
{transaction.type === 'debit' && !transaction.is_reconciled && !transaction.is_transfer && (
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => openReconcileLiabilityModal(transaction)}>
|
||||
<i className="bi bi-link-45deg text-purple me-2"></i>
|
||||
{t('transactions.reconcileWithLiability')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* Criar Recorrência */}
|
||||
{!transaction.is_transfer && !transaction.recurring_instance_id && (
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => openRecurrenceModal(transaction)}>
|
||||
<i className="bi bi-calendar-check text-info me-2"></i>
|
||||
{t('recurring.makeRecurring')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* Cancelar */}
|
||||
{transaction.status === 'pending' && (
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleCancel(transaction)}>
|
||||
<i className="bi bi-x-circle text-secondary me-2"></i>
|
||||
{t('transactions.cancel')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li><hr className="dropdown-divider" /></li>
|
||||
<li>
|
||||
<button className="dropdown-item text-danger" onClick={() => handleDelete(transaction)}>
|
||||
<i className="bi bi-trash me-2"></i>
|
||||
{t('common.delete')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// Desktop: Table Layout
|
||||
<table className="txn-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -1562,6 +1785,7 @@ export default function Transactions() {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{/* Transferências Agrupadas */}
|
||||
{week.transfers && week.transfers.length > 0 && (
|
||||
@ -1666,27 +1890,51 @@ export default function Transactions() {
|
||||
|
||||
{/* Pagination */}
|
||||
{pagination && pagination.total_pages > 1 && (
|
||||
<div className="txn-pagination">
|
||||
<div className={`txn-pagination ${isMobile ? 'mobile' : ''}`}>
|
||||
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>
|
||||
<i className="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
{Array.from({ length: Math.min(pagination.total_pages, 7) }, (_, i) => {
|
||||
let pageNum;
|
||||
if (pagination.total_pages <= 7) {
|
||||
pageNum = i + 1;
|
||||
} else if (page <= 4) {
|
||||
pageNum = i + 1;
|
||||
} else if (page >= pagination.total_pages - 3) {
|
||||
pageNum = pagination.total_pages - 6 + i;
|
||||
} else {
|
||||
pageNum = page - 3 + i;
|
||||
}
|
||||
return (
|
||||
<button key={pageNum} className={page === pageNum ? 'active' : ''} onClick={() => setPage(pageNum)}>
|
||||
{pageNum}
|
||||
{isMobile ? (
|
||||
// Mobile: Mostrar apenas 3 páginas (anterior, atual, próxima)
|
||||
<>
|
||||
{page > 1 && (
|
||||
<button onClick={() => setPage(page - 1)}>
|
||||
{page - 1}
|
||||
</button>
|
||||
)}
|
||||
<button className="active">
|
||||
{page}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{page < pagination.total_pages && (
|
||||
<button onClick={() => setPage(page + 1)}>
|
||||
{page + 1}
|
||||
</button>
|
||||
)}
|
||||
{/* Info de página */}
|
||||
<span className="page-info">
|
||||
/ {pagination.total_pages}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
// Desktop: Mostrar até 7 páginas
|
||||
Array.from({ length: Math.min(pagination.total_pages, 7) }, (_, i) => {
|
||||
let pageNum;
|
||||
if (pagination.total_pages <= 7) {
|
||||
pageNum = i + 1;
|
||||
} else if (page <= 4) {
|
||||
pageNum = i + 1;
|
||||
} else if (page >= pagination.total_pages - 3) {
|
||||
pageNum = pagination.total_pages - 6 + i;
|
||||
} else {
|
||||
pageNum = page - 3 + i;
|
||||
}
|
||||
return (
|
||||
<button key={pageNum} className={page === pageNum ? 'active' : ''} onClick={() => setPage(pageNum)}>
|
||||
{pageNum}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<button onClick={() => setPage(p => Math.min(pagination.total_pages, p + 1))} disabled={page === pagination.total_pages}>
|
||||
<i className="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user