feat: article creating

This commit is contained in:
Sergey Yarkov 2022-03-18 22:14:56 +03:00
parent b4838dc19b
commit 9c84c17308
16 changed files with 153 additions and 39 deletions

View File

@ -81,8 +81,15 @@ class ArticleController {
*/ */
static async create(req, res) { static async create(req, res) {
try { try {
res.json({ data: `POST ${req.url}` }); const data = req.getPostData();
return; if (data && data.title && data.description && data.body) {
const article = await ArticleModel.create({
title: data.title,
description: data.description,
body: data.body,
});
res.redirect(`/articles/${article.id}`);
}
} catch (error) { } catch (error) {
res.serverError(error); res.serverError(error);
} }

View File

@ -0,0 +1,20 @@
import http from 'node:http';
import Response from '#app/Response';
class EditorController {
/**
* Render editor for creating article
*
* @param {http.ClientRequest} req
* @param {Response} res
*/
static create(req, res) {
try {
res.render('./views/editor/create.hbs', { title: 'Create new article' });
} catch (error) {
res.serverError(error);
}
}
}
export default EditorController;

2
db.js
View File

@ -1,5 +1,5 @@
import pg from 'pg'; import pg from 'pg';
import env from './env.js'; import env from '#app/env';
class Database { class Database {
constructor() { constructor() {

View File

@ -1,5 +1,5 @@
import fs from 'node:fs'; import fs from 'node:fs';
import db from '../db.js'; import db from '#app/db';
class DbHelper { class DbHelper {
/** /**

View File

@ -1,4 +1,4 @@
import Application from './server.js'; import Application from '#app/server';
const app = new Application(); const app = new Application();

View File

@ -1,3 +1,6 @@
import DbHelper from './helpers/db.js'; /**
* Helpers
*/
import DbHelper from '#helpers/db';
DbHelper.runQueryFromFile('sql/schema_init.sql'); DbHelper.runQueryFromFile('sql/schema_init.sql');

View File

@ -18,8 +18,8 @@ class ArticleModel {
*/ */
static async getAll() { static async getAll() {
try { try {
const data = await db.client.query('SELECT * FROM "articles"'); const result = await db.client.query('SELECT * FROM "articles"');
return data.rows; return result.rows;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -33,14 +33,14 @@ class ArticleModel {
*/ */
static async getById(id) { static async getById(id) {
try { try {
const data = await db.client.query( const result = await db.client.query(
'SELECT * from "articles" WHERE "id" = $1', 'SELECT * from "articles" WHERE "id" = $1',
[id] [id]
); );
if (data.rows.length === 0) return null; if (result.rows.length === 0) return null;
return data.rows[0]; return result.rows[0];
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -63,12 +63,12 @@ class ArticleModel {
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
}); });
const article = await db.client.query( const result = await db.client.query(
`INSERT INTO "articles" ("id", "title", "description", "body", "created_at", "updated_at") VALUES($1, $2, $3, $4, $5, $6)`, `INSERT INTO "articles" ("id", "title", "description", "body", "created_at", "updated_at") VALUES($1, $2, $3, $4, $5, $6) RETURNING *`,
values values
); );
return article; return result.rows[0];
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@ -7,6 +7,7 @@ import router from '#app/router';
* Controllers * Controllers
*/ */
import ArticleController from '#controllers/ArticleController'; import ArticleController from '#controllers/ArticleController';
import EditorController from '#app/controllers/EditorController';
/** /**
* Middlewares * Middlewares
@ -16,3 +17,4 @@ import WaitMiddleware from '#middlewares/WaitMiddleware';
router.get('/', ArticleController.index).middleware([WaitMiddleware.handle]); router.get('/', ArticleController.index).middleware([WaitMiddleware.handle]);
router.get('/articles/:id', ArticleController.show); router.get('/articles/:id', ArticleController.show);
router.post('/articles', ArticleController.create); router.post('/articles', ArticleController.create);
router.get('/editor/create', EditorController.create);

View File

@ -1,5 +1,8 @@
body { * {
font-family: monospace; font-family: monospace;
}
body {
text-align: justify; text-align: justify;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@ -9,6 +12,28 @@ body {
padding: 15px; padding: 15px;
} }
button {
cursor: pointer;
padding: 5px 20px;
}
input {
padding: 7px 5px;
font-size: 16px;
}
textarea {
resize: none;
font-size: 16px;
padding: 7px 5px;
}
.textarea-body {
border: none;
border-bottom: 1px solid;
outline: none;
}
.app .app-heading { .app .app-heading {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -25,15 +50,11 @@ body {
user-select: none; user-select: none;
} }
.app .article-heading { .app .article-heading,
.app .editor-heading {
display: block; display: block;
} }
.app .app-body article {
margin-top: 2rem;
line-height: 1.3rem;
}
.app .app-footer { .app .app-footer {
text-align: center; text-align: center;
} }
@ -69,3 +90,9 @@ body {
margin-top: 1rem; margin-top: 1rem;
text-align: center; text-align: center;
} }
.app .app-body .app-body__editor form {
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -27,6 +27,11 @@ class Response {
this.send(text, 404); this.send(text, 404);
} }
redirect(location) {
this.res.writeHead(302, { Location: location });
this.res.end();
}
serverError(error, text = '500 | Internal server error') { serverError(error, text = '500 | Internal server error') {
console.error(error); console.error(error);
this.render('./views/500.hbs', { title: text }, 500); this.render('./views/500.hbs', { title: text }, 500);

View File

@ -1,7 +1,12 @@
import http from 'node:http'; import http from 'node:http';
import Response from './response.js'; import qs from 'node:querystring';
import { pathToRegexp } from 'path-to-regexp'; import { pathToRegexp } from 'path-to-regexp';
import StringHelper from './helpers/string.js'; import Response from '#app/response';
/**
* Helpers
*/
import StringHelper from '#helpers/string';
/** /**
* Single route type * Single route type
@ -29,11 +34,13 @@ class Router {
* @param {Response} res * @param {Response} res
* @returns * @returns
*/ */
call(req, res) { async call(req, res) {
const mask = `${req.method}:${StringHelper.delTrailingSlash(req.url)}`; const mask = `${req.method}:${StringHelper.delTrailingSlash(req.url)}`;
const keys = Object.keys(this.routes); const keys = Object.keys(this.routes);
let i = keys.length; let i = keys.length;
await this.#collectData(req, res);
while (i--) { while (i--) {
let route = this.routes[mask]; let route = this.routes[mask];
@ -92,6 +99,30 @@ class Router {
} }
} }
async #collectData(req, res) {
/**
* Collect POST data
*/
if (
req.method === 'POST' &&
req.headers['content-type'] === 'application/x-www-form-urlencoded'
) {
const buffers = [];
for await (const chunk of req) {
if (
req.method === 'POST' &&
req.headers['content-type'] === 'application/x-www-form-urlencoded'
) {
buffers.push(chunk);
}
}
const data = qs.parse(Buffer.concat(buffers).toString());
req['getPostData'] = () => data;
}
}
/** /**
* Add more routes to router store * Add more routes to router store
* *

View File

@ -1,3 +1,6 @@
import DbHelper from './helpers/db.js'; /**
* Helpers
*/
import DbHelper from '#helpers/db';
DbHelper.runQueryFromFile('sql/seed.sql'); DbHelper.runQueryFromFile('sql/seed.sql');

View File

@ -1,6 +1,5 @@
import http from 'node:http'; import http from 'node:http';
import fs from 'node:fs'; import fs from 'node:fs';
import qs from 'node:querystring';
import path from 'node:path'; import path from 'node:path';
import mime from 'mime-types'; import mime from 'mime-types';
import hbs from 'handlebars'; import hbs from 'handlebars';
@ -9,7 +8,7 @@ import db from '#app/db';
import router from '#app/router'; import router from '#app/router';
import Response from '#app/response'; import Response from '#app/response';
import FileHelper from '#helpers/file'; import FileHelper from '#helpers/file';
import './preloads/index.js'; import '#app/preloads/index';
class Application { class Application {
constructor() { constructor() {
@ -44,17 +43,7 @@ class Application {
#handle() { #handle() {
this.server.on('request', async (req, res) => { this.server.on('request', async (req, res) => {
const response = new Response(req, res); const response = new Response(req, res);
this.router.call(req, response); await this.router.call(req, response);
// const buffer = Buffer.alloc(parseInt(req.headers['content-length'], 10));
// req.on('data', (chunk) => {
// if (req.method === 'POST') {
// console.log(chunk);
// // console.log(buffer.toString());
// // const buffer = Buffer.alloc(chunk.length)
// }
// });
}); });
this.server.listen(env.app.port, () => this.server.listen(env.app.port, () =>
console.log(`[LOG]: Server is running on port "${env.app.port}"`) console.log(`[LOG]: Server is running on port "${env.app.port}"`)

View File

@ -5,7 +5,7 @@
<a href="/">&lt; return to home</a> <a href="/">&lt; return to home</a>
<div class='app-heading article-heading'> <div class='app-heading article-heading'>
<h1>{{ article.title }}</h1> <h1>{{ article.title }}</h1>
<span>{{ article.created_at }}</span> <p>{{ article.created_at }}</p>
</div> </div>
<div class="app-body"> <div class="app-body">
<p>{{ article.description }}</p> <p>{{ article.description }}</p>

26
views/editor/create.hbs Normal file
View File

@ -0,0 +1,26 @@
<html lang='en'>
{{> head }}
<body>
<main class='app'>
<a href="/">&lt; return to home</a>
<div class='app-heading editor-heading'>
<h1>Create new article</h1>
<p>Fill in the required fields to create a new article</p>
</div>
<div class="app-body">
<hr />
<div class="app-body__editor">
<div class="editor-form">
<form action="/articles" method="post" enctype="application/x-www-form-urlencoded">
<input placeholder="e.g. My first nice article" type="text" name="title" id="title" required="true">
<textarea placeholder="Description" name="description" id="description" cols="30" rows="10" required="true"></textarea>
<textarea placeholder="Content" class="textarea-body" name="body" id="body" cols="30" rows="20" required="true"></textarea>
<button type="submit">Create</button>
</form>
</div>
</div>
</div>
{{> footer }}
</main>
</body>
</html>

View File

@ -15,6 +15,7 @@
<hr /> <hr />
<div class="app-body"> <div class="app-body">
<button onclick="location.href='/editor/create'" type="button">Write new</button>
{{#unless articles.length }} {{#unless articles.length }}
<p style="text-align: center;margin: 30px 0;"> <p style="text-align: center;margin: 30px 0;">
No articles found. No articles found.