forked from tanner/qotnews
progress
This commit is contained in:
parent
3e78765952
commit
f670479bd7
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}`);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
13
webapp/src/routes/search.json.js
Normal file
13
webapp/src/routes/search.json.js
Normal 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());
|
||||||
|
}
|
103
webapp/src/routes/search.svelte
Normal file
103
webapp/src/routes/search.svelte
Normal 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>
|
|
@ -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 />
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user