forked from tanner/qotnews
		
	progress
This commit is contained in:
		| @@ -85,14 +85,16 @@ def get_reflist(): | ||||
|     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()] | ||||
|  | ||||
| def get_stories(maxage=60*60*24*2): | ||||
| def get_stories(maxage=0, skip=0, limit=20): | ||||
|     time = datetime.now().timestamp() - maxage | ||||
|     session = Session() | ||||
|     q = session.query(Reflist, Story.meta).\ | ||||
|             join(Story).\ | ||||
|             filter(Story.title != None).\ | ||||
|             filter(Story.meta['date'].as_integer() > time).\ | ||||
|             order_by(Story.meta['date'].desc()) | ||||
|             filter(maxage == 0 or Story.meta['date'].as_integer() > time).\ | ||||
|             order_by(Story.meta['date'].desc()).\ | ||||
|             offset(skip).\ | ||||
|             limit(limit) | ||||
|     return [x[1] for x in q] | ||||
|  | ||||
| def put_ref(ref, sid, source, urlref): | ||||
|   | ||||
| @@ -41,7 +41,9 @@ cors = CORS(flask_app) | ||||
|  | ||||
| @flask_app.route('/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.headers['content-type'] = 'application/json' | ||||
|     return res | ||||
|   | ||||
| @@ -1,5 +1,22 @@ | ||||
| <script> | ||||
|   import { goto, prefetch } from "@sapper/app"; | ||||
|   import { stores } from "@sapper/app"; | ||||
|  | ||||
|   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> | ||||
|  | ||||
| <style> | ||||
| @@ -46,6 +63,12 @@ | ||||
|     padding: 1em 0.5em; | ||||
|     display: block; | ||||
|   } | ||||
|  | ||||
|   input { | ||||
|     line-height: 2; | ||||
|     margin: 1em; | ||||
|     vertical-align: middle; | ||||
|   } | ||||
| </style> | ||||
|  | ||||
| <nav> | ||||
| @@ -56,5 +79,17 @@ | ||||
|         rel="prefetch" | ||||
|         href=".">News</a> | ||||
|     </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> | ||||
| </nav> | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| 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) { | ||||
| 	const response = await fetch(`${API_URL}/api/${req.params.id}`); | ||||
|   | ||||
| @@ -23,6 +23,22 @@ | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* .article { | ||||
|   } | ||||
|   .article-header { | ||||
|   } | ||||
|  | ||||
|   .article-header .article-title { | ||||
|   } | ||||
|  | ||||
|   .article-header .article-byline { | ||||
|   } | ||||
|   .article-body { | ||||
|   } */ | ||||
|   .article-body :global(img) { | ||||
|     max-width: 100%; | ||||
|   } | ||||
|  | ||||
|   .spacer { | ||||
|     margin: 3rem 0; | ||||
|   } | ||||
|   | ||||
| @@ -1,9 +1,14 @@ | ||||
| import fetch from 'isomorphic-fetch'; | ||||
|  | ||||
| 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) { | ||||
| 	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.end(await response.text()); | ||||
| } | ||||
| @@ -1,24 +1,51 @@ | ||||
| <script context="module"> | ||||
|   export function preload() { | ||||
|     return this.fetch(`index.json`) | ||||
|       .then((r) => r.json()) | ||||
|       .then(({ stories }) => { | ||||
|         return { stories }; | ||||
|       }); | ||||
|   export async function preload(page) { | ||||
|     const { skip, limit } = { | ||||
|       skip: page.query.skip || 0, | ||||
|       limit: page.query.query || 20, | ||||
|     }; | ||||
|     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> | ||||
|   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; | ||||
|  | ||||
|   const { page } = stores(); | ||||
|  | ||||
|   page.subscribe((value) => { | ||||
|     skip = value.query.skip || 0; | ||||
|     limit = value.query.limit || 20; | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   article { | ||||
|     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> | ||||
|  | ||||
| <svelte:head> | ||||
| @@ -29,10 +56,6 @@ | ||||
|  | ||||
| <section> | ||||
|   {#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> | ||||
|       <header> | ||||
|         <img | ||||
| @@ -49,3 +72,18 @@ | ||||
|     </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> | ||||
|   | ||||
							
								
								
									
										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} /> | ||||
| 					</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> | ||||
| 						<Route path='/search' component={Results} /> | ||||
| 						<Route path='/:id' exact render={(props) => <Article {...props} cache={this.cache} />} /> | ||||
| 					</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 /> | ||||
|  | ||||
|   | ||||
| @@ -229,3 +229,13 @@ span.source { | ||||
| .indented { | ||||
|   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 { Helmet } from 'react-helmet'; | ||||
| import localForage from 'localforage'; | ||||
| import { Link } from "react-router-dom"; | ||||
| import { StoryItem } from '../components/StoryItem.js'; | ||||
|  | ||||
| class Feed extends React.Component { | ||||
| 	constructor(props) { | ||||
| 		super(props); | ||||
|  | ||||
| 		const query = new URLSearchParams(this.props.location.search); | ||||
|  | ||||
| 		this.state = { | ||||
| 			stories: JSON.parse(localStorage.getItem('stories')) || false, | ||||
| 			error: false, | ||||
| 			skip: +query.get('skip') || 0, | ||||
| 			limit: +query.get('limit') || 20 | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	componentDidMount() { | ||||
| 		fetch('/api') | ||||
| 		fetch(`/api?skip=${this.state.skip}&limit=${this.state.limit}`) | ||||
| 			.then(res => res.json()) | ||||
| 			.then( | ||||
| 				(result) => { | ||||
| @@ -51,6 +56,8 @@ class Feed extends React.Component { | ||||
| 	render() { | ||||
| 		const stories = this.state.stories; | ||||
| 		const error = this.state.error; | ||||
| 		const skip = this.state.skip; | ||||
| 		const limit = this.state.limit; | ||||
|  | ||||
| 		return ( | ||||
| 			<div className='container'> | ||||
| @@ -59,9 +66,21 @@ class Feed extends React.Component { | ||||
| 				</Helmet> | ||||
| 				{error && <p>Connection error?</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> | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user