image uploads, changed design
This commit is contained in:
parent
5f7779f99b
commit
26799bc5bc
@ -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>
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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">< Back to Dashboard</a>
|
<a href="/cms">< 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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
@ -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
BIN
static/background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user