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 { 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"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
{Array.from({ length: totalPages }).map((_, i) => (
|
|
||||||
<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'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{i + 1}
|
|
||||||
</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>
|
</div>
|
||||||
|
{/* Gözlemci çalışmazsa elle yükleme yolu açık kalsın */}
|
||||||
|
<button
|
||||||
|
onClick={dahaGoster}
|
||||||
|
className="mt-2 text-xs font-bold text-blue-600 hover:text-blue-700 hover:underline"
|
||||||
|
>
|
||||||
|
Daha fazla göster
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs font-medium text-slate-400">
|
||||||
|
Tüm kayıtlar gösterildi — {filteredTenders.length.toLocaleString('tr-TR')} ihale
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user