article edit feature

This commit is contained in:
James Shiffer 2020-09-01 21:30:40 -07:00
parent cd878c2b25
commit 60a76979df
No known key found for this signature in database
GPG Key ID: C0DB8774A1B3BA45
7 changed files with 123 additions and 39 deletions

View File

@ -11,6 +11,7 @@ const ArticleSchema = new Schema({
slug: { type: String, index: { unique: true } }, slug: { type: String, index: { unique: true } },
image: { type: String, required: true }, image: { type: String, required: true },
created_at: { type: Date, default: Date.now }, created_at: { type: Date, default: Date.now },
updated_at: { type: Date },
html: { type: String, required: true }, html: { type: String, required: true },
comments: [{ comments: [{
content: { type: String, required: true }, content: { type: String, required: true },

View File

@ -12,7 +12,9 @@ export async function get(req, res, next) {
}); });
if (article) { if (article) {
article.set({ views: article.views + 1 }); if (req.query.no_view !== '1') {
article.set({ views: article.views + 1 });
}
await article.save(); await article.save();
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'

View File

@ -214,6 +214,9 @@
<p>Author: <img class="avatar" alt={article.author.realname} src={`/u/${article.author.avatar || 'default.jpg'}`}> <strong>{article.author.realname}</strong></p> <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>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> <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> <p>Views: <strong>{article.views}</strong></p>
</blockquote> </blockquote>
</div> </div>

View File

@ -6,7 +6,9 @@
} }
const res = await this.fetch('/c.json'); const res = await this.fetch('/c.json');
const categories = await res.json(); const categories = await res.json();
return { user: session.user, categories }; const origRes = page.query.edit && await this.fetch(`/a/${page.query.edit}.json?no_view=1`)
const editArticle = origRes && await origRes.json();
return { user: session.user, categories, editArticle };
} }
</script> </script>
@ -20,6 +22,7 @@
let editor, form, uploadForm; let editor, form, uploadForm;
let loading = false, loadingAttach = false; let loading = false, loadingAttach = false;
let title = '', category = ''; let title = '', category = '';
export let editArticle = undefined;
export let categories; export let categories;
let actions = [ let actions = [
@ -55,9 +58,9 @@
]; ];
onMount(function load() { onMount(function load() {
title = window.localStorage['title'] || ''; title = editArticle ? editArticle.title : (window.localStorage['title'] || '');
category = window.localStorage['category'] || ''; category = editArticle ? editArticle.category.slug : (window.localStorage['category'] || '');
editor.setHtml(window.localStorage['html'] || '', false); editor.setHtml(editArticle ? editArticle.html : (window.localStorage['html'] || ''), false);
}); });
async function submit() async function submit()
@ -67,7 +70,7 @@
fd.append('html', html); fd.append('html', html);
loading = true; loading = true;
try { try {
const res = await fetch(`/cms/article`, { const res = await fetch(editArticle ? `/cms/article/${editArticle.slug}` : `/cms/article`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json' 'Accept': 'application/json'

View File

@ -0,0 +1,42 @@
<script context="module">
export async function preload(page, session)
{
if (!session.user || !session.user.author) {
return this.redirect(302, '/cms');
}
const res = await this.fetch(`/c/all.json`);
const json = await res.json();
if (res.status === 200) {
return { articles: json.articles, user: session.user };
} else {
this.error(res.status, json.message);
}
}
</script>
<svelte:head>
<title>Edit Article | HOWFEED.BIZ</title>
</svelte:head>
<script>
import { goto } from '@sapper/app';
export let articles = [];
</script>
<div class="content">
<a href="/cms">&lt; Back to Dashboard</a>
<h1>Edit an Article</h1>
<p>Choose an article to edit:</p>
<ul>
{#each articles as article}
<li>
<a href={`/cms/create?edit=${encodeURIComponent(article.slug)}`}>
<strong>{article.title}</strong> by <strong>{article.author.realname}</strong>
({article.views} views)
</a>
</li>
{:else}
<li>There are no published articles.</li>
{/each}
</ul>
</div>

View File

@ -22,7 +22,7 @@
{#if user.author} {#if user.author}
<h1>HowFeed Publisher Dashboard</h1> <h1>HowFeed Publisher Dashboard</h1>
<p><a href="/cms/create">Publish a new article</a></p> <p><a href="/cms/create">Publish a new article</a></p>
<p><strike><a href="/cms/update">Edit an existing article</a></strike> Coming soon!</p> <p><a href="/cms/edit">Edit an existing article</a></p>
<p><a href="/cms/delete">Delete an article</a></p> <p><a href="/cms/delete">Delete an article</a></p>
<h1>Account Settings</h1> <h1>Account Settings</h1>
<p><a href="/me/avatar">Change your avatar</a></p> <p><a href="/me/avatar">Change your avatar</a></p>

View File

@ -251,13 +251,27 @@ express()
}); });
}) })
.post('/cms/article', .post('/cms/article/:edit?',
isAuthor, isAuthor,
async function(req, res, next) { async function(req, res, next) {
try { try {
let editArticle;
if (req.params.edit) {
editArticle = await Article.findOne({ slug: req.params.edit });
if (!editArticle) {
res.writeHead(404, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Article to edit not found`
}));
return false;
}
}
const { html, title, category } = req.body; const { html, title, category } = req.body;
const { image } = req.files; const image = req.files && req.files.image;
if (!title || !image || !html || !category) { if (!title || (!editArticle && !image) || !html || !category) {
res.writeHead(422, { res.writeHead(422, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
@ -266,23 +280,30 @@ express()
})); }));
return false; return false;
} }
if (!/^image\//.test(image.mimetype)) { let ext, filename, url;
res.writeHead(422, { if (image) {
'Content-Type': 'application/json' if (!/^image\//.test(image.mimetype)) {
}); res.writeHead(422, {
res.end(JSON.stringify({ 'Content-Type': 'application/json'
message: `Invalid MIME type for the header image file.` });
})); res.end(JSON.stringify({
return false; message: `Invalid MIME type for the header image file.`
} }));
if (image.truncated) { return false;
res.writeHead(422, { }
'Content-Type': 'application/json' if (image.truncated) {
}); res.writeHead(422, {
res.end(JSON.stringify({ 'Content-Type': 'application/json'
message: `Received truncated image file. Try again with a smaller file.` });
})); res.end(JSON.stringify({
return false; message: `Received truncated image file. Try again with a smaller file.`
}));
return false;
}
ext = image.name.match(/(\.[^.]+)$/)[0];
filename = crypto.randomBytes(20).toString('hex') + ext;
url = `/a/${filename}`;
await image.mv('./static' + url);
} }
const cat = await Category.findOne({ slug: category }); const cat = await Category.findOne({ slug: category });
if (!cat) { if (!cat) {
@ -294,18 +315,30 @@ express()
})); }));
return false; return false;
} }
const ext = image.name.match(/(\.[^.]+)$/)[0]; if (editArticle) {
const filename = crypto.randomBytes(20).toString('hex') + ext; let newObj = {
const url = `/a/${filename}`; html, title, category: cat, updated_at: Date.now()
await image.mv('./static' + url); };
const article = await new Article({ html, title, image: filename, category: cat, author: req.user._id }); if (filename) {
await article.save(); newObj.image = filename;
res.writeHead(200, { }
'Content-Type': 'application/json' await Article.updateOne({ slug: editArticle.slug }, { $set: newObj });
}); res.writeHead(200, {
res.end(JSON.stringify({ 'Content-Type': 'application/json'
slug: article.slug });
})); res.end(JSON.stringify({
slug: editArticle.slug
}));
} else {
const article = await new Article({ html, title, image: filename, category: cat, author: req.user._id });
await article.save();
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
slug: article.slug
}));
}
} catch (err) { } catch (err) {
res.writeHead(500, { res.writeHead(500, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'