master
Jason Schwarzenberger 4 years ago
parent 60e34935ee
commit 8115d86335
  1. 52
      webapp/src/components/Article.svelte
  2. 11
      webapp/src/components/Comment.svelte
  3. 136
      webapp/src/components/Nav.svelte
  4. 55
      webapp/src/components/Pagination.svelte
  5. 36
      webapp/src/components/StoryInfo.svelte
  6. 55
      webapp/src/components/StoryList.svelte
  7. 11
      webapp/src/components/Time.svelte
  8. 1
      webapp/src/routes/[id].json.js
  9. 84
      webapp/src/routes/[id].svelte
  10. 4
      webapp/src/routes/_layout.svelte
  11. 3
      webapp/src/routes/index.json.js
  12. 66
      webapp/src/routes/index.svelte
  13. 2
      webapp/src/routes/search.json.js
  14. 77
      webapp/src/routes/search.svelte
  15. 48
      webapp/static/global.css
  16. 8
      webclient/src/App.js
  17. 7
      webclient/src/pages/Feed.js

@ -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>
&bull; &bull;
<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>

@ -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}&bull; {story.score} points{/if}
on {#if story.num_comments}
<a href={story.url}>{story.source}</a> &bull;
{#if story.score}&bull; {story.score} points{/if} <a rel="prefetch" href="/{story.id}#comments">{story.num_comments}
{#if story.num_comments} comments</a>
&bull; {/if}
<a rel="prefetch" href="/{story.id}#comments">{story.num_comments}
comments</a>
{/if}
</div>

@ -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 />

@ -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">
{@html story.text}
</section>
</article>
{#if hasComments} {#if hasComments}
<hr class="spacer" /> <hr class="spacer" />
<section class="comments" id="comments"> <section id="comments">
<header> <header>
<h2>Comments</h2> <h2>Comments</h2>
{#if others.length} {#if others.length}
<h3> <h3>
Other discussions: Other discussions:
{#each others as r} {#each others as r}
{#if r.num_comments} {#if r.num_comments}
<a href="/{r.id}#comments" rel="prefetch">{r.source}</a> <a href="/{r.id}#comments" rel="prefetch">
{/if} {r.source}
({r.num_comments})
</a>
{/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…
Cancel
Save