feat: ihale listesi sayfalama yerine sonsuz kaydirma (10 bin kayitta 1000+ sayfa dugmesi de kalkti)

This commit is contained in:
2026-08-30 16:57:01 +03:00
parent 40fea290e4
commit 7701ce43e3
+56 -36
View File
@@ -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 { useApp } from '../context/AppContext';
import { TenderCard } from './TenderCard'; 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'; import confetti from 'canvas-confetti';
// İl adları kaynaklar arasında farklı yazılabiliyor (Ankara / ANKARA); // İl adları kaynaklar arasında farklı yazılabiliyor (Ankara / ANKARA);
@@ -23,8 +23,12 @@ export const TenderList = () => {
} = useApp(); } = useApp();
const [sortBy, setSortBy] = useState('newest'); // 'newest', 'urgent', 'distant' 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 // Filter logic
const filteredTenders = useMemo(() => { const filteredTenders = useMemo(() => {
@@ -66,8 +70,33 @@ export const TenderList = () => {
}); });
}, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy]); }, [tenders, searchQuery, selectedCity, selectedAlimTuru, selectedSektorler, selectedCpvler, selectedKurumTipi, quickFilter, sortBy]);
const totalPages = Math.max(1, Math.ceil(filteredTenders.length / itemsPerPage)); const gorunenTenders = filteredTenders.slice(0, gosterilen);
const paginatedTenders = filteredTenders.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); 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 = () => { const handleExportExcel = () => {
confetti({ particleCount: 80, spread: 60, origin: { y: 0.6 } }); confetti({ particleCount: 80, spread: 60, origin: { y: 0.6 } });
@@ -128,7 +157,7 @@ export const TenderList = () => {
</div> </div>
{/* Tender Cards List */} {/* 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"> <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" /> <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> <h3 className="text-base font-bold text-slate-800">Arama kriterlerine uygun ihale bulunamadı</h3>
@@ -136,43 +165,34 @@ export const TenderList = () => {
</div> </div>
) : ( ) : (
<div className="space-y-3.5"> <div className="space-y-3.5">
{paginatedTenders.map(tender => ( {gorunenTenders.map(tender => (
<TenderCard key={tender.id} tender={tender} /> <TenderCard key={tender.id} tender={tender} />
))} ))}
</div> </div>
)} )}
{/* Pagination Bar */} {/* Sonsuz kaydırma dibi — sayfa yok, aşağı indikçe dolar */}
{totalPages > 1 && ( {filteredTenders.length > 0 && (
<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"> <div ref={dipRef} className="py-4 text-center">
<span>Sayfa {currentPage} / {totalPages} (Toplam {filteredTenders.length} Kayıt)</span> {dahaVar ? (
<div className="flex items-center gap-1"> <>
<button <div className="flex items-center justify-center gap-2 text-xs font-medium text-slate-500">
disabled={currentPage === 1} <Loader2 className="w-4 h-4 animate-spin text-blue-500" />
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))} {gorunenTenders.length.toLocaleString('tr-TR')} / {filteredTenders.length.toLocaleString('tr-TR')} ihale gösteriliyor
className="p-1.5 rounded-lg border border-slate-300 disabled:opacity-40 hover:bg-slate-50" </div>
> {/* Gözlemci çalışmazsa elle yükleme yolu açık kalsın */}
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: totalPages }).map((_, i) => (
<button <button
key={i} onClick={dahaGoster}
onClick={() => setCurrentPage(i + 1)} className="mt-2 text-xs font-bold text-blue-600 hover:text-blue-700 hover:underline"
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'
}`}
> >
{i + 1} Daha fazla göster
</button> </button>
))} </>
<button ) : (
disabled={currentPage === totalPages} <p className="text-xs font-medium text-slate-400">
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))} Tüm kayıtlar gösterildi — {filteredTenders.length.toLocaleString('tr-TR')} ihale
className="p-1.5 rounded-lg border border-slate-300 disabled:opacity-40 hover:bg-slate-50" </p>
> )}
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div> </div>
)} )}