image uploads, changed design
This commit is contained in:
parent
5f7779f99b
commit
26799bc5bc
@ -15,13 +15,16 @@
|
||||
<style>
|
||||
nav {
|
||||
font-weight: bold;
|
||||
padding: 1rem 0 0 0;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
padding: 0.5rem;
|
||||
margin: 0 auto;
|
||||
width: 85%;
|
||||
z-index: 100;
|
||||
background-color: #fff;
|
||||
top: 0;
|
||||
box-shadow: 0 -2px 5px #000;
|
||||
background-color: white;
|
||||
top: 1rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 2px 0.5rem black;
|
||||
}
|
||||
div.items {
|
||||
margin: 0;
|
||||
@ -29,12 +32,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-transform: uppercase;
|
||||
padding: 0 1.5rem 0.25rem;
|
||||
align-items: start;
|
||||
align-items: center;
|
||||
}
|
||||
div.search {
|
||||
flex: 1 0 0;
|
||||
margin: auto 0;
|
||||
display: flex;
|
||||
}
|
||||
div.link a {
|
||||
@ -50,7 +51,6 @@
|
||||
@media (min-width: 800px) {
|
||||
div.items {
|
||||
flex-direction: row;
|
||||
align-items: end;
|
||||
}
|
||||
div.link a {
|
||||
font-size: 2rem !important;
|
||||
@ -62,10 +62,18 @@
|
||||
}
|
||||
input.search {
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
font-size: 1rem;
|
||||
height: 2.75rem;
|
||||
font-size: 2rem;
|
||||
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>
|
||||
|
||||
|
@ -55,8 +55,14 @@ export async function del(req, res, next) {
|
||||
const article = await Article.findOneAndDelete({ slug });
|
||||
|
||||
if (article) {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
const articles = await Article.find()
|
||||
.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 {
|
||||
res.writeHead(404, {
|
||||
'Content-Type': 'application/json'
|
||||
|
@ -142,7 +142,7 @@
|
||||
|
||||
<div class="content">
|
||||
<figure class="article-image">
|
||||
<img alt={article.title} src={article.image}>
|
||||
<img alt={article.title} src={`/a/${article.image}`}>
|
||||
</figure>
|
||||
<div class="article-meta">
|
||||
<h1 class="article-title">{article.title}</h1>
|
||||
|
@ -24,7 +24,6 @@
|
||||
<style>
|
||||
h1 {
|
||||
margin: 0 auto;
|
||||
color: whitesmoke;
|
||||
margin-top: 1rem;
|
||||
font-size: 2rem;
|
||||
font-size: 3rem;
|
||||
@ -55,7 +54,7 @@
|
||||
{#each articles as {title, slug, image, created_at}}
|
||||
<a rel="prefetch" href={`/a/${slug}`}>
|
||||
<figure class="article-image">
|
||||
<img src={image || '/logo.png'} alt={title}>
|
||||
<img src={image ? `/a/${image}` : '/logo.png'} alt={title}>
|
||||
</figure>
|
||||
<div class="article-meta">
|
||||
<p class="article-title">{title}</p>
|
||||
|
@ -17,8 +17,8 @@
|
||||
|
||||
const { session } = stores();
|
||||
|
||||
let editor;
|
||||
let title = '', image = '', category = '';
|
||||
let editor, form;
|
||||
let title = '', category = '';
|
||||
export let categories;
|
||||
|
||||
let actions = [
|
||||
@ -28,7 +28,6 @@
|
||||
title: 'Save',
|
||||
result: function save() {
|
||||
window.localStorage['title'] = title;
|
||||
window.localStorage['image'] = image;
|
||||
window.localStorage['category'] = category;
|
||||
window.localStorage['html'] = editor.getHtml(true);
|
||||
alert('Successfully saved draft to browser local storage');
|
||||
@ -56,7 +55,6 @@
|
||||
|
||||
onMount(function load() {
|
||||
title = window.localStorage['title'] || '';
|
||||
image = window.localStorage['image'] || '';
|
||||
category = window.localStorage['category'] || '';
|
||||
editor.setHtml(window.localStorage['html'] || '', false);
|
||||
});
|
||||
@ -64,13 +62,14 @@
|
||||
async function submit()
|
||||
{
|
||||
let html = editor.getHtml(true);
|
||||
let fd = new FormData(form);
|
||||
fd.append('html', html);
|
||||
const res = await fetch(`/cms/article`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ html, image, title, category })
|
||||
body: fd
|
||||
});
|
||||
const json = await res.json();
|
||||
if (res.status === 200) {
|
||||
@ -116,19 +115,19 @@
|
||||
<div class="content">
|
||||
<a href="/cms">< Back to Dashboard</a>
|
||||
<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 Author: <strong>{$session.user.realname}</strong></p>
|
||||
<p>Article Category:
|
||||
{#if categories.length}
|
||||
<select bind:value={category}>
|
||||
<select name="category" bind:value={category}>
|
||||
{#each categories as { name, slug }}
|
||||
<option value={slug}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<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>
|
||||
<Editor bind:this={editor} {actions} />
|
||||
<button on:click={submit}>Submit</button>
|
||||
|
@ -25,11 +25,15 @@
|
||||
async function del(article)
|
||||
{
|
||||
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'
|
||||
});
|
||||
const res = await fetch(`/c/all.json`);
|
||||
articles = await res.json();
|
||||
const json = await res.json();
|
||||
if (res.status === 200) {
|
||||
articles = json.articles;
|
||||
} else {
|
||||
alert(`Error ${res.status}: ${json.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -35,11 +35,8 @@
|
||||
font-size: 3.5rem !important;
|
||||
}
|
||||
}
|
||||
h1.welcome, h2.desc {
|
||||
color: whitesmoke;
|
||||
}
|
||||
h1.welcome {
|
||||
margin-top: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-size: 3.75rem;
|
||||
font-size: 3rem;
|
||||
text-transform: uppercase;
|
||||
@ -58,12 +55,11 @@
|
||||
<div class="background"></div>
|
||||
<div class="floaty">
|
||||
<h1 class="welcome">Welcome</h1>
|
||||
<h2 class="desc">Find an Article</h2>
|
||||
<div class="article-list">
|
||||
{#each articles as {title, slug, image, created_at}}
|
||||
<a rel="prefetch" href={`/a/${slug}`}>
|
||||
<figure class="article-image">
|
||||
<img src={image || '/logo.png'} alt={title}>
|
||||
<img src={image ? `/a/${image}` : '/logo.png'} alt={title}>
|
||||
</figure>
|
||||
<div class="article-meta">
|
||||
<p class="article-title">{title}</p>
|
||||
|
@ -20,7 +20,6 @@
|
||||
<style>
|
||||
h1 {
|
||||
margin: 0 auto;
|
||||
color: whitesmoke;
|
||||
margin-top: 1rem;
|
||||
font-size: 2rem;
|
||||
font-size: 3rem;
|
||||
@ -41,7 +40,7 @@
|
||||
{#each results as {title, slug, image, created_at}}
|
||||
<a rel="prefetch" href={`/a/${slug}`}>
|
||||
<figure class="article-image">
|
||||
<img src={image || '/logo.png'} alt={title}>
|
||||
<img src={image ? `/a/${image}` : '/logo.png'} alt={title}>
|
||||
</figure>
|
||||
<div class="article-meta">
|
||||
<p class="article-title">{title}</p>
|
||||
|
@ -11,6 +11,7 @@ import sessionFileStore from 'session-file-store';
|
||||
import { RateLimiterMemory } from 'rate-limiter-flexible';
|
||||
import fileUpload from 'express-fileupload';
|
||||
import helmet from 'helmet';
|
||||
import crypto from 'crypto';
|
||||
import Article from './models/article.js';
|
||||
import Category from './models/category.js';
|
||||
import User from './models/user.js';
|
||||
@ -109,7 +110,9 @@ express()
|
||||
.use(helmet())
|
||||
.use(bodyParser.json())
|
||||
.use(bodyParser.urlencoded({ extended: true }))
|
||||
.use(fileUpload())
|
||||
.use(fileUpload({
|
||||
limits: { fileSize: 16000000 }
|
||||
}))
|
||||
.use(session({
|
||||
secret: SESSION_SECRET,
|
||||
resave: false,
|
||||
@ -248,14 +251,34 @@ express()
|
||||
isAuthor,
|
||||
async function(req, res, next) {
|
||||
try {
|
||||
const { html, title, image, category } = req.body;
|
||||
const { html, title, category } = req.body;
|
||||
const { image } = req.files;
|
||||
if (!title || !image || !html || !category) {
|
||||
res.writeHead(422, {
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
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 });
|
||||
if (!cat) {
|
||||
@ -265,8 +288,12 @@ express()
|
||||
res.end(JSON.stringify({
|
||||
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();
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json'
|
||||
|
@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
|
||||
<meta name='description' content='HOWFEED.BIZ: Where we break the news.'>
|
||||
<meta name='keywords' content='news, satire, blog'>
|
||||
<meta name='description' content='HOWFEED.BIZ: Where we break the news.'>
|
||||
<meta name='keywords' content='news, satire, blog'>
|
||||
<meta name='theme-color' content='#508FC3'>
|
||||
%sapper.base%
|
||||
<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 {
|
||||
background: url('/cityscape.jpg') no-repeat center;
|
||||
background: url('/background.png') no-repeat center;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
position: fixed;
|
||||
@ -124,14 +124,14 @@ div.background {
|
||||
}
|
||||
|
||||
div.article-list {
|
||||
box-shadow: 0 0 5px #000;
|
||||
box-shadow: 0 -5px 0.75rem black;
|
||||
}
|
||||
div.article-meta {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.floaty {
|
||||
padding-top: 8rem;
|
||||
padding-top: 16rem;
|
||||
padding-bottom: 4rem;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
Loading…
x
Reference in New Issue
Block a user