forked from tanner/qotnews
fun.
This commit is contained in:
parent
60e34935ee
commit
8115d86335
52
webapp/src/components/Article.svelte
Normal file
52
webapp/src/components/Article.svelte
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import StoryInfo from "../components/StoryInfo.svelte";
|
||||||
|
export let story;
|
||||||
|
|
||||||
|
let host = new URL(story.url || story.link).hostname.replace(/^www\./, "");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.article-title {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
.article-header {
|
||||||
|
padding: 0 0 1rem;
|
||||||
|
}
|
||||||
|
.article-body {
|
||||||
|
max-width: 45rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.article-body :global(figure) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.article-body :global(figcaption p),
|
||||||
|
.article-body :global(figcaption) {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.article-body :global(figcaption) {
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0 1rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
.article-body :global(figure),
|
||||||
|
.article-body :global(img) {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<article class="article">
|
||||||
|
<header class="article-header">
|
||||||
|
<h1 class="article-title">{story.title}</h1>
|
||||||
|
{#if story.url}
|
||||||
|
<div>source: <a href={story.url}>{host}</a></div>
|
||||||
|
{/if}
|
||||||
|
<StoryInfo class="article-byline" {story} />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="article-body">
|
||||||
|
{@html story.text}
|
||||||
|
</section>
|
||||||
|
</article>
|
|
@ -1,12 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import fromUnixTime from "date-fns/fromUnixTime";
|
import Time from "../components/Time.svelte";
|
||||||
import formatDistanceToNow from "date-fns/formatDistanceToNow";
|
|
||||||
export let story;
|
export let story;
|
||||||
export let comment;
|
export let comment;
|
||||||
export let showComments = true;
|
export let showComments = true;
|
||||||
export let dateString = formatDistanceToNow(fromUnixTime(comment.date), {
|
|
||||||
addSuffix: true,
|
|
||||||
});
|
|
||||||
const author = (comment.author || "").replace(" ", "");
|
const author = (comment.author || "").replace(" ", "");
|
||||||
export let id = `${author}-${comment.date}`;
|
export let id = `${author}-${comment.date}`;
|
||||||
|
|
||||||
|
@ -80,9 +77,7 @@
|
||||||
class={comment.author === story.author ? 'comment-author is-op' : 'comment-author'}>{comment.author || '[Deleted]'}</span>
|
class={comment.author === story.author ? 'comment-author is-op' : 'comment-author'}>{comment.author || '[Deleted]'}</span>
|
||||||
•
|
•
|
||||||
<a class="time-link" href="{story.id}#comment-{id}">
|
<a class="time-link" href="{story.id}#comment-{id}">
|
||||||
<time
|
<Time date={comment.date} />
|
||||||
datetime={fromUnixTime(comment.date).toISOString()}
|
|
||||||
title={fromUnixTime(comment.date)}>{dateString}</time>
|
|
||||||
</a>
|
</a>
|
||||||
{#if comment.comments.length}
|
{#if comment.comments.length}
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from "lodash";
|
||||||
import { goto, prefetch } from "@sapper/app";
|
import { goto, prefetch, stores } from "@sapper/app";
|
||||||
import { stores } from "@sapper/app";
|
|
||||||
|
|
||||||
export let segment;
|
export let segment;
|
||||||
|
|
||||||
const { page } = stores();
|
const { page } = stores();
|
||||||
|
|
||||||
let q;
|
|
||||||
let search;
|
let search;
|
||||||
|
|
||||||
page.subscribe((value) => {
|
let handleSearch = debounce(_handleSearch, 300, {
|
||||||
q = value.query.q || "";
|
trailing: true,
|
||||||
|
leading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let handleSearch = debounce(_handleSearch);
|
page.subscribe((page) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (segment === "search") {
|
||||||
|
search && search.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
async function _handleSearch(event) {
|
async function _handleSearch(event) {
|
||||||
const url = `/search?q=${event.target.value}`;
|
const url = `/search?q=${event.target.value}`;
|
||||||
|
@ -23,35 +28,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
nav {
|
.navigation [aria-current] {
|
||||||
border-bottom: 1px solid rgba(255, 62, 0, 0.1);
|
|
||||||
font-weight: 300;
|
|
||||||
padding: 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* clearfix */
|
|
||||||
ul::after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
[aria-current] {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
[aria-current]::after {
|
.navigation [aria-current]::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: "";
|
||||||
width: calc(100% - 1em);
|
width: calc(100% - 1em);
|
||||||
|
@ -61,38 +43,78 @@
|
||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.navigation {
|
||||||
|
border-bottom: 1px solid rgba(255, 62, 0, 0.1);
|
||||||
|
font-weight: 300;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-container {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 64rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.navigation-container > * {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.navigation-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-item {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.navigation-text,
|
||||||
|
.navigation-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 1em 0.5em;
|
padding: 1em 0.5em;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.navigation-input {
|
||||||
input {
|
|
||||||
line-height: 2;
|
line-height: 2;
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
width: 15rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<nav>
|
<nav class="navigation">
|
||||||
<ul>
|
<div class="navigation-container">
|
||||||
<li>
|
<ul class="navigation-list" role="menubar">
|
||||||
<a
|
<li class="navigation-item">
|
||||||
aria-current={segment === undefined ? 'page' : undefined}
|
<a
|
||||||
rel="prefetch"
|
class="navigation-link"
|
||||||
href=".">News</a>
|
aria-current={segment === undefined ? 'page' : undefined}
|
||||||
</li>
|
rel="prefetch"
|
||||||
<li>
|
href=".">News</a>
|
||||||
<form action="/search" method="GET" rel="prefetch">
|
</li>
|
||||||
<input
|
<li class="navigation-item">
|
||||||
id="search"
|
<a
|
||||||
bind:this={search}
|
class="navigation-link"
|
||||||
type="text"
|
aria-current={segment === 'search' ? 'page' : undefined}
|
||||||
name="q"
|
rel="prefetch"
|
||||||
value={q}
|
href="/search">Search</a>
|
||||||
placeholder="Search..."
|
</li>
|
||||||
on:keypress={handleSearch} />
|
</ul>
|
||||||
</form>
|
<form action="/search" method="GET" rel="prefetch" role="search">
|
||||||
</li>
|
<input
|
||||||
</ul>
|
class="navigation-input"
|
||||||
|
id="search"
|
||||||
|
bind:this={search}
|
||||||
|
type="text"
|
||||||
|
name="q"
|
||||||
|
value={$page.query.q || ''}
|
||||||
|
placeholder="Search..."
|
||||||
|
on:keypress={handleSearch} />
|
||||||
|
</form>
|
||||||
|
<ul class="navigation-list">
|
||||||
|
<li class="navigation-item"><span class="navigation-text">Qot.</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
55
webapp/src/components/Pagination.svelte
Normal file
55
webapp/src/components/Pagination.svelte
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import { stores } from "@sapper/app";
|
||||||
|
export let href;
|
||||||
|
export let search;
|
||||||
|
export let count;
|
||||||
|
|
||||||
|
const { page } = stores();
|
||||||
|
|
||||||
|
let skip = 0;
|
||||||
|
let limit = 20;
|
||||||
|
let prevLink = "";
|
||||||
|
let nextLink = "";
|
||||||
|
|
||||||
|
page.subscribe((p) => {
|
||||||
|
count = Number(count);
|
||||||
|
skip = Number(p.query.skip) || 0;
|
||||||
|
limit = Number(p.query.limit) || 20;
|
||||||
|
|
||||||
|
let previous = new URLSearchParams(search || "");
|
||||||
|
let next = new URLSearchParams(search || "");
|
||||||
|
|
||||||
|
previous.append("skip", skip - Math.min(skip, limit));
|
||||||
|
previous.append("limit", limit);
|
||||||
|
|
||||||
|
next.append("skip", skip + limit);
|
||||||
|
next.append("limit", limit);
|
||||||
|
|
||||||
|
prevLink = href + "?" + previous.toString();
|
||||||
|
nextLink = href + "?" + next.toString();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pagination {
|
||||||
|
margin: 3rem 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.pagination-link.is-next {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
{#if skip > 0}
|
||||||
|
<a
|
||||||
|
class="pagination-link is-prev"
|
||||||
|
href={prevLink}
|
||||||
|
rel="prefetch">Previous</a>
|
||||||
|
{/if}
|
||||||
|
{#if count >= limit}
|
||||||
|
<a class="pagination-link is-next" href={nextLink} rel="prefetch">Next</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
|
@ -1,26 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
import fromUnixTime from "date-fns/fromUnixTime";
|
import Time from "../components/Time.svelte";
|
||||||
import formatDistanceToNow from "date-fns/formatDistanceToNow";
|
|
||||||
export let story;
|
export let story;
|
||||||
export let dateString = formatDistanceToNow(fromUnixTime(story.date), {
|
|
||||||
addSuffix: true,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="info">
|
<Time date={story.date} />
|
||||||
<time
|
{#if story.author && story.author_link}
|
||||||
datetime={fromUnixTime(story.date).toISOString()}
|
by
|
||||||
title={fromUnixTime(story.date)}>{dateString}</time>
|
<a href={story.author_link}>{story.author}</a>
|
||||||
{#if story.author && story.author_link}
|
{:else if story.author}by {story.author}{/if}
|
||||||
by
|
on
|
||||||
<a href={story.author_link}>{story.author}</a>
|
<a href={story.link || story.url}>{story.source}</a>
|
||||||
{:else if story.author}by {story.author}{/if}
|
{#if story.score}• {story.score} points{/if}
|
||||||
on
|
{#if story.num_comments}
|
||||||
<a href={story.url}>{story.source}</a>
|
•
|
||||||
{#if story.score}• {story.score} points{/if}
|
<a rel="prefetch" href="/{story.id}#comments">{story.num_comments}
|
||||||
{#if story.num_comments}
|
comments</a>
|
||||||
•
|
{/if}
|
||||||
<a rel="prefetch" href="/{story.id}#comments">{story.num_comments}
|
|
||||||
comments</a>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
55
webapp/src/components/StoryList.svelte
Normal file
55
webapp/src/components/StoryList.svelte
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import { getLogoUrl } from "../utils/logos.js";
|
||||||
|
import StoryInfo from "../components/StoryInfo.svelte";
|
||||||
|
export let stories;
|
||||||
|
|
||||||
|
const host = (url) => new URL(url).hostname.replace(/^www\./, "");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.story-item {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding-left: 1.2em;
|
||||||
|
}
|
||||||
|
.story-icon,
|
||||||
|
.story-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.story-icon {
|
||||||
|
margin-left: -1.2rem;
|
||||||
|
}
|
||||||
|
.story-source::before {
|
||||||
|
content: "(";
|
||||||
|
}
|
||||||
|
.story-source::after {
|
||||||
|
content: ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-item :global(a) {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.story-item :global(a:hover) {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#each stories as story}
|
||||||
|
<article class="story-item">
|
||||||
|
<header class="story-header">
|
||||||
|
<img
|
||||||
|
src={getLogoUrl(story)}
|
||||||
|
alt="logo"
|
||||||
|
class="story-icon"
|
||||||
|
style="height: 1rem; width: 1rem;" />
|
||||||
|
<a class="story-title" rel="prefetch" href="/{story.id}">{story.title}</a>
|
||||||
|
<a
|
||||||
|
class="story-source"
|
||||||
|
href={story.url || story.link}>{host(story.url || story.link)}</a>
|
||||||
|
</header>
|
||||||
|
<section class="story-info">
|
||||||
|
<StoryInfo {story} />
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<slot />
|
11
webapp/src/components/Time.svelte
Normal file
11
webapp/src/components/Time.svelte
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<script>
|
||||||
|
import fromUnixTime from "date-fns/fromUnixTime";
|
||||||
|
import formatDistanceToNow from "date-fns/formatDistanceToNow";
|
||||||
|
export let date;
|
||||||
|
let d = fromUnixTime(date);
|
||||||
|
let datetime = d.toISOString();
|
||||||
|
let title = d.toLocaleString();
|
||||||
|
let dateString = formatDistanceToNow(d, { addSuffix: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<time {datetime} {title}>{dateString}</time>
|
|
@ -1,6 +1,5 @@
|
||||||
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://localhost:33842';
|
const API_URL = process.env.API_URL || 'http://localhost:33842';
|
||||||
|
|
||||||
export async function get(req, res) {
|
export async function get(req, res) {
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<script>
|
<script>
|
||||||
import fromUnixTime from "date-fns/fromUnixTime";
|
import fromUnixTime from "date-fns/fromUnixTime";
|
||||||
import Comment from "../components/Comment.svelte";
|
import Comment from "../components/Comment.svelte";
|
||||||
import StoryInfo from "../components/StoryInfo.svelte";
|
import Article from "../components/Article.svelte";
|
||||||
export let story;
|
export let story;
|
||||||
export let related;
|
export let related;
|
||||||
|
|
||||||
|
@ -23,25 +23,13 @@
|
||||||
</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;
|
||||||
}
|
}
|
||||||
|
.single {
|
||||||
|
max-width: 56rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
@ -54,41 +42,37 @@
|
||||||
<meta property="article:author" content={story.author || story.source} />
|
<meta property="article:author" content={story.author || story.source} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<article class="article">
|
<section class="single">
|
||||||
<header class="article-header">
|
<Article {story} />
|
||||||
<h1 class="article-title">{story.title}</h1>
|
|
||||||
<StoryInfo class="article-byline" {story} />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="article-body">
|
{#if hasComments}
|
||||||
{@html story.text}
|
<hr class="spacer" />
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{#if hasComments}
|
<section id="comments">
|
||||||
<hr class="spacer" />
|
<header>
|
||||||
|
<h2>Comments</h2>
|
||||||
|
|
||||||
<section class="comments" id="comments">
|
{#if others.length}
|
||||||
<header>
|
<h3>
|
||||||
<h2>Comments</h2>
|
Other discussions:
|
||||||
|
{#each others as r}
|
||||||
{#if others.length}
|
{#if r.num_comments}
|
||||||
<h3>
|
<a href="/{r.id}#comments" rel="prefetch">
|
||||||
Other discussions:
|
{r.source}
|
||||||
{#each others as r}
|
({r.num_comments})
|
||||||
{#if r.num_comments}
|
</a>
|
||||||
<a href="/{r.id}#comments" rel="prefetch">{r.source}</a>
|
{/if}
|
||||||
{/if}
|
{/each}
|
||||||
|
</h3>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
{#if story.comments.length}
|
||||||
|
<div class="comments">
|
||||||
|
{#each story.comments as comment}
|
||||||
|
<Comment {story} {comment} />
|
||||||
{/each}
|
{/each}
|
||||||
</h3>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</section>
|
||||||
{#if story.comments.length}
|
{/if}
|
||||||
<div class="comments">
|
</section>
|
||||||
{#each story.comments as comment}
|
|
||||||
<Comment {story} {comment} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 56em;
|
max-width: 64rem;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 2em;
|
padding: 0.5rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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://localhost:33842';
|
||||||
// const API_URL = process.env.API_URL || 'http://localhost:33842';
|
|
||||||
|
|
||||||
export async function get(req, res) {
|
export async function get(req, res) {
|
||||||
const { skip, limit } = {
|
const { skip, limit } = {
|
||||||
|
|
|
@ -16,74 +16,18 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { stores } from "@sapper/app";
|
import StoryList from "../components/StoryList.svelte";
|
||||||
import { getLogoUrl } from "../utils/logos.js";
|
import Pagination from "../components/Pagination.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>
|
|
||||||
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>
|
<svelte:head>
|
||||||
<title>QotNews</title>
|
<title>QotNews</title>
|
||||||
<meta property="og:title" content="QotNews" />
|
<meta property="og:title" content="QotNews" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section>
|
<StoryList {stories}>
|
||||||
{#each stories as story}
|
<Pagination href="/" count={stories.length} />
|
||||||
<article>
|
</StoryList>
|
||||||
<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>
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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://localhost:33842';
|
||||||
|
|
||||||
export async function get(req, res) {
|
export async function get(req, res) {
|
||||||
const { skip, limit } = {
|
const { skip, limit } = {
|
||||||
|
|
|
@ -20,84 +20,23 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { stores } from "@sapper/app";
|
import { stores } from "@sapper/app";
|
||||||
import { getLogoUrl } from "../utils/logos.js";
|
import StoryList from "../components/StoryList.svelte";
|
||||||
import StoryInfo from "../components/StoryInfo.svelte";
|
import Pagination from "../components/Pagination.svelte";
|
||||||
|
|
||||||
export let stories;
|
export let stories;
|
||||||
export let skip;
|
|
||||||
export let limit;
|
|
||||||
export let q;
|
|
||||||
|
|
||||||
const { page } = stores();
|
const { page } = stores();
|
||||||
|
|
||||||
page.subscribe((value) => {
|
|
||||||
skip = value.query.skip || 0;
|
|
||||||
limit = value.query.limit || 20;
|
|
||||||
q = value.query.query || "";
|
|
||||||
});
|
|
||||||
</script>
|
</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>
|
<svelte:head>
|
||||||
<title>QotNews</title>
|
<title>QotNews</title>
|
||||||
<meta property="og:title" content="QotNews" />
|
<meta property="og:title" content="QotNews" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section>
|
<StoryList {stories}>
|
||||||
{#each stories as story}
|
<Pagination
|
||||||
<article>
|
href="/search"
|
||||||
<header>
|
search="q={$page.query.q}"
|
||||||
<img
|
count={stories.length} />
|
||||||
src={getLogoUrl(story)}
|
</StoryList>
|
||||||
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>
|
|
||||||
|
|
|
@ -1,36 +1,38 @@
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen,
|
||||||
font-size: 14px;
|
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
line-height: 1.5;
|
font-size: 16px;
|
||||||
color: #333;
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
margin-bottom: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
margin: 0 0 0.5em 0;
|
h2,
|
||||||
font-weight: 400;
|
h3,
|
||||||
line-height: 1.2;
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0 0 0.5em 0;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: menlo, inconsolata, monospace;
|
font-family: menlo, inconsolata, monospace;
|
||||||
font-size: calc(1em - 2px);
|
font-size: calc(1em - 2px);
|
||||||
color: #555;
|
color: #555;
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 400px) {
|
|
||||||
body {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,6 +13,12 @@ import Article from './pages/Article.js';
|
||||||
import Comments from './pages/Comments.js';
|
import Comments from './pages/Comments.js';
|
||||||
import Results from './pages/Results.js';
|
import Results from './pages/Results.js';
|
||||||
|
|
||||||
|
const pagingKey = (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}`;
|
||||||
|
}
|
||||||
|
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -66,7 +72,7 @@ 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} key={Feed.key(props)} />} />
|
<Route path='/' exact render={(props) => <Feed {...props} updateCache={this.updateCache} key={pagingKey(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} />} />
|
||||||
|
|
|
@ -76,11 +76,4 @@ class Feed extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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