chore: lazy loading

This commit is contained in:
Alexander Medvedev 2026-06-23 13:38:03 +02:00
parent 8a7feb93e8
commit d0e3121391

View File

@ -18,39 +18,67 @@ const Home = () => {
const [topPaid, setTopPaid] = useState<any[]>(homeCache?.topPaid || []); const [topPaid, setTopPaid] = useState<any[]>(homeCache?.topPaid || []);
const [popular, setPopular] = useState<any[]>(homeCache?.popular || []); const [popular, setPopular] = useState<any[]>(homeCache?.popular || []);
const [newest, setNewest] = useState<any[]>(homeCache?.newest || []); const [newest, setNewest] = useState<any[]>(homeCache?.newest || []);
const [loading, setLoading] = useState(!homeCache); // Load sections lazily when the user scrolls them into view
const hasFetched = useRef(!!homeCache); const [loading, setLoading] = useState(false);
useEffect(() => { const topPaidRef = useRef<HTMLElement | null>(null);
if (hasFetched.current) return; const popularRef = useRef<HTMLElement | null>(null);
const newestRef = useRef<HTMLElement | null>(null);
const fetchSections = async () => {
try {
const [topPaidRes, popularRes, newestRes] = await Promise.all([
api.get('/plugins', { params: { type: 'paid', sort: 'downloads', limit: 4 } }),
api.get('/plugins', { params: { sort: 'downloads', limit: 8 } }),
api.get('/plugins', { params: { sort: 'newest', limit: 12 } })
]);
const data = { const fetched = useRef({ topPaid: !!homeCache?.topPaid, popular: !!homeCache?.popular, newest: !!homeCache?.newest });
topPaid: Array.isArray(topPaidRes.data) ? topPaidRes.data : [],
popular: Array.isArray(popularRes.data) ? popularRes.data : [],
newest: Array.isArray(newestRes.data) ? newestRes.data : []
};
setTopPaid(data.topPaid); const saveSection = (key: 'topPaid' | 'popular' | 'newest', data: any[]) => {
setPopular(data.popular); if (key === 'topPaid') setTopPaid(data);
setNewest(data.newest); if (key === 'popular') setPopular(data);
homeCache = data; if (key === 'newest') setNewest(data);
hasFetched.current = true;
} catch (err) { homeCache = {
console.error('Fetch error:', err); topPaid: key === 'topPaid' ? data : homeCache?.topPaid || [],
} finally { popular: key === 'popular' ? data : homeCache?.popular || [],
setLoading(false); newest: key === 'newest' ? data : homeCache?.newest || []
}
}; };
fetchSections(); fetched.current[key] = true;
};
useEffect(() => {
if (fetched.current.topPaid && fetched.current.popular && fetched.current.newest) return;
const observers: IntersectionObserver[] = [];
const createObserver = (el: HTMLElement | null, key: 'topPaid' | 'popular' | 'newest', params: any) => {
if (!el || fetched.current[key]) return;
const obs = new IntersectionObserver(async (entries, observer) => {
for (const entry of entries) {
if (entry.isIntersecting) {
fetched.current[key] = true; // prevent duplicate triggers
try {
setLoading(true);
const res = await api.get('/plugins', { params });
const list = Array.isArray(res.data) ? res.data : [];
saveSection(key, list);
} catch (err) {
console.error('Fetch error for', key, err);
} finally {
setLoading(false);
observer.disconnect();
}
}
}
}, { rootMargin: '200px' });
obs.observe(el);
observers.push(obs);
};
// Ensure refs are attached before creating observers
requestAnimationFrame(() => {
createObserver(topPaidRef.current, 'topPaid', { type: 'paid', sort: 'downloads', limit: 4 });
createObserver(popularRef.current, 'popular', { sort: 'downloads', limit: 8 });
createObserver(newestRef.current, 'newest', { sort: 'newest', limit: 12 });
});
return () => observers.forEach(o => o.disconnect());
}, []); }, []);
if (loading) { if (loading) {
@ -75,36 +103,46 @@ const Home = () => {
</section> </section>
<div className="container" id="browse"> <div className="container" id="browse">
{topPaid.length > 0 && ( <section className="home-section" ref={topPaidRef}>
<section className="home-section"> <h2 className="section-title"><span>Top</span> Premium</h2>
<h2 className="section-title"><span>Top</span> Premium</h2> <div className="home-plugin-grid">
<div className="home-plugin-grid"> {topPaid.length > 0 ? (
{topPaid.map((plugin) => ( topPaid.map((plugin) => (
<PluginCard key={`top-${plugin.id}`} plugin={plugin} /> <PluginCard key={`top-${plugin.id}`} plugin={plugin} />
))} ))
</div> ) : (
</section> <div className="section-placeholder">{loading ? 'Loading premium plugins...' : 'Scroll to load premium plugins'}</div>
)} )}
</div>
</section>
<section className="home-section"> <section className="home-section" ref={popularRef}>
<h2 className="section-title"><span>Most</span> Popular</h2> <h2 className="section-title"><span>Most</span> Popular</h2>
<div className="home-plugin-grid"> <div className="home-plugin-grid">
{popular.map((plugin) => ( {popular.length > 0 ? (
<PluginCard key={`pop-${plugin.id}`} plugin={plugin} /> popular.map((plugin) => (
))} <PluginCard key={`pop-${plugin.id}`} plugin={plugin} />
))
) : (
<div className="section-placeholder">{loading ? 'Loading popular plugins...' : 'Scroll to load popular plugins'}</div>
)}
</div> </div>
</section> </section>
<section className="home-section"> <section className="home-section" ref={newestRef}>
<h2 className="section-title"><span>New</span> & Trending</h2> <h2 className="section-title"><span>New</span> & Trending</h2>
<div className="home-plugin-grid"> <div className="home-plugin-grid">
{newest.map((plugin) => ( {newest.length > 0 ? (
<PluginCard key={`new-${plugin.id}`} plugin={plugin} /> newest.map((plugin) => (
))} <PluginCard key={`new-${plugin.id}`} plugin={plugin} />
))
) : (
<div className="section-placeholder">{loading ? 'Loading new plugins...' : 'Scroll to load new plugins'}</div>
)}
</div> </div>
</section> </section>
{topPaid.length === 0 && popular.length === 0 && newest.length === 0 && ( {(fetched.current?.topPaid && fetched.current?.popular && fetched.current?.newest && topPaid.length === 0 && popular.length === 0 && newest.length === 0) && (
<div className="empty-state"> <div className="empty-state">
<p>No plugins found. Check back later!</p> <p>No plugins found. Check back later!</p>
</div> </div>