forked from tanner/qotnews
		
	fun.
This commit is contained in:
		
							
								
								
									
										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> | ||||
|   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> | ||||
|     • | ||||
|     <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> | ||||
|   | ||||
							
								
								
									
										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> | ||||
|   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}• {story.score} points{/if} | ||||
|   {#if story.num_comments} | ||||
|     • | ||||
|     <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}• {story.score} points{/if} | ||||
| {#if story.num_comments} | ||||
|   • | ||||
|   <a rel="prefetch" href="/{story.id}#comments">{story.num_comments} | ||||
|     comments</a> | ||||
| {/if} | ||||
|   | ||||
							
								
								
									
										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'; | ||||
|  | ||||
| // 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="single"> | ||||
|   <Article {story} /> | ||||
|  | ||||
|   <section class="article-body"> | ||||
|     {@html story.text} | ||||
|   </section> | ||||
| </article> | ||||
|   {#if hasComments} | ||||
|     <hr class="spacer" /> | ||||
|  | ||||
| {#if hasComments} | ||||
|   <hr class="spacer" /> | ||||
|     <section id="comments"> | ||||
|       <header> | ||||
|         <h2>Comments</h2> | ||||
|  | ||||
|   <section class="comments" 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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user