image uploads, changed design

This commit is contained in:
James Shiffer 2020-06-24 10:52:01 -07:00
parent 5f7779f99b
commit 26799bc5bc
No known key found for this signature in database
GPG Key ID: C0DB8774A1B3BA45
12 changed files with 86 additions and 48 deletions

View File

@ -15,13 +15,16 @@
<style> <style>
nav { nav {
font-weight: bold; font-weight: bold;
padding: 1rem 0 0 0; position: absolute;
position: fixed; padding: 0.5rem;
width: 100%; margin: 0 auto;
width: 85%;
z-index: 100; z-index: 100;
background-color: #fff; background-color: white;
top: 0; top: 1rem;
box-shadow: 0 -2px 5px #000; left: 0;
right: 0;
box-shadow: 0 2px 0.5rem black;
} }
div.items { div.items {
margin: 0; margin: 0;
@ -29,12 +32,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-transform: uppercase; text-transform: uppercase;
padding: 0 1.5rem 0.25rem; align-items: center;
align-items: start;
} }
div.search { div.search {
flex: 1 0 0; flex: 1 0 0;
margin: auto 0;
display: flex; display: flex;
} }
div.link a { div.link a {
@ -50,7 +51,6 @@
@media (min-width: 800px) { @media (min-width: 800px) {
div.items { div.items {
flex-direction: row; flex-direction: row;
align-items: end;
} }
div.link a { div.link a {
font-size: 2rem !important; font-size: 2rem !important;
@ -62,10 +62,18 @@
} }
input.search { input.search {
width: 100%; width: 100%;
height: 1.5rem; height: 2.75rem;
font-size: 1rem; font-size: 2rem;
margin: 0 auto; margin: 0 auto;
text-align: center; border: 0.25rem solid black;
}
input.search:focus::placeholder {
opacity: 0;
}
input.search::placeholder {
opacity: 1;
font-weight: bold;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
} }
</style> </style>

View File

@ -55,8 +55,14 @@ export async function del(req, res, next) {
const article = await Article.findOneAndDelete({ slug }); const article = await Article.findOneAndDelete({ slug });
if (article) { if (article) {
res.writeHead(204); const articles = await Article.find()
res.end(); .sort({ created_at: 'desc' })
.populate({ path: 'category' })
.populate({ path: 'author', select: 'realname' });
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({ category: 'all', articles }));
} else { } else {
res.writeHead(404, { res.writeHead(404, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'

View File

@ -142,7 +142,7 @@
<div class="content"> <div class="content">
<figure class="article-image"> <figure class="article-image">
<img alt={article.title} src={article.image}> <img alt={article.title} src={`/a/${article.image}`}>
</figure> </figure>
<div class="article-meta"> <div class="article-meta">
<h1 class="article-title">{article.title}</h1> <h1 class="article-title">{article.title}</h1>

View File

@ -24,7 +24,6 @@
<style> <style>
h1 { h1 {
margin: 0 auto; margin: 0 auto;
color: whitesmoke;
margin-top: 1rem; margin-top: 1rem;
font-size: 2rem; font-size: 2rem;
font-size: 3rem; font-size: 3rem;
@ -55,7 +54,7 @@
{#each articles as {title, slug, image, created_at}} {#each articles as {title, slug, image, created_at}}
<a rel="prefetch" href={`/a/${slug}`}> <a rel="prefetch" href={`/a/${slug}`}>
<figure class="article-image"> <figure class="article-image">
<img src={image || '/logo.png'} alt={title}> <img src={image ? `/a/${image}` : '/logo.png'} alt={title}>
</figure> </figure>
<div class="article-meta"> <div class="article-meta">
<p class="article-title">{title}</p> <p class="article-title">{title}</p>

View File

@ -17,8 +17,8 @@
const { session } = stores(); const { session } = stores();
let editor; let editor, form;
let title = '', image = '', category = ''; let title = '', category = '';
export let categories; export let categories;
let actions = [ let actions = [
@ -28,7 +28,6 @@
title: 'Save', title: 'Save',
result: function save() { result: function save() {
window.localStorage['title'] = title; window.localStorage['title'] = title;
window.localStorage['image'] = image;
window.localStorage['category'] = category; window.localStorage['category'] = category;
window.localStorage['html'] = editor.getHtml(true); window.localStorage['html'] = editor.getHtml(true);
alert('Successfully saved draft to browser local storage'); alert('Successfully saved draft to browser local storage');
@ -56,7 +55,6 @@
onMount(function load() { onMount(function load() {
title = window.localStorage['title'] || ''; title = window.localStorage['title'] || '';
image = window.localStorage['image'] || '';
category = window.localStorage['category'] || ''; category = window.localStorage['category'] || '';
editor.setHtml(window.localStorage['html'] || '', false); editor.setHtml(window.localStorage['html'] || '', false);
}); });
@ -64,13 +62,14 @@
async function submit() async function submit()
{ {
let html = editor.getHtml(true); let html = editor.getHtml(true);
let fd = new FormData(form);
fd.append('html', html);
const res = await fetch(`/cms/article`, { const res = await fetch(`/cms/article`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json'
'Content-Type': 'application/json'
}, },
body: JSON.stringify({ html, image, title, category }) body: fd
}); });
const json = await res.json(); const json = await res.json();
if (res.status === 200) { if (res.status === 200) {
@ -116,19 +115,19 @@
<div class="content"> <div class="content">
<a href="/cms">&lt; Back to Dashboard</a> <a href="/cms">&lt; Back to Dashboard</a>
<h1>HowFeed Publisher</h1> <h1>HowFeed Publisher</h1>
<form method="POST" action="/cms/article"> <form enctype="multipart/form-data" method="POST" action="/cms/article" bind:this={form}>
<p>Article Title: <input type="text" name="title" bind:value={title} required placeholder="How to Assassinate the Governor of California"></p> <p>Article Title: <input type="text" name="title" bind:value={title} required placeholder="How to Assassinate the Governor of California"></p>
<p>Article Author: <strong>{$session.user.realname}</strong></p> <p>Article Author: <strong>{$session.user.realname}</strong></p>
<p>Article Category: <p>Article Category:
{#if categories.length} {#if categories.length}
<select bind:value={category}> <select name="category" bind:value={category}>
{#each categories as { name, slug }} {#each categories as { name, slug }}
<option value={slug}>{name}</option> <option value={slug}>{name}</option>
{/each} {/each}
</select> </select>
{/if} {/if}
<button on:click|preventDefault={addCategory}>+</button></p> <button on:click|preventDefault={addCategory}>+</button></p>
<p>Article Header Image URL: <input type="text" name="image" bind:value={image} required placeholder="http:// ..."></p> <p>Article Header Image: <input type="file" name="image" accept="image/*" required placeholder="http:// ..."></p>
</form> </form>
<Editor bind:this={editor} {actions} /> <Editor bind:this={editor} {actions} />
<button on:click={submit}>Submit</button> <button on:click={submit}>Submit</button>

View File

@ -25,11 +25,15 @@
async function del(article) async function del(article)
{ {
if (confirm(`Are you sure you want to delete "${article.title}"?`)) { if (confirm(`Are you sure you want to delete "${article.title}"?`)) {
await fetch(`/a/${article.slug}.json`, { const res = await fetch(`/a/${article.slug}.json`, {
method: 'DELETE' method: 'DELETE'
}); });
const res = await fetch(`/c/all.json`); const json = await res.json();
articles = await res.json(); if (res.status === 200) {
articles = json.articles;
} else {
alert(`Error ${res.status}: ${json.message}`);
}
} }
} }
</script> </script>

View File

@ -35,11 +35,8 @@
font-size: 3.5rem !important; font-size: 3.5rem !important;
} }
} }
h1.welcome, h2.desc {
color: whitesmoke;
}
h1.welcome { h1.welcome {
margin-top: 1rem; margin: 1rem 0;
font-size: 3.75rem; font-size: 3.75rem;
font-size: 3rem; font-size: 3rem;
text-transform: uppercase; text-transform: uppercase;
@ -58,12 +55,11 @@
<div class="background"></div> <div class="background"></div>
<div class="floaty"> <div class="floaty">
<h1 class="welcome">Welcome</h1> <h1 class="welcome">Welcome</h1>
<h2 class="desc">Find an Article</h2>
<div class="article-list"> <div class="article-list">
{#each articles as {title, slug, image, created_at}} {#each articles as {title, slug, image, created_at}}
<a rel="prefetch" href={`/a/${slug}`}> <a rel="prefetch" href={`/a/${slug}`}>
<figure class="article-image"> <figure class="article-image">
<img src={image || '/logo.png'} alt={title}> <img src={image ? `/a/${image}` : '/logo.png'} alt={title}>
</figure> </figure>
<div class="article-meta"> <div class="article-meta">
<p class="article-title">{title}</p> <p class="article-title">{title}</p>

View File

@ -20,7 +20,6 @@
<style> <style>
h1 { h1 {
margin: 0 auto; margin: 0 auto;
color: whitesmoke;
margin-top: 1rem; margin-top: 1rem;
font-size: 2rem; font-size: 2rem;
font-size: 3rem; font-size: 3rem;
@ -41,7 +40,7 @@
{#each results as {title, slug, image, created_at}} {#each results as {title, slug, image, created_at}}
<a rel="prefetch" href={`/a/${slug}`}> <a rel="prefetch" href={`/a/${slug}`}>
<figure class="article-image"> <figure class="article-image">
<img src={image || '/logo.png'} alt={title}> <img src={image ? `/a/${image}` : '/logo.png'} alt={title}>
</figure> </figure>
<div class="article-meta"> <div class="article-meta">
<p class="article-title">{title}</p> <p class="article-title">{title}</p>

View File

@ -11,6 +11,7 @@ import sessionFileStore from 'session-file-store';
import { RateLimiterMemory } from 'rate-limiter-flexible'; import { RateLimiterMemory } from 'rate-limiter-flexible';
import fileUpload from 'express-fileupload'; import fileUpload from 'express-fileupload';
import helmet from 'helmet'; import helmet from 'helmet';
import crypto from 'crypto';
import Article from './models/article.js'; import Article from './models/article.js';
import Category from './models/category.js'; import Category from './models/category.js';
import User from './models/user.js'; import User from './models/user.js';
@ -109,7 +110,9 @@ express()
.use(helmet()) .use(helmet())
.use(bodyParser.json()) .use(bodyParser.json())
.use(bodyParser.urlencoded({ extended: true })) .use(bodyParser.urlencoded({ extended: true }))
.use(fileUpload()) .use(fileUpload({
limits: { fileSize: 16000000 }
}))
.use(session({ .use(session({
secret: SESSION_SECRET, secret: SESSION_SECRET,
resave: false, resave: false,
@ -248,14 +251,34 @@ express()
isAuthor, isAuthor,
async function(req, res, next) { async function(req, res, next) {
try { try {
const { html, title, image, category } = req.body; const { html, title, category } = req.body;
const { image } = req.files;
if (!title || !image || !html || !category) { if (!title || !image || !html || !category) {
res.writeHead(422, { res.writeHead(422, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
res.end(JSON.stringify({ res.end(JSON.stringify({
message: `You must supply an article title, image URL, category, and content.` message: `You must supply an article title, header image file, category, and content.`
})); }));
return false;
}
if (!/^image\//.test(image.mimetype)) {
res.writeHead(422, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Invalid MIME type for the header image file.`
}));
return false;
}
if (image.truncated) {
res.writeHead(422, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Received truncated image file. Try again with a smaller file.`
}));
return false;
} }
const cat = await Category.findOne({ slug: category }); const cat = await Category.findOne({ slug: category });
if (!cat) { if (!cat) {
@ -265,8 +288,12 @@ express()
res.end(JSON.stringify({ res.end(JSON.stringify({
message: `That category does not exist.` message: `That category does not exist.`
})); }));
return false;
} }
const article = await new Article({ html, title, image, category: cat, author: req.user._id }); const ext = image.name.match(/(\.[^.]+)$/)[0];
const filename = crypto.randomBytes(20).toString('hex') + ext;
await image.mv('./static/a/' + filename);
const article = await new Article({ html, title, image: filename, category: cat, author: req.user._id });
await article.save(); await article.save();
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'application/json' 'Content-Type': 'application/json'

View File

@ -3,8 +3,8 @@
<head> <head>
<meta charset='utf-8'> <meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1.0'> <meta name='viewport' content='width=device-width,initial-scale=1.0'>
<meta name='description' content='HOWFEED.BIZ: Where we break the news.'> <meta name='description' content='HOWFEED.BIZ: Where we break the news.'>
<meta name='keywords' content='news, satire, blog'> <meta name='keywords' content='news, satire, blog'>
<meta name='theme-color' content='#508FC3'> <meta name='theme-color' content='#508FC3'>
%sapper.base% %sapper.base%
<link rel='stylesheet' href='global.css'> <link rel='stylesheet' href='global.css'>

BIN
static/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -113,7 +113,7 @@ div.article-meta {
} }
div.background { div.background {
background: url('/cityscape.jpg') no-repeat center; background: url('/background.png') no-repeat center;
background-size: cover; background-size: cover;
background-attachment: fixed; background-attachment: fixed;
position: fixed; position: fixed;
@ -124,14 +124,14 @@ div.background {
} }
div.article-list { div.article-list {
box-shadow: 0 0 5px #000; box-shadow: 0 -5px 0.75rem black;
} }
div.article-meta { div.article-meta {
font-weight: bold; font-weight: bold;
} }
div.floaty { div.floaty {
padding-top: 8rem; padding-top: 16rem;
padding-bottom: 4rem; padding-bottom: 4rem;
position: absolute; position: absolute;
z-index: 1; z-index: 1;