refactor: Convert Comments class to functional using hooks

This commit is contained in:
2025-07-07 17:40:02 +00:00
committed by Tanner Collin
parent 26a6353ca5
commit 034c440e46

View File

@@ -1,35 +1,32 @@
import React from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { HashLink } from 'react-router-hash-link'; import { HashLink } from 'react-router-hash-link';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import moment from 'moment'; import moment from 'moment';
import localForage from 'localforage'; import localForage from 'localforage';
import { infoLine, ToggleDot } from './utils.js'; import { infoLine, ToggleDot } from './utils.js';
class Comments extends React.Component { function countComments(c) {
constructor(props) { return c.comments.reduce((sum, x) => sum + countComments(x), 1);
super(props); }
const id = this.props.match.params.id; function Comments({ cache }) {
const cache = this.props.cache; const { id } = useParams();
if (id in cache) console.log('cache hit'); if (id in cache) console.log('cache hit');
this.state = { const [story, setStory] = useState(cache[id] || false);
story: cache[id] || false, const [error, setError] = useState(false);
error: false, const [collapsed, setCollapsed] = useState([]);
collapsed: [], const [expanded, setExpanded] = useState([]);
expanded: [],
};
}
componentDidMount() {
const id = this.props.match.params.id;
useEffect(() => {
localForage.getItem(id) localForage.getItem(id)
.then( .then(
(value) => { (value) => {
this.setState({ story: value }); if (value) {
setStory(value);
}
} }
); );
@@ -37,47 +34,41 @@ class Comments extends React.Component {
.then(res => res.json()) .then(res => res.json())
.then( .then(
(result) => { (result) => {
this.setState({ story: result.story }, () => { setStory(result.story);
const hash = window.location.hash.substring(1);
if (hash) {
document.getElementById(hash).scrollIntoView();
}
});
localForage.setItem(id, 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) => { (error) => {
this.setState({ error: true }); setError(true);
} }
); );
} }, [id]);
collapseComment(cid) { const collapseComment = useCallback((cid) => {
this.setState(prevState => ({ setCollapsed(prev => [...prev, cid]);
...prevState, setExpanded(prev => prev.filter(x => x !== cid));
collapsed: [...prevState.collapsed, cid], }, []);
expanded: prevState.expanded.filter(x => x !== cid),
}));
}
expandComment(cid) { const expandComment = useCallback((cid) => {
this.setState(prevState => ({ setCollapsed(prev => prev.filter(x => x !== cid));
...prevState, setExpanded(prev => [...prev, cid]);
collapsed: prevState.collapsed.filter(x => x !== cid), }, []);
expanded: [...prevState.expanded, cid],
}));
}
countComments(c) { const displayComment = useCallback((story, c, level) => {
return c.comments.reduce((sum, x) => sum + this.countComments(x), 1);
}
displayComment(story, c, level) {
const cid = c.author+c.date; const cid = c.author+c.date;
const collapsed = this.state.collapsed.includes(cid); const isCollapsed = collapsed.includes(cid);
const expanded = this.state.expanded.includes(cid); const isExpanded = expanded.includes(cid);
const hidden = collapsed || (level == 4 && !expanded); const hidden = isCollapsed || (level == 4 && !isExpanded);
const hasChildren = c.comments.length !== 0; const hasChildren = c.comments.length !== 0;
return ( return (
@@ -88,56 +79,50 @@ class Comments extends React.Component {
{' '} | <HashLink to={'#'+cid} id={cid}>{moment.unix(c.date).fromNow()}</HashLink> {' '} | <HashLink to={'#'+cid} id={cid}>{moment.unix(c.date).fromNow()}</HashLink>
{hidden || hasChildren && {hidden || hasChildren &&
<span className='collapser pointer' onClick={() => this.collapseComment(cid)}></span> <span className='collapser pointer' onClick={() => collapseComment(cid)}></span>
} }
</p> </p>
</div> </div>
<div className={collapsed ? 'text hidden' : 'text'} dangerouslySetInnerHTML={{ __html: c.text }} /> <div className={isCollapsed ? 'text hidden' : 'text'} dangerouslySetInnerHTML={{ __html: c.text }} />
{hidden && hasChildren ? {hidden && hasChildren ?
<div className='comment lined info pointer' onClick={() => this.expandComment(cid)}>[show {this.countComments(c)-1} more]</div> <div className='comment lined info pointer' onClick={() => expandComment(cid)}>[show {countComments(c)-1} more]</div>
: :
c.comments.map(i => this.displayComment(story, i, level + 1)) c.comments.map(i => displayComment(story, i, level + 1))
} }
</div> </div>
); );
} }, [collapsed, expanded, collapseComment, expandComment]);
render() { return (
const id = this.props.match.params.id; <div className='container'>
const story = this.state.story; {error && <p>Connection error?</p>}
const error = this.state.error; {story ?
<div className='article'>
<Helmet>
<title>{story.title} | QotNews</title>
<meta name="robots" content="noindex" />
</Helmet>
return ( <h1>{story.title}</h1>
<div className='container'>
{error && <p>Connection error?</p>}
{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 className='info'>
<Link to={'/' + story.id}>View article</Link>
</div>
{infoLine(story)}
<div className='comments'>
{story.comments.map(c => this.displayComment(story, c, 0))}
</div>
</div> </div>
:
<p>loading...</p> {infoLine(story)}
}
<ToggleDot id={id} article={true} /> <div className='comments'>
</div> {story.comments.map(c => displayComment(story, c, 0))}
); </div>
} </div>
:
<p>loading...</p>
}
<ToggleDot id={id} article={true} />
</div>
);
} }
export default Comments; export default Comments;