feat: ihale listesi sayfalama yerine sonsuz kaydirma (10 bin kayitta 1000+ sayfa dugmesi de kalkti)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { TenderCard } from './TenderCard';
|
||||
import { Download, SlidersHorizontal, Layers, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Download, SlidersHorizontal, Layers, Loader2 } from 'lucide-react';
|
||||
import confetti from 'canvas-confetti';
|
||||
|
||||
// İl adları kaynaklar arasında farklı yazılabiliyor (Ankara / ANKARA);
|
||||
@@ -23,8 +23,12 @@ export const TenderList = () => {
|
||||
} = useApp();
|
||||
|
||||
const [sortBy, setSortBy] = useState('newest'); // 'newest', 'urgent', 'distant'
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 10;
|
||||
|
||||
// Sonsuz kaydırma: sayfa yerine aşağı indikçe liste büyür.
|
||||
// (Sayfalama 10 bin kayıtta 1000+ sayfa düğmesi basıyordu.)
|
||||
const SAYFA_ADIM = 20;
|
||||
const [gosterilen, setGosterilen] = useState(SAYFA_ADIM);
|
||||
const dipRef = useRef(null);
|
||||
|
||||
// Filter logic
|
||||
const filteredTenders = useMemo(() => {
|
||||
@@ -66,8 +70,33 @@ export const TenderList = () => {
|
||||
});
|
||||
}, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredTenders.length / itemsPerPage));
|
||||
const paginatedTenders = filteredTenders.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
|
||||
const gorunenTenders = filteredTenders.slice(0, gosterilen);
|
||||
const dahaVar = gosterilen < filteredTenders.length;
|
||||
|
||||
// Baştan başlama YALNIZCA filtre/sıralama değişiminde olmalı. filteredTenders
|
||||
// dizisine bağlansaydı havuz her yenilendiğinde (10 dk) kullanıcı listenin
|
||||
// başına fırlardı.
|
||||
useEffect(() => { setGosterilen(SAYFA_ADIM); }, [
|
||||
searchQuery, selectedCity, selectedAlimTuru, selectedSektorler,
|
||||
selectedCpvler, selectedKurumTipi, quickFilter, sortBy,
|
||||
]);
|
||||
|
||||
const dahaGoster = useCallback(() => {
|
||||
setGosterilen((n) => Math.min(n + SAYFA_ADIM, filteredTenders.length));
|
||||
}, [filteredTenders.length]);
|
||||
|
||||
// Dipteki işaretçi görünür olunca yükle. IntersectionObserver yoksa
|
||||
// (eski tarayıcı) aşağıdaki "Daha fazla göster" düğmesi devreye girer.
|
||||
useEffect(() => {
|
||||
const el = dipRef.current;
|
||||
if (!el || !dahaVar || typeof IntersectionObserver === 'undefined') return;
|
||||
const gozlemci = new IntersectionObserver(
|
||||
(girisler) => { if (girisler[0]?.isIntersecting) dahaGoster(); },
|
||||
{ rootMargin: '600px 0px' }, // ekrana girmeden önce yükle
|
||||
);
|
||||
gozlemci.observe(el);
|
||||
return () => gozlemci.disconnect();
|
||||
}, [dahaVar, dahaGoster]);
|
||||
|
||||
const handleExportExcel = () => {
|
||||
confetti({ particleCount: 80, spread: 60, origin: { y: 0.6 } });
|
||||
@@ -128,7 +157,7 @@ export const TenderList = () => {
|
||||
</div>
|
||||
|
||||
{/* Tender Cards List */}
|
||||
{paginatedTenders.length === 0 ? (
|
||||
{gorunenTenders.length === 0 ? (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center shadow-xs">
|
||||
<SlidersHorizontal className="w-12 h-12 text-slate-300 mx-auto mb-3" />
|
||||
<h3 className="text-base font-bold text-slate-800">Arama kriterlerine uygun ihale bulunamadı</h3>
|
||||
@@ -136,43 +165,34 @@ export const TenderList = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3.5">
|
||||
{paginatedTenders.map(tender => (
|
||||
{gorunenTenders.map(tender => (
|
||||
<TenderCard key={tender.id} tender={tender} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination Bar */}
|
||||
{totalPages > 1 && (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-3 shadow-xs flex items-center justify-between text-xs font-medium text-slate-600">
|
||||
<span>Sayfa {currentPage} / {totalPages} (Toplam {filteredTenders.length} Kayıt)</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
className="p-1.5 rounded-lg border border-slate-300 disabled:opacity-40 hover:bg-slate-50"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
{Array.from({ length: totalPages }).map((_, i) => (
|
||||
{/* Sonsuz kaydırma dibi — sayfa yok, aşağı indikçe dolar */}
|
||||
{filteredTenders.length > 0 && (
|
||||
<div ref={dipRef} className="py-4 text-center">
|
||||
{dahaVar ? (
|
||||
<>
|
||||
<div className="flex items-center justify-center gap-2 text-xs font-medium text-slate-500">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
{gorunenTenders.length.toLocaleString('tr-TR')} / {filteredTenders.length.toLocaleString('tr-TR')} ihale gösteriliyor
|
||||
</div>
|
||||
{/* Gözlemci çalışmazsa elle yükleme yolu açık kalsın */}
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
className={`w-8 h-8 rounded-lg font-bold transition-colors ${
|
||||
currentPage === i + 1 ? 'bg-blue-600 text-white shadow-xs' : 'hover:bg-slate-100 text-slate-700'
|
||||
}`}
|
||||
onClick={dahaGoster}
|
||||
className="mt-2 text-xs font-bold text-blue-600 hover:text-blue-700 hover:underline"
|
||||
>
|
||||
{i + 1}
|
||||
Daha fazla göster
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
||||
className="p-1.5 rounded-lg border border-slate-300 disabled:opacity-40 hover:bg-slate-50"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs font-medium text-slate-400">
|
||||
Tüm kayıtlar gösterildi — {filteredTenders.length.toLocaleString('tr-TR')} ihale
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user