Files
qotnews/webclient/src/Article.js
2025-12-03 01:43:33 +00:00

126 lines
3.3 KiB
JavaScript

import React, { useState, useEffect, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import localForage from 'localforage';
import { sourceLink, infoLine, ToggleDot } from './utils.js';
function Article({ cache }) {
const { id } = useParams();
if (id in cache) console.log('cache hit');
const [story, setStory] = useState(cache[id] || false);
const [error, setError] = useState('');
const [pConv, setPConv] = useState([]);
const [copyButtonText, setCopyButtonText] = useState('\ue92c');
useEffect(() => {
localForage.getItem(id)
.then(
(value) => {
if (value) {
setStory(value);
}
}
);
fetch('/api/' + id)
.then(res => {
if (!res.ok) {
throw new Error(`Server responded with ${res.status} ${res.statusText}`);
}
return res.json();
})
.then(
(result) => {
setStory(result.story);
localForage.setItem(id, result.story);
},
(error) => {
const errorMessage = `Failed to fetch new article content (ID: ${id}). Your connection may be down or the server might be experiencing issues. ${error.toString()}.`;
setError(errorMessage);
}
);
}, [id]);
const copyLink = () => {
navigator.clipboard.writeText(`${story.title}:\n${window.location.href}`).then(() => {
setCopyButtonText('\uea10');
setTimeout(() => setCopyButtonText('\ue92c'), 2000);
}, () => {
setCopyButtonText('\uea0f');
setTimeout(() => setCopyButtonText('\ue92c'), 2000);
});
};
const pConvert = (n) => {
setPConv(prevPConv => [...prevPConv, n]);
};
const nodes = useMemo(() => {
if (story && story.text) {
let div = document.createElement('div');
div.innerHTML = story.text;
return div.childNodes;
}
return null;
}, [story]);
return (
<div className='article-container'>
{error &&
<details style={{marginBottom: '1rem'}}>
<summary>Connection error? Click to expand.</summary>
<p>{error}</p>
{story && <p>Loaded article from cache.</p>}
</details>
}
{story ?
<div className='article'>
<Helmet>
<title>{story.title} | QotNews</title>
<meta name="robots" content="noindex" />
</Helmet>
<h1>{story.title} <button className='copy-button' onClick={copyLink}>{copyButtonText}</button></h1>
<div className='info'>
Source: {sourceLink(story)}
</div>
{infoLine(story)}
{nodes ?
<div className='story-text'>
{Object.entries(nodes).map(([k, v]) =>
pConv.includes(k) ?
<React.Fragment key={k}>
{v.innerHTML.split('\n\n').map((x, i) =>
<p key={i} dangerouslySetInnerHTML={{ __html: x }} />
)}
</React.Fragment>
:
(v.nodeName === '#text' ?
<p key={k}>{v.data}</p>
:
<React.Fragment key={k}>
<v.localName dangerouslySetInnerHTML={v.innerHTML ? { __html: v.innerHTML } : null} />
{(v.localName === 'pre' || v.localName === 'code' || (v.children?.length === 1 && v.children[0].localName === 'code')) && <button onClick={() => pConvert(k)}>Convert Code to Paragraph</button>}
</React.Fragment>
)
)}
</div>
:
<p>Problem getting article :(</p>
}
</div>
:
<p>Loading...</p>
}
<ToggleDot id={id} article={false} />
</div>
);
}
export default Article;