280 lines
8.4 KiB
Svelte
280 lines
8.4 KiB
Svelte
<script context="module">
|
|
export async function preload({ params, query }) {
|
|
// the `slug` parameter is available because
|
|
// this file is called [slug].svelte
|
|
const articleRes = await this.fetch(`a/${params.slug}.json`);
|
|
let article = await articleRes.json();
|
|
|
|
// parse article steps from numbered list
|
|
article.steps = [...article.html.matchAll(/>\d+\. ([^<]+)</g)]
|
|
.map(i => i[1])
|
|
.map(i => ({ "@type": "HowToStep", text: i }));
|
|
|
|
const commentsRes = await this.fetch(`a/${params.slug}/comments`);
|
|
const comments = await commentsRes.json();
|
|
|
|
if (articleRes.status === 200 && commentsRes.status === 200) {
|
|
return { article, comments };
|
|
} else if (articleRes.status !== 200) {
|
|
this.error(articleRes.status, article.message);
|
|
} else {
|
|
this.error(commentsRes.status, comments.message);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
import { stores } from '@sapper/app';
|
|
const { session } = stores();
|
|
export let article;
|
|
export let comments;
|
|
|
|
let author = '', content = '';
|
|
|
|
async function postComment()
|
|
{
|
|
const res = await fetch(`/a/${article.slug}/comments`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({ author, content })
|
|
});
|
|
const json = await res.json();
|
|
if (json.message) {
|
|
alert(json.message);
|
|
} else {
|
|
const res = await fetch(`/a/${article.slug}/comments`);
|
|
comments = await res.json();
|
|
author = content = '';
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
/*
|
|
By default, CSS is locally scoped to the component,
|
|
and any unused styles are dead-code-eliminated.
|
|
In this page, Svelte can't know which elements are
|
|
going to appear inside the {{{article.html}}} block,
|
|
so we have to use the :global(...) modifier to target
|
|
all elements inside .content
|
|
*/
|
|
:global(.article-content) {
|
|
font-size: 18px;
|
|
font-family: 'EB Garamond';
|
|
}
|
|
|
|
.content :global(h2) {
|
|
font-size: 1.4em;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.content :global(pre) {
|
|
background-color: #f9f9f9;
|
|
box-shadow: inset 1px 1px 5px rgba(0,0,0,0.05);
|
|
padding: 0.5em;
|
|
border-radius: 2px;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.content :global(pre) :global(code) {
|
|
background-color: transparent;
|
|
padding: 0;
|
|
}
|
|
|
|
.content :global(ul) {
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.content :global(li) {
|
|
margin: 0 0 0.5em 0;
|
|
}
|
|
|
|
.content :global(img) {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
div.article-image {
|
|
height: 12rem;
|
|
width: auto;
|
|
margin: -2rem;
|
|
margin-bottom: 1rem;
|
|
background: #81b0cd;
|
|
}
|
|
|
|
div.article-image img {
|
|
box-shadow: -5px 5px 1rem black;
|
|
height: 100%;
|
|
}
|
|
|
|
div.article-meta {
|
|
margin: 0 0 2rem 0;
|
|
padding: 0;
|
|
}
|
|
|
|
div.article-meta blockquote {
|
|
border-left: 2px solid gray;
|
|
padding-left: 0.5rem;
|
|
margin: 0;
|
|
}
|
|
|
|
div.article-meta h1 {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
form input, form textarea {
|
|
width: 100%;
|
|
}
|
|
|
|
span.comment-username {
|
|
font-weight: bold;
|
|
}
|
|
|
|
span.comment-verified {
|
|
font-style: italic;
|
|
}
|
|
|
|
div.comment {
|
|
background-color: rgb(150, 200, 234);
|
|
border: 1px solid #508FC3;
|
|
padding: 1rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
p.comment-meta {
|
|
margin: 0;
|
|
}
|
|
|
|
div.article-content {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
div.comment-content {
|
|
word-break: break-word;
|
|
}
|
|
|
|
@media (min-width: 800px) {
|
|
.content {
|
|
width: 75vw;
|
|
}
|
|
form input, form textarea {
|
|
width: 25%;
|
|
}
|
|
div.article-image {
|
|
height: 24rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<svelte:head>
|
|
<title>{article.title} | HOWFEED.BIZ</title>
|
|
|
|
<meta property="og:title" content={article.title}>
|
|
<meta property="og:type" content="article">
|
|
<meta property="og:locale" content="en_US">
|
|
<meta property="og:site_name" content="HowFeed">
|
|
<meta name="thumbnail" property="og:image" content={`https://howfeed.biz/a/${article.image}`}>
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta property="og:url" content={`https://howfeed.biz/a/${article.slug}`}>
|
|
<meta property="article:published_time" content={new Date(article.created_at).toISOString()}>
|
|
<meta name="pubdate" property="og:pubdate" content={new Date(article.created_at).toISOString()}>
|
|
{#if article.updated_at}
|
|
<meta property="article:modified_time" content={new Date(article.updated_at).toISOString()}>
|
|
{/if}
|
|
<meta property="article:section" content={article.category.name}>
|
|
|
|
<meta name="author" content={article.author.realname}>
|
|
{#if article.description}
|
|
<meta name="description" property="og:description" content={article.description}>
|
|
{/if}
|
|
{@html `<script type="application/ld+json">{
|
|
"@context": "https://schema.org",
|
|
"@type": "BreadcrumbList",
|
|
"itemListElement": [{
|
|
"@type": "ListItem",
|
|
"position": 1,
|
|
"name": "HowFeed.biz",
|
|
"item": "https://howfeed.biz"
|
|
},{
|
|
"@type": "ListItem",
|
|
"position": 2,
|
|
"name": "${article.category.name}",
|
|
"item": "https://howfeed.biz/c/${article.category.slug}"
|
|
},{
|
|
"@type": "ListItem",
|
|
"position": 3,
|
|
"name": "${article.title}",
|
|
"item": "https://howfeed.biz/a/${article.slug}"
|
|
}]
|
|
}</script>`}
|
|
{@html `<script type="application/ld+json">{
|
|
"@context": "http://schema.org",
|
|
"@type": "HowTo",
|
|
"image": {
|
|
"@type": "ImageObject",
|
|
"url": "https://howfeed.biz/a/${article.image}"
|
|
},
|
|
"name": ${JSON.stringify(article.title)},
|
|
"step": ${JSON.stringify(article.steps)}
|
|
}</script>`}
|
|
</svelte:head>
|
|
|
|
<div class="content">
|
|
<div class="article-image">
|
|
<img alt={article.title} src={`/a/${article.image}`}>
|
|
</div>
|
|
<!-- ad goes here -->
|
|
<div class="article-meta">
|
|
<h1 class="article-title">{article.title}</h1>
|
|
<blockquote>
|
|
<p>Author: <img class="avatar" alt={article.author.realname} src={`/u/${article.author.avatar || 'default.jpg'}`}> <strong>{article.author.realname}</strong></p>
|
|
<p>Category: <strong><a href={`/c/${article.category.slug}`}>{article.category.name}</a></strong></p>
|
|
<p>Published: <strong>{new Date(article.created_at).toLocaleString()}</strong></p>
|
|
{#if article.updated_at}
|
|
<p>Last Updated: <strong>{new Date(article.updated_at).toLocaleString()}</strong></p>
|
|
{/if}
|
|
<p>Views: <strong>{article.views}</strong></p>
|
|
</blockquote>
|
|
</div>
|
|
<div class="article-content">
|
|
{@html article.html}
|
|
</div>
|
|
<!-- ad goes here -->
|
|
<hr>
|
|
<h3>Comments</h3>
|
|
<div class="comments">
|
|
{#each comments as comment}
|
|
<div class="comment">
|
|
{#if comment.author_user}
|
|
<img class="avatar" alt={comment.author_user.realname} src={`/u/${comment.author_user.avatar || 'default.jpg'}`}>
|
|
{/if}
|
|
<p class="comment-meta">
|
|
{#if comment.author_user}
|
|
<span class="comment-username">{comment.author_user.realname}</span> <span class="comment-verified">(verified)</span> - {new Date(comment.created_at).toLocaleString()}
|
|
{:else}
|
|
<span class="comment-username">{comment.author}</span> - {new Date(comment.created_at).toLocaleString()}
|
|
{/if}
|
|
</p>
|
|
<div class="comment-content">{comment.content}</div>
|
|
</div>
|
|
{:else}
|
|
<p>No comments.</p>
|
|
{/each}
|
|
</div>
|
|
<h3>Add a Comment</h3>
|
|
<form method="POST" action={`/a/${article.slug}/comments`}>
|
|
<p>Name:
|
|
{#if $session.user}
|
|
<input type="text" disabled value={$session.user.realname}>
|
|
{:else}
|
|
<input type="text" bind:value={author} name="author" maxlength="100" placeholder="Anonymous">
|
|
{/if}
|
|
</p>
|
|
<p><textarea name="content" bind:value={content} maxlength="5000"></textarea></p>
|
|
<p><button type="submit" on:click|preventDefault={postComment}>Submit</button></p>
|
|
</form>
|
|
</div>
|