Files
qotnews/webclient/src/Feed.js
2025-11-21 00:49:14 +00:00

124 lines
3.9 KiB
JavaScript

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('');
useEffect(() => {
fetch('/api')
.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;
if (!stories || !stories.length) {
setStories(newApiStories);
localStorage.setItem('stories', JSON.stringify(newApiStories));
}
let currentStories = Array.isArray(stories) ? [...stories] : [];
for (const newStory of [...newApiStories].reverse()) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10-second timeout
const storyRes = await fetch('/api/' + newStory.id, { signal: controller.signal });
clearTimeout(timeoutId);
if (!storyRes.ok) throw new Error('Story fetch failed');
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);
const existingStoryIndex = currentStories.findIndex(s => s.id === newStory.id);
if (existingStoryIndex > -1) {
currentStories.splice(existingStoryIndex, 1);
}
currentStories.unshift(newStory);
} 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.`;
console.log('Fetch timed out for story:', newStory.id);
} else {
errorMessage = `An error occurred while fetching story '${newStory.title}' (${newStory.id}): ${error.toString()}`;
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);
},
(error) => {
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: ${error.toString()}`;
setError(errorMessage);
}
);
}, [updateCache]);
return (
<div className='container'>
<Helmet>
<title>QotNews</title>
<meta name="robots" content="index" />
</Helmet>
{error &&
<details>
<summary>Connection error? Click to expand.</summary>
<p>{error}</p>
</details>
}
{stories ?
<div>
{stories.map(x =>
<div className='item' key={x.id}>
<div className='title'>
<Link className='link' to={'/' + x.id}>
<img className='source-logo' src={logos[x.source]} alt='source logo' /> {x.title}
</Link>
<span className='source'>
({sourceLink(x)})
</span>
</div>
{infoLine(x)}
</div>
)}
</div>
:
<p>loading...</p>
}
</div>
);
}
export default Feed;