Set title on article and comment pages, add comment anchors
This commit is contained in:
parent
5fd4fdb21c
commit
25a671f58e
|
@ -6,7 +6,9 @@
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"react": "^16.9.0",
|
"react": "^16.9.0",
|
||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.9.0",
|
||||||
|
"react-helmet": "^5.2.1",
|
||||||
"react-router-dom": "^5.0.1",
|
"react-router-dom": "^5.0.1",
|
||||||
|
"react-router-hash-link": "^1.2.2",
|
||||||
"react-scripts": "3.1.1"
|
"react-scripts": "3.1.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -5,8 +5,9 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Reddit, Hacker News, and Tildes combined, then pre-rendered in reader mode."
|
content="{{ description }}"
|
||||||
/>
|
/>
|
||||||
|
<meta content="{{ url }}" name="og:site_name">
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
|
@ -25,7 +26,7 @@
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>QNN - Qot News Network</title>
|
<title>{{ title }} - QotNews</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
|
|
|
@ -36,15 +36,15 @@ class App extends React.Component {
|
||||||
<Router>
|
<Router>
|
||||||
<div className='container menu'>
|
<div className='container menu'>
|
||||||
<p>
|
<p>
|
||||||
<Link to='/'>QNN - Home</Link>
|
<Link to='/'>QotNews - Feed</Link>
|
||||||
<span className='theme'>Theme: <a href='#' onClick={() => this.light()}>Light</a> - <a href='#' onClick={() => this.dark()}>Dark</a></span>
|
<span className='theme'>Theme: <a href='#' onClick={() => this.light()}>Light</a> - <a href='#' onClick={() => this.dark()}>Dark</a></span>
|
||||||
<br />
|
<br />
|
||||||
<span className='slogan'>Reddit, Hacker News, and Tildes combined, then pre-rendered in reader mode.</span>
|
<span className='slogan'>Reddit, Hacker News, and Tildes combined, then pre-rendered in reader mode.</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Route path='/' exact component={Feed} />
|
<Route path='/' exact component={Feed} />
|
||||||
<Route path='/:id' exact component={Comments} />
|
<Route path='/:id/c' exact component={Comments} />
|
||||||
<Route path='/:id/a' exact component={Article} />
|
<Route path='/:id' exact component={Article} />
|
||||||
|
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import { sourceLink, infoLine, ToggleDot } from './utils.js';
|
import { sourceLink, infoLine, ToggleDot } from './utils.js';
|
||||||
|
|
||||||
const apiUrl = 'https://news-api.t0.vc/';
|
|
||||||
|
|
||||||
class Article extends React.Component {
|
class Article extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -19,7 +18,7 @@ class Article extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const id = this.props.match.params.id;
|
const id = this.props.match.params.id;
|
||||||
|
|
||||||
fetch(apiUrl + id)
|
fetch('/api/' + id)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(
|
.then(
|
||||||
(result) => {
|
(result) => {
|
||||||
|
@ -42,6 +41,10 @@ class Article extends React.Component {
|
||||||
{error && <p>Connection error?</p>}
|
{error && <p>Connection error?</p>}
|
||||||
{story ?
|
{story ?
|
||||||
<div className='article'>
|
<div className='article'>
|
||||||
|
<Helmet>
|
||||||
|
<title>{story.title} - QotNews</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<h1>{story.title}</h1>
|
<h1>{story.title}</h1>
|
||||||
|
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { HashLink } from 'react-router-hash-link';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { sourceLink, infoLine, ToggleDot } from './utils.js';
|
import { sourceLink, infoLine, ToggleDot } from './utils.js';
|
||||||
|
|
||||||
const apiUrl = 'https://news-api.t0.vc/';
|
|
||||||
|
|
||||||
class Article extends React.Component {
|
class Article extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -20,12 +20,17 @@ class Article extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const id = this.props.match.params.id;
|
const id = this.props.match.params.id;
|
||||||
|
|
||||||
fetch(apiUrl + id)
|
fetch('/api/' + id)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(
|
.then(
|
||||||
(result) => {
|
(result) => {
|
||||||
this.setState({ story: result.story });
|
|
||||||
localStorage.setItem(id, JSON.stringify(result.story));
|
localStorage.setItem(id, JSON.stringify(result.story));
|
||||||
|
this.setState({ story: result.story }, () => {
|
||||||
|
const hash = window.location.hash.substring(1);
|
||||||
|
if (hash) {
|
||||||
|
document.getElementById(hash).scrollIntoView();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
|
@ -37,7 +42,10 @@ class Article extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className={level ? 'comment lined' : 'comment'}>
|
<div className={level ? 'comment lined' : 'comment'}>
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
<p>{c.author === story.author ? '[OP]' : ''} {c.author || '[Deleted]'} | {moment.unix(c.date).fromNow()}</p>
|
<p>
|
||||||
|
{c.author === story.author ? '[OP]' : ''} {c.author || '[Deleted]'}
|
||||||
|
​ | <HashLink to={'#'+c.author+c.date} id={c.author+c.date}>{moment.unix(c.date).fromNow()}</HashLink>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text' dangerouslySetInnerHTML={{ __html: c.text }} />
|
<div className='text' dangerouslySetInnerHTML={{ __html: c.text }} />
|
||||||
|
@ -61,10 +69,14 @@ class Article extends React.Component {
|
||||||
{error && <p>Connection error?</p>}
|
{error && <p>Connection error?</p>}
|
||||||
{story ?
|
{story ?
|
||||||
<div className='article'>
|
<div className='article'>
|
||||||
|
<Helmet>
|
||||||
|
<title>{story.title} - QotNews Comments</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<h1>{story.title}</h1>
|
<h1>{story.title}</h1>
|
||||||
|
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
<Link to={'/' + story.id + '/a'}>View article</Link>
|
<Link to={'/' + story.id}>View article</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{infoLine(story)}
|
{infoLine(story)}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import { siteLogo, sourceLink, infoLine } from './utils.js';
|
import { siteLogo, sourceLink, infoLine } from './utils.js';
|
||||||
import { clearStorage } from './utils.js';
|
import { clearStorage } from './utils.js';
|
||||||
|
|
||||||
const apiUrl = 'https://news-api.t0.vc/';
|
|
||||||
|
|
||||||
class Feed extends React.Component {
|
class Feed extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -16,7 +15,7 @@ class Feed extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
fetch(apiUrl)
|
fetch('/api')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(
|
.then(
|
||||||
(result) => {
|
(result) => {
|
||||||
|
@ -24,7 +23,7 @@ class Feed extends React.Component {
|
||||||
clearStorage();
|
clearStorage();
|
||||||
localStorage.setItem('stories', JSON.stringify(result.stories));
|
localStorage.setItem('stories', JSON.stringify(result.stories));
|
||||||
result.stories.filter(x => x.score >= 20).slice(0, 25).forEach(x => {
|
result.stories.filter(x => x.score >= 20).slice(0, 25).forEach(x => {
|
||||||
fetch(apiUrl + x.id)
|
fetch('/api/' + x.id)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
localStorage.setItem(x.id, JSON.stringify(result.story));
|
localStorage.setItem(x.id, JSON.stringify(result.story));
|
||||||
|
@ -45,6 +44,10 @@ class Feed extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
|
<Helmet>
|
||||||
|
<title>Feed - QotNews</title>
|
||||||
|
<meta name="description" content="Reddit, Hacker News, and Tildes combined, then pre-rendered in reader mode" />
|
||||||
|
</Helmet>
|
||||||
{error && <p>Connection error?</p>}
|
{error && <p>Connection error?</p>}
|
||||||
{stories ?
|
{stories ?
|
||||||
<div>
|
<div>
|
||||||
|
@ -55,7 +58,7 @@ class Feed extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='title'>
|
<div className='title'>
|
||||||
<Link className='link' to={'/' + x.id + '/a'}>{siteLogo[x.source]} {x.title}</Link>
|
<Link className='link' to={'/' + x.id}>{siteLogo[x.source]} {x.title}</Link>
|
||||||
|
|
||||||
<span className='source'>
|
<span className='source'>
|
||||||
​({sourceLink(x)})
|
​({sourceLink(x)})
|
||||||
|
|
|
@ -2,7 +2,7 @@ body {
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
font: 1rem/1.3 sans-serif;
|
font: 1rem/1.3 sans-serif;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
margin-bottom: 50%;
|
margin-bottom: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const infoLine = (story) =>
|
||||||
by <a href={story.author_link}>{story.author}</a>
|
by <a href={story.author_link}>{story.author}</a>
|
||||||
​ {moment.unix(story.date).fromNow()}
|
​ {moment.unix(story.date).fromNow()}
|
||||||
​ on <a href={story.link}>{story.source}</a> | ​
|
​ on <a href={story.link}>{story.source}</a> | ​
|
||||||
<Link className={story.num_comments > 99 ? 'hot' : ''} to={'/' + story.id}>
|
<Link className={story.num_comments > 99 ? 'hot' : ''} to={'/' + story.id + '/c'}>
|
||||||
{story.num_comments} comment{story.num_comments !== 1 && 's'}
|
{story.num_comments} comment{story.num_comments !== 1 && 's'}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,7 +42,7 @@ export class ToggleDot extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className='toggleDot'>
|
<div className='toggleDot'>
|
||||||
<div className='button'>
|
<div className='button'>
|
||||||
<Link to={'/' + id + (article ? '/a' : '')}>
|
<Link to={'/' + id + (article ? '' : '/c')}>
|
||||||
<img src={Switch} />
|
<img src={Switch} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7723,7 +7723,7 @@ prompts@^2.0.1:
|
||||||
kleur "^3.0.3"
|
kleur "^3.0.3"
|
||||||
sisteransi "^1.0.3"
|
sisteransi "^1.0.3"
|
||||||
|
|
||||||
prop-types@^15.6.2, prop-types@^15.7.2:
|
prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||||
version "15.7.2"
|
version "15.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||||
|
@ -7937,6 +7937,21 @@ react-error-overlay@^6.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.1.tgz#b8d3cf9bb991c02883225c48044cb3ee20413e0f"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.1.tgz#b8d3cf9bb991c02883225c48044cb3ee20413e0f"
|
||||||
integrity sha512-V9yoTr6MeZXPPd4nV/05eCBvGH9cGzc52FN8fs0O0TVQ3HYYf1n7EgZVtHbldRq5xU9zEzoXIITjYNIfxDDdUw==
|
integrity sha512-V9yoTr6MeZXPPd4nV/05eCBvGH9cGzc52FN8fs0O0TVQ3HYYf1n7EgZVtHbldRq5xU9zEzoXIITjYNIfxDDdUw==
|
||||||
|
|
||||||
|
react-fast-compare@^2.0.2:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
|
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||||
|
|
||||||
|
react-helmet@^5.2.1:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-5.2.1.tgz#16a7192fdd09951f8e0fe22ffccbf9bb3e591ffa"
|
||||||
|
integrity sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==
|
||||||
|
dependencies:
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
prop-types "^15.5.4"
|
||||||
|
react-fast-compare "^2.0.2"
|
||||||
|
react-side-effect "^1.1.0"
|
||||||
|
|
||||||
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4:
|
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4:
|
||||||
version "16.9.0"
|
version "16.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb"
|
||||||
|
@ -7955,6 +7970,13 @@ react-router-dom@^5.0.1:
|
||||||
tiny-invariant "^1.0.2"
|
tiny-invariant "^1.0.2"
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
|
|
||||||
|
react-router-hash-link@^1.2.2:
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-router-hash-link/-/react-router-hash-link-1.2.2.tgz#7a0ad5e925d49596d19554de8bc6c554ce4f8099"
|
||||||
|
integrity sha512-LBthLVHdqPeKDVt3+cFRhy15Z7veikOvdKRZRfyBR2vjqIE7rxn+tKLjb6DOmLm6JpoQVemVDnxQ35RVnEHdQA==
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.6.0"
|
||||||
|
|
||||||
react-router@5.0.1:
|
react-router@5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.0.1.tgz#04ee77df1d1ab6cb8939f9f01ad5702dbadb8b0f"
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.0.1.tgz#04ee77df1d1ab6cb8939f9f01ad5702dbadb8b0f"
|
||||||
|
@ -8032,6 +8054,13 @@ react-scripts@3.1.1:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "2.0.7"
|
fsevents "2.0.7"
|
||||||
|
|
||||||
|
react-side-effect@^1.1.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-1.2.0.tgz#0e940c78faba0c73b9b0eba9cd3dda8dfb7e7dae"
|
||||||
|
integrity sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==
|
||||||
|
dependencies:
|
||||||
|
shallowequal "^1.0.1"
|
||||||
|
|
||||||
react@^16.9.0:
|
react@^16.9.0:
|
||||||
version "16.9.0"
|
version "16.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa"
|
||||||
|
@ -8643,6 +8672,11 @@ shallow-clone@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
kind-of "^6.0.2"
|
kind-of "^6.0.2"
|
||||||
|
|
||||||
|
shallowequal@^1.0.1:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
|
||||||
|
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
|
||||||
|
|
||||||
shebang-command@^1.2.0:
|
shebang-command@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user