import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import localForage from 'localforage'; import { sourceLink, infoLine, logos } from './utils.js'; function Feed({ updateCache }) { const [stories, setStories] = useState(() => JSON.parse(localStorage.getItem('stories')) || false); const [error, setError] = useState(''); const [loadingStatus, setLoadingStatus] = useState(null); const [filterSmallweb, setFilterSmallweb] = useState(() => localStorage.getItem('filterSmallweb') === 'true'); const handleFilterChange = e => { const isChecked = e.target.checked; setStories(false); setFilterSmallweb(isChecked); localStorage.setItem('filterSmallweb', isChecked); }; useEffect(() => { const controller = new AbortController(); fetch(filterSmallweb ? '/api?smallweb=true' : '/api', { signal: controller.signal }) .then(res => { if (!res.ok) { throw new Error(`Server responded with ${res.status} ${res.statusText}`); } return res.json(); }) .then( async (result) => { const newApiStories = result.stories; const updated = !stories || !stories.length || stories[0].id !== newApiStories[0].id; console.log('New stories available:', updated); if (!updated) return; setLoadingStatus({ current: 0, total: newApiStories.length }); let currentStories = Array.isArray(stories) ? [...stories] : []; let preloadedCount = 0; for (const [index, newStory] of newApiStories.entries()) { if (controller.signal.aborted) { break; } try { const storyFetchController = new AbortController(); const timeoutId = setTimeout(() => storyFetchController.abort(), 10000); // 10-second timeout const storyRes = await fetch('/api/' + newStory.id, { signal: storyFetchController.signal }); clearTimeout(timeoutId); if (!storyRes.ok) { throw new Error(`Server responded with ${storyRes.status} ${storyRes.statusText}`); } const storyResult = await storyRes.json(); const fullStory = storyResult.story; await localForage.setItem(fullStory.id, fullStory); console.log('Preloaded story:', fullStory.id, fullStory.title); updateCache(fullStory.id, fullStory); preloadedCount++; setLoadingStatus({ current: preloadedCount, total: newApiStories.length }); const existingStoryIndex = currentStories.findIndex(s => s.id === newStory.id); if (existingStoryIndex > -1) { currentStories.splice(existingStoryIndex, 1); } currentStories.splice(index, 0, newStory); localStorage.setItem('stories', JSON.stringify(currentStories)); setStories(currentStories); } catch (error) { let errorMessage; if (error.name === 'AbortError') { errorMessage = `The request to fetch story '${newStory.title}' (${newStory.id}) timed out after 10 seconds. Your connection may be unstable. (${preloadedCount} / ${newApiStories.length} stories preloaded)`; console.log('Fetch timed out for story:', newStory.id); } else { errorMessage = `An error occurred while fetching story '${newStory.title}' (ID: ${newStory.id}): ${error.toString()}. (${preloadedCount} / ${newApiStories.length} stories preloaded)`; console.log('Fetch failed for story:', newStory.id, error); } setError(errorMessage); break; } } const finalStories = currentStories.slice(0, newApiStories.length); const removedStories = currentStories.slice(newApiStories.length); for (const story of removedStories) { console.log('Removed story:', story.id, story.title); localForage.removeItem(story.id); } localStorage.setItem('stories', JSON.stringify(finalStories)); setStories(finalStories); setLoadingStatus(null); }, (error) => { if (error.name === 'AbortError') { console.log('Feed fetch aborted.'); return; } const errorMessage = `Failed to fetch the main story list from the API. Your connection may be down or the server might be experiencing issues. ${error.toString()}.`; setError(errorMessage); } ); return () => controller.abort(); }, [updateCache, filterSmallweb]); return (
QotNews
{error &&
Connection error? Click to expand.

{error}

{stories &&

Loaded feed from cache.

}
} {stories ?
{stories.map(x =>
source logo {x.title} ({sourceLink(x)})
{infoLine(x)}
)}
:

Loading...

} {loadingStatus &&

Preloading stories {loadingStatus.current} / {loadingStatus.total}...

}
); } export default Feed;