master
Jason Schwarzenberger 3 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>
import fromUnixTime from "date-fns/fromUnixTime";
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import Time from "../components/Time.svelte";
export let story;
export let comment;
export let showComments = true;
export let dateString = formatDistanceToNow(fromUnixTime(comment.date), {
addSuffix: true,
});
const author = (comment.author || "").replace(" ", "");
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>
&bull;
<a class="time-link" href="{story.id}#comment-{id}">
<time
datetime={fromUnixTime(comment.date).toISOString()}
title={fromUnixTime(comment.date)}>{dateString}</time>
<Time date={comment.date} />
</a>
{#if comment.comments.length}
<button

@ -1,19 +1,24 @@
<script>
import { debounce } from 'lodash';
import { goto, prefetch } from "@sapper/app";
import { stores } from "@sapper/app";
import { debounce } from "lodash";
import { goto, prefetch, stores } from "@sapper/app";
export let segment;
const { page } = stores();
let q;
let search;
page.subscribe((value) => {
q = value.query.q || "";
let handleSearch = debounce(_handleSearch, 300, {
trailing: true,
leading: false,
});
let handleSearch = debounce(_handleSearch);
page.subscribe((page) => {
setTimeout(() => {
if (segment === "search") {
search && search.focus();
}
}, 0);
});
async function _handleSearch(event) {
const url = `/search?q=${event.target.value}`;
@ -23,35 +28,12 @@
</script>
<style>
nav {
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] {
.navigation [aria-current] {
position: relative;
display: inline-block;
}
[aria-current]::after {
.navigation [aria-current]::after {
position: absolute;
content: "";
width: calc(100% - 1em);
@ -61,38 +43,78 @@
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;
padding: 1em 0.5em;
display: block;
}
input {
.navigation-input {
line-height: 2;
margin: 1em;
vertical-align: middle;
width: 15rem;
}
</style>
<nav>
<ul>
<li>
<a
aria-current={segment === undefined ? 'page' : undefined}
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 class="navigation">
<div class="navigation-container">
<ul class="navigation-list" role="menubar">
<li class="navigation-item">
<a
class="navigation-link"
aria-current={segment === undefined ? 'page' : undefined}
rel="prefetch"
href=".">News</a>
</li>
<li class="navigation-item">
<a
class="navigation-link"
aria-current={segment === 'search' ? 'page' : undefined}
rel="prefetch"
href="/search">Search</a>
</li>
</ul>
<form action="/search" method="GET" rel="prefetch" role="search">
<input
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>

@ -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>
import fromUnixTime from "date-fns/fromUnixTime";
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import Time from "../components/Time.svelte";
export let story;
export let dateString = formatDistanceToNow(fromUnixTime(story.date), {
addSuffix: true,
});
</script>
<div class="info">
<time
datetime={fromUnixTime(story.date).toISOString()}
title={fromUnixTime(story.date)}>{dateString}</time>
{#if story.author && story.author_link}
by
<a href={story.author_link}>{story.author}</a>
{:else if story.author}by {story.author}{/if}
on
<a href={story.url}>{story.source}</a>
{#if story.score}&bull; {story.score} points{/if}
{#if story.num_comments}
&bull;
<a rel="prefetch" href="/{story.id}#comments">{story.num_comments}
comments</a>
{/if}
</div>
<Time date={story.date} />
{#if story.author && story.author_link}
by
<a href={story.author_link}>{story.author}</a>
{:else if story.author}by {story.author}{/if}
on
<a href={story.link || story.url}>{story.source}</a>
{#if story.score}&bull; {story.score} points{/if}
{#if story.num_comments}
&bull;
<a rel="prefetch" href="/{story.id}#comments">{story.num_comments}
comments</a>
{/if}

@ -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';
// 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) {

@ -14,7 +14,7 @@
<script>
import fromUnixTime from "date-fns/fromUnixTime";
import Comment from "../components/Comment.svelte";
import StoryInfo from "../components/StoryInfo.svelte";
import Article from "../components/Article.svelte";
export let story;
export let related;
@ -23,25 +23,13 @@
</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;
}
.single {
max-width: 56rem;
margin: 0 auto;
}
</style>
<svelte:head>
@ -54,41 +42,37 @@
<meta property="article:author" content={story.author || story.source} />
</svelte:head>
<article class="article">
<header class="article-header">
<h1 class="article-title">{story.title}</h1>
<StoryInfo class="article-byline" {story} />
</header>
<section class="article-body">
{@html story.text}
</section>
</article>
<section class="single">
<Article {story} />
{#if hasComments}
<hr class="spacer" />
{#if hasComments}
<hr class="spacer" />
<section class="comments" id="comments">
<header>
<h2>Comments</h2>
<section id="comments">
<header>
<h2>Comments</h2>
{#if others.length}
<h3>
Other discussions:
{#each others as r}
{#if r.num_comments}
<a href="/{r.id}#comments" rel="prefetch">{r.source}</a>
{/if}
{#if others.length}
<h3>
Other discussions:
{#each others as r}
{#if r.num_comments}
<a href="/{r.id}#comments" rel="prefetch">
{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}
</h3>
</div>
{/if}
</header>
{#if story.comments.length}
<div class="comments">
{#each story.comments as comment}
<Comment {story} {comment} />
{/each}
</div>
{/if}
</section>
{/if}
</section>
{/if}
</section>

@ -6,9 +6,9 @@
<style>
main {
position: relative;
max-width: 56em;
max-width: 64rem;
background-color: white;
padding: 2em;
padding: 0.5rem;
margin: 0 auto;
box-sizing: border-box;
}

@ -1,7 +1,6 @@
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) {
const { skip, limit } = {

@ -16,74 +16,18 @@
</script>
<script>
import { stores } from "@sapper/app";
import { getLogoUrl } from "../utils/logos.js";
import StoryInfo from "../components/StoryInfo.svelte";
import StoryList from "../components/StoryList.svelte";
import Pagination from "../components/Pagination.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>
<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>
<StoryList {stories}>
<Pagination href="/" count={stories.length} />
</StoryList>

@ -1,6 +1,6 @@
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 { skip, limit } = {

@ -20,84 +20,23 @@
<script>
import { stores } from "@sapper/app";
import { getLogoUrl } from "../utils/logos.js";
import StoryInfo from "../components/StoryInfo.svelte";
import StoryList from "../components/StoryList.svelte";
import Pagination from "../components/Pagination.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>
<StoryList {stories}>
<Pagination
href="/search"
search="q={$page.query.q}"
count={stories.length} />
</StoryList>

@ -1,36 +1,38 @@
body {
margin: 0;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
margin: 0;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-size: 16px;
line-height: 1.5;
color: #333;
margin-bottom: 50vh;
}
h1, h2, h3, h4, h5, h6 {
margin: 0 0 0.5em 0;
font-weight: 400;
line-height: 1.2;
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 0.5em 0;
font-weight: 400;
line-height: 1.2;
}
h1 {
font-size: 2em;
font-size: 2em;
}
a {
color: inherit;
color: inherit;
}
code {
font-family: menlo, inconsolata, monospace;
font-size: calc(1em - 2px);
color: #555;
background-color: #f0f0f0;
padding: 0.2em 0.4em;
border-radius: 2px;
font-family: menlo, inconsolata, monospace;
font-size: calc(1em - 2px);
color: #555;
background-color: #f0f0f0;
padding: 0.2em 0.4em;
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 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 {
constructor(props) {
@ -66,7 +72,7 @@ class App extends React.Component {
<Route path='/(|search)' component={Submit} />
</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>
<Route path='/search' component={Results} />
<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;

Loading…
Cancel
Save