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