This commit is contained in:
Jason Schwarzenberger 2020-11-30 18:11:45 +13:00
parent 3e78765952
commit f670479bd7
12 changed files with 263 additions and 19 deletions

View File

@ -85,14 +85,16 @@ def get_reflist():
q = session.query(Reflist).order_by(Reflist.rid.desc()) q = session.query(Reflist).order_by(Reflist.rid.desc())
return [dict(ref=x.ref, sid=x.sid, source=x.source, urlref=x.urlref) for x in q.all()] return [dict(ref=x.ref, sid=x.sid, source=x.source, urlref=x.urlref) for x in q.all()]
def get_stories(maxage=60*60*24*2): def get_stories(maxage=0, skip=0, limit=20):
time = datetime.now().timestamp() - maxage time = datetime.now().timestamp() - maxage
session = Session() session = Session()
q = session.query(Reflist, Story.meta).\ q = session.query(Reflist, Story.meta).\
join(Story).\ join(Story).\
filter(Story.title != None).\ filter(Story.title != None).\
filter(Story.meta['date'].as_integer() > time).\ filter(maxage == 0 or Story.meta['date'].as_integer() > time).\
order_by(Story.meta['date'].desc()) order_by(Story.meta['date'].desc()).\
offset(skip).\
limit(limit)
return [x[1] for x in q] return [x[1] for x in q]
def put_ref(ref, sid, source, urlref): def put_ref(ref, sid, source, urlref):

View File

@ -41,7 +41,9 @@ cors = CORS(flask_app)
@flask_app.route('/api') @flask_app.route('/api')
def api(): def api():
stories = database.get_stories(settings.MAX_STORY_AGE) skip = request.args.get('skip', 0)
limit = request.args.get('limit', 20)
stories = database.get_stories(skip=skip, limit=limit)
res = Response(json.dumps({"stories": stories})) res = Response(json.dumps({"stories": stories}))
res.headers['content-type'] = 'application/json' res.headers['content-type'] = 'application/json'
return res return res

View File

@ -1,5 +1,22 @@
<script> <script>
import { goto, prefetch } from "@sapper/app";
import { stores } from "@sapper/app";
export let segment; export let segment;
const { page } = stores();
let q;
let search;
page.subscribe((value) => {
q = value.query.q || "";
});
async function handleSearch(event) {
const url = `/search?q=${event.target.value}`;
await prefetch(url);
await goto(url);
}
</script> </script>
<style> <style>
@ -46,6 +63,12 @@
padding: 1em 0.5em; padding: 1em 0.5em;
display: block; display: block;
} }
input {
line-height: 2;
margin: 1em;
vertical-align: middle;
}
</style> </style>
<nav> <nav>
@ -56,5 +79,17 @@
rel="prefetch" rel="prefetch"
href=".">News</a> href=".">News</a>
</li> </li>
<li>
<form action="/search" method="GET" rel="prefetch">
<input
id="search"
bind:this={search}
type="text"
name="q"
value={q}
placeholder="Search..."
on:keypress={handleSearch} />
</form>
</li>
</ul> </ul>
</nav> </nav>

View File

@ -1,6 +1,7 @@
import fetch from 'isomorphic-fetch'; import fetch from 'isomorphic-fetch';
const API_URL = process.env.API_URL || 'http://news.1j.nz'; // const API_URL = process.env.API_URL || 'http://news.1j.nz';
const API_URL = process.env.API_URL || 'http://localhost:33842';
export async function get(req, res) { export async function get(req, res) {
const response = await fetch(`${API_URL}/api/${req.params.id}`); const response = await fetch(`${API_URL}/api/${req.params.id}`);

View File

@ -23,6 +23,22 @@
</script> </script>
<style> <style>
/* .article {
}
.article-header {
}
.article-header .article-title {
}
.article-header .article-byline {
}
.article-body {
} */
.article-body :global(img) {
max-width: 100%;
}
.spacer { .spacer {
margin: 3rem 0; margin: 3rem 0;
} }

View File

@ -1,9 +1,14 @@
import fetch from 'isomorphic-fetch'; import fetch from 'isomorphic-fetch';
const API_URL = process.env.API_URL || 'http://news.1j.nz'; const API_URL = process.env.API_URL || 'http://news.1j.nz';
// const API_URL = process.env.API_URL || 'http://localhost:33842';
export async function get(req, res) { export async function get(req, res) {
const response = await fetch(`${API_URL}/api`); const { skip, limit } = {
skip: req.query.skip || 0,
limit: req.query.query || 20,
};
const response = await fetch(`${API_URL}/api?skip=${skip}&limit=${limit}`);
res.writeHead(response.status, { 'Content-Type': 'application/json' }); res.writeHead(response.status, { 'Content-Type': 'application/json' });
res.end(await response.text()); res.end(await response.text());
} }

View File

@ -1,24 +1,51 @@
<script context="module"> <script context="module">
export function preload() { export async function preload(page) {
return this.fetch(`index.json`) const { skip, limit } = {
.then((r) => r.json()) skip: page.query.skip || 0,
.then(({ stories }) => { limit: page.query.query || 20,
return { stories }; };
}); const res = await this.fetch(`index.json?skip=${skip}&limit=${limit}`);
const data = await res.json();
if (res.status === 200) {
return { stories: data.stories, skip, limit };
} else {
this.error(res.status, data.message);
}
} }
</script> </script>
<script> <script>
import { stores } from "@sapper/app";
import { getLogoUrl } from "../utils/logos.js"; import { getLogoUrl } from "../utils/logos.js";
import StoryInfo from "../components/StoryInfo.svelte"; import StoryInfo from "../components/StoryInfo.svelte";
export let stories; export let stories;
export let skip;
export let limit;
const { page } = stores();
page.subscribe((value) => {
skip = value.query.skip || 0;
limit = value.query.limit || 20;
});
</script> </script>
<style> <style>
article { article {
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.pagination {
margin: 3rem 0;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.pagination-link.is-right {
margin-left: auto;
}
</style> </style>
<svelte:head> <svelte:head>
@ -29,10 +56,6 @@
<section> <section>
{#each stories as story} {#each stories as story}
<!-- we're using the non-standard `rel=prefetch` attribute to
tell Sapper to load the data for the page as soon as
the user hovers over the link or taps it, instead of
waiting for the 'click' event -->
<article> <article>
<header> <header>
<img <img
@ -49,3 +72,18 @@
</article> </article>
{/each} {/each}
</section> </section>
<div class="pagination">
{#if Number(skip) > 0}
<a
class="pagination-link is-left"
href="?skip={Number(skip) - Math.min(Number(skip), Number(limit))}&limit={limit}"
rel="prefetch">Previous</a>
{/if}
{#if stories.length == Number(limit)}
<a
class="pagination-link is-right"
href="?skip={Number(skip) + Number(limit)}&limit={limit}"
rel="prefetch">Next</a>
{/if}
</div>

View File

@ -0,0 +1,13 @@
import fetch from 'isomorphic-fetch';
const API_URL = process.env.API_URL || 'http://news.1j.nz';
export async function get(req, res) {
const { skip, limit } = {
skip: req.query.skip || 0,
limit: req.query.limit || 20,
};
const response = await fetch(`${API_URL}/api/search?q=${req.query.q}&skip=${skip}&limit=${limit}`);
res.writeHead(response.status, { 'Content-Type': 'application/json' });
res.end(await response.text());
}

View File

@ -0,0 +1,103 @@
<script context="module">
export async function preload(page) {
const { skip, limit, q } = {
skip: page.query.skip || 0,
limit: page.query.query || 20,
q: page.query.q || "",
};
const res = await this.fetch(
`search.json?q=${q}&skip=${skip}&limit=${limit}`
);
const data = await res.json();
if (res.status === 200) {
return { stories: data.results, skip, limit };
} else {
this.error(res.status, data.message);
}
}
</script>
<script>
import { stores } from "@sapper/app";
import { getLogoUrl } from "../utils/logos.js";
import StoryInfo from "../components/StoryInfo.svelte";
export let stories;
export let skip;
export let limit;
export let q;
const { page } = stores();
page.subscribe((value) => {
skip = value.query.skip || 0;
limit = value.query.limit || 20;
q = value.query.query || "";
});
</script>
<style>
article {
margin: 0.5rem 0;
}
.pagination {
margin: 3rem 0;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.pagination-link {
/* border: solid 1px #aaa;
border-radius: 0;
background: #f1f1f1;
border-radius: 5px;
margin: 0.5rem;
padding: 0.5rem;
text-decoration: none; */
}
.pagination-link.is-right {
margin-left: auto;
}
</style>
<svelte:head>
<title>QotNews</title>
<meta property="og:title" content="QotNews" />
<meta property="og:type" content="website" />
</svelte:head>
<section>
{#each stories as story}
<article>
<header>
<img
src={getLogoUrl(story)}
alt="logo"
style="height: 1rem; width: 1rem;" />
<a rel="prefetch" href="/{story.id}">{story.title}</a>
(<a
href={story.url || story.link}>{new URL(story.url || story.link).hostname.replace(/^www\./, '')}</a>)
</header>
<section>
<StoryInfo {story} />
</section>
</article>
{/each}
</section>
<div class="pagination">
{#if Number(skip) > 0}
<a
class="pagination-link is-left"
href="?skip={Number(skip) - Math.min(Number(skip), Number(limit))}&limit={limit}"
rel="prefetch">Previous</a>
{/if}
{#if stories.length == Number(limit)}
<a
class="pagination-link is-right"
href="?skip={Number(skip) + Number(limit)}&limit={limit}"
rel="prefetch">Next</a>
{/if}
</div>

View File

@ -66,12 +66,12 @@ class App extends React.Component {
<Route path='/(|search)' component={Submit} /> <Route path='/(|search)' component={Submit} />
</div> </div>
<Route path='/' exact render={(props) => <Feed {...props} updateCache={this.updateCache} />} /> <Route path='/' exact render={(props) => <Feed {...props} updateCache={this.updateCache} key={Feed.key(props)} />} />
<Switch> <Switch>
<Route path='/search' component={Results} /> <Route path='/search' component={Results} />
<Route path='/:id' exact render={(props) => <Article {...props} cache={this.cache} />} /> <Route path='/:id' exact render={(props) => <Article {...props} cache={this.cache} />} />
</Switch> </Switch>
<Route path='/:id/c' exact render={(props) => <Comments {...props} cache={this.cache} key={props.match.params.id} />} /> <Route path='/:id/c' exact render={(props) => <Comments {...props} cache={this.cache} key={`${props.match.params.id}`} />} />
<ForwardDot /> <ForwardDot />

View File

@ -229,3 +229,13 @@ span.source {
.indented { .indented {
padding: 0 0 0 1rem; padding: 0 0 0 1rem;
} }
.pagination {
margin: 3rem 0;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.pagination-link.is-right {
margin-left: auto;
}

View File

@ -1,20 +1,25 @@
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import localForage from 'localforage'; import localForage from 'localforage';
import { Link } from "react-router-dom";
import { StoryItem } from '../components/StoryItem.js'; import { StoryItem } from '../components/StoryItem.js';
class Feed extends React.Component { class Feed extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const query = new URLSearchParams(this.props.location.search);
this.state = { this.state = {
stories: JSON.parse(localStorage.getItem('stories')) || false, stories: JSON.parse(localStorage.getItem('stories')) || false,
error: false, error: false,
skip: +query.get('skip') || 0,
limit: +query.get('limit') || 20
}; };
} }
componentDidMount() { componentDidMount() {
fetch('/api') fetch(`/api?skip=${this.state.skip}&limit=${this.state.limit}`)
.then(res => res.json()) .then(res => res.json())
.then( .then(
(result) => { (result) => {
@ -51,6 +56,8 @@ class Feed extends React.Component {
render() { render() {
const stories = this.state.stories; const stories = this.state.stories;
const error = this.state.error; const error = this.state.error;
const skip = this.state.skip;
const limit = this.state.limit;
return ( return (
<div className='container'> <div className='container'>
@ -59,9 +66,21 @@ class Feed extends React.Component {
</Helmet> </Helmet>
{error && <p>Connection error?</p>} {error && <p>Connection error?</p>}
{stories ? stories.map(story => <StoryItem story={story}></StoryItem>) : <p>loading...</p>} {stories ? stories.map(story => <StoryItem story={story}></StoryItem>) : <p>loading...</p>}
<div className="pagination">
{Number(skip) > 0 && <Link className="pagination-link" to={`/?skip=${Number(skip) - Math.min(Number(skip), Number(limit))}&limit=${limit}`}>Previous</Link>}
{stories.length == Number(limit) && <Link className="pagination-link is-right" to={`/?skip=${Number(skip) + Number(limit)}&limit=${limit}`}>Next</Link>}
</div>
</div> </div>
); );
} }
} }
Feed.key = function (props) {
const query = new URLSearchParams(props.location.search);
const skip = query.get('skip') || 0;
const limit = query.get('limit') || 20;
return `skip=${skip}&limit=${limit}`;
}
export default Feed; export default Feed;