mirror of
https://github.com/sergeyyarkov/node-docker-example.git
synced 2026-10-11 10:19:41 +03:00
feat: article creating
This commit is contained in:
parent
b4838dc19b
commit
9c84c17308
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
20
controllers/EditorController.js
Normal file
20
controllers/EditorController.js
Normal 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
2
db.js
@ -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() {
|
||||||
|
|||||||
@ -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 {
|
||||||
/**
|
/**
|
||||||
|
|||||||
2
index.js
2
index.js
@ -1,4 +1,4 @@
|
|||||||
import Application from './server.js';
|
import Application from '#app/server';
|
||||||
|
|
||||||
const app = new Application();
|
const app = new Application();
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
37
router.js
37
router.js
@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
5
seed.js
5
seed.js
@ -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');
|
||||||
|
|||||||
15
server.js
15
server.js
@ -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}"`)
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<a href="/">< return to home</a>
|
<a href="/">< 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
26
views/editor/create.hbs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<html lang='en'>
|
||||||
|
{{> head }}
|
||||||
|
<body>
|
||||||
|
<main class='app'>
|
||||||
|
<a href="/">< 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>
|
||||||
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user