141 lines
3.7 KiB
JavaScript
141 lines
3.7 KiB
JavaScript
import React, { useState, useEffect, useCallback } from 'react';
|
||
import { Link, useParams } from 'react-router-dom';
|
||
import { HashLink } from 'react-router-hash-link';
|
||
import { Helmet } from 'react-helmet';
|
||
import moment from 'moment';
|
||
import localForage from 'localforage';
|
||
import { infoLine, ToggleDot } from './utils.js';
|
||
|
||
function countComments(c) {
|
||
return c.comments.reduce((sum, x) => sum + countComments(x), 1);
|
||
}
|
||
|
||
function Comments({ cache }) {
|
||
const { id } = useParams();
|
||
|
||
if (id in cache) console.log('cache hit');
|
||
|
||
const [story, setStory] = useState(cache[id] || false);
|
||
const [error, setError] = useState('');
|
||
const [collapsed, setCollapsed] = useState([]);
|
||
const [expanded, setExpanded] = useState([]);
|
||
|
||
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);
|
||
const hash = window.location.hash.substring(1);
|
||
if (hash) {
|
||
setTimeout(() => {
|
||
const element = document.getElementById(hash);
|
||
if (element) {
|
||
element.scrollIntoView();
|
||
}
|
||
}, 0);
|
||
}
|
||
},
|
||
(error) => {
|
||
const errorMessage = `Failed to fetch comments (ID: ${id}). Your connection may be down or the server might be experiencing issues. ${error.toString()}.`;
|
||
setError(errorMessage);
|
||
}
|
||
);
|
||
}, [id]);
|
||
|
||
const collapseComment = useCallback((cid) => {
|
||
setCollapsed(prev => [...prev, cid]);
|
||
setExpanded(prev => prev.filter(x => x !== cid));
|
||
}, []);
|
||
|
||
const expandComment = useCallback((cid) => {
|
||
setCollapsed(prev => prev.filter(x => x !== cid));
|
||
setExpanded(prev => [...prev, cid]);
|
||
}, []);
|
||
|
||
const displayComment = useCallback((story, c, level) => {
|
||
const cid = c.author+c.date;
|
||
|
||
const isCollapsed = collapsed.includes(cid);
|
||
const isExpanded = expanded.includes(cid);
|
||
|
||
const hidden = isCollapsed || (level == 4 && !isExpanded);
|
||
const hasChildren = c.comments.length !== 0;
|
||
|
||
return (
|
||
<div className={level ? 'comment lined' : 'comment'} key={cid}>
|
||
<div className='info'>
|
||
<p>
|
||
{c.author === story.author ? '[OP]' : ''} {c.author || '[Deleted]'}
|
||
{' '} | <HashLink to={'#'+cid} id={cid}>{moment.unix(c.date).fromNow()}</HashLink>
|
||
|
||
{hidden || hasChildren &&
|
||
<span className='collapser pointer' onClick={() => collapseComment(cid)}>–</span>
|
||
}
|
||
</p>
|
||
</div>
|
||
|
||
<div className={isCollapsed ? 'text hidden' : 'text'} dangerouslySetInnerHTML={{ __html: c.text }} />
|
||
|
||
{hidden && hasChildren ?
|
||
<div className='comment lined info pointer' onClick={() => expandComment(cid)}>[show {countComments(c)-1} more]</div>
|
||
:
|
||
c.comments.map(i => displayComment(story, i, level + 1))
|
||
}
|
||
</div>
|
||
);
|
||
}, [collapsed, expanded, collapseComment, expandComment]);
|
||
|
||
return (
|
||
<div className='container'>
|
||
{error &&
|
||
<details style={{marginBottom: '1rem'}}>
|
||
<summary>Connection error? Click to expand.</summary>
|
||
<p>{error}</p>
|
||
{story && <p>Loaded comments from cache.</p>}
|
||
</details>
|
||
}
|
||
{story ?
|
||
<div className='article'>
|
||
<Helmet>
|
||
<title>{story.title} | QotNews</title>
|
||
<meta name="robots" content="noindex" />
|
||
</Helmet>
|
||
|
||
<h1>{story.title}</h1>
|
||
|
||
<div className='info'>
|
||
<Link to={'/' + story.id}>View article</Link>
|
||
</div>
|
||
|
||
{infoLine(story)}
|
||
|
||
<div className='comments'>
|
||
{story.comments.map(c => displayComment(story, c, 0))}
|
||
</div>
|
||
</div>
|
||
:
|
||
<p>loading...</p>
|
||
}
|
||
<ToggleDot id={id} article={true} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default Comments;
|