commit 1c646c69370fd3b71e169d409da07b234c99ee3e Author: “Naeel” Date: Mon Jul 20 13:49:41 2026 +0400 init: Node.js CRUD v1.0.0 — Express + PG, same table, Nubes design diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a433ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +*.log +.env +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..818c734 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Node.js CRUD Example + +CRUD-интерфейс на Express + PostgreSQL. Аналог Lucee/Flask CRUD, та же таблица. + +## Переменные окружения + +| Переменная | По умолчанию | +|-------------|---------------| +| `PGHOST` | `127.0.0.1` | +| `PGPORT` | `5432` | +| `PGDATABASE`| `postgres` | +| `PGUSER` | `postgres` | +| `PGPASSWORD`| (пусто) | +| `TABLE_NAME`| `crud_items` | +| `PORT` | `3000` | + +## Запуск + +```bash +npm install +npm start +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..1210183 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "tfnodejscrud", + "version": "1.0.0", + "description": "Node.js CRUD — PostgreSQL", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "ejs": "^3.1.10", + "express": "^4.21.0", + "pg": "^8.13.0" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..11a9ece --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,55 @@ + + diff --git a/public/logo.svg b/public/logo.svg new file mode 100755 index 0000000..d76eef6 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..8a5eb67 --- /dev/null +++ b/public/style.css @@ -0,0 +1,111 @@ +:root { + --brand-primary: #2563eb; + --brand-primary-dark: #1d4ed8; + --brand-gray: #d1d5db; + --brand-grey-light: #f3f4f6; + --brand-destructive: #ef4444; + --brand-success: #22c55e; + --text-primary: #1a1a1a; + --text-muted: #6b7280; + --bg-card: #ffffff; + --radius: 12px; +} +* { box-sizing: border-box; margin: 0; padding: 0; } +body { + font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + color: var(--text-primary); + background: var(--brand-grey-light); + min-height: 100vh; + padding: 40px 16px; +} +.container { max-width: 720px; margin: 0 auto; } + +/* Header */ +.header { + display: flex; align-items: center; gap: 12px; + margin-bottom: 24px; +} +.header img { height: 36px; } +.header h1 { font-size: 20px; font-weight: 600; color: #001C34; } + +/* Card */ +.card { + background: var(--bg-card); + border-radius: var(--radius); + border: 1px solid var(--brand-gray); + box-shadow: 0 1px 2px rgba(0,0,0,0.05); + overflow: hidden; + margin-bottom: 20px; +} +.card-header { + background: var(--brand-grey-light); + padding: 12px 16px; + font-weight: 600; + font-size: 16px; +} +.card-body { padding: 16px; } + +/* Table */ +table { width: 100%; border-collapse: collapse; } +th { + background: var(--brand-grey-light); + text-transform: uppercase; + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + padding: 8px 12px; + text-align: left; + border-right: 1px solid var(--brand-gray); +} +th:last-child { border-right: none; text-align: center; width: 60px; } +td { + padding: 8px 12px; + border-bottom: 1px solid var(--brand-gray); + border-right: 1px solid var(--brand-gray); +} +td:last-child { border-right: none; text-align: center; width: 60px; } +tr:hover { background: rgba(243,244,246,0.5); } +tr:last-child td { border-bottom: none; } + +/* Buttons */ +.btn { + display: inline-flex; align-items: center; gap: 4px; + height: 32px; padding: 0 12px; + border: 1px solid var(--brand-gray); + border-radius: 6px; + background: var(--bg-card); + font-size: 14px; font-family: inherit; + cursor: pointer; white-space: nowrap; + text-decoration: none; + color: var(--text-primary); +} +.btn:hover { background: var(--brand-grey-light); } +.btn-primary { + background: var(--brand-primary); color: #fff; border-color: var(--brand-primary); +} +.btn-primary:hover { background: var(--brand-primary-dark); } +.btn-sm { height: 28px; padding: 0 8px; font-size: 13px; } + +/* Form */ +.form-grid { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} +.form-row { + display: flex; gap: 8px; align-items: flex-end; +} +.form-row input { flex: 1; } +input[type="text"] { + height: 36px; padding: 0 12px; + border: 1px solid var(--brand-gray); + border-radius: 6px; font-size: 14px; font-family: inherit; +} +input[type="text"]:focus { outline: none; border-color: var(--brand-primary); box-shadow: 0 0 0 2px rgba(37,99,235,0.15); } + +/* Footer */ +.footer { text-align: center; padding: 16px; color: #9ca3af; font-size: 12px; } + +/* Empty state */ +.empty-cell { color: var(--text-muted); padding: 24px; text-align: center; } diff --git a/server.js b/server.js new file mode 100644 index 0000000..b5f2bbe --- /dev/null +++ b/server.js @@ -0,0 +1,103 @@ +/** + * server.js — Node.js CRUD (Express + PostgreSQL). + * + * Переменные окружения: + * PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD + * TABLE_NAME (по умолчанию crud_items) + * PORT (по умолчанию 3000) + * + * При старте: CREATE TABLE IF NOT EXISTS. + */ +const express = require("express"); +const { Pool } = require("pg"); +const path = require("path"); + +const VERSION = "1.0.0"; +const TABLE_NAME = process.env.TABLE_NAME || "crud_items"; +const PORT = process.env.PORT || 3000; + +const pool = new Pool({ + host: process.env.PGHOST || "127.0.0.1", + port: parseInt(process.env.PGPORT || "5432"), + database: process.env.PGDATABASE || "postgres", + user: process.env.PGUSER || "postgres", + password: process.env.PGPASSWORD || "", +}); + +const app = express(); +app.use(express.urlencoded({ extended: true })); +app.use(express.static(path.join(__dirname, "public"))); +app.set("view engine", "ejs"); +app.set("views", path.join(__dirname, "views")); + +// --- Инициализация БД --- +async function initDB() { + const client = await pool.connect(); + try { + await client.query(` + CREATE TABLE IF NOT EXISTS ${TABLE_NAME} ( + id SERIAL PRIMARY KEY, + value TEXT NOT NULL DEFAULT '' + ) + `); + } finally { + client.release(); + } +} + +// --- Главная страница (GET + POST) --- +app.get("/", async (req, res) => { + try { + let editRow = null; + if (req.query.edit) { + const result = await pool.query( + `SELECT id, value FROM ${TABLE_NAME} WHERE id = $1`, + [parseInt(req.query.edit)] + ); + if (result.rows.length > 0) editRow = result.rows[0]; + } + + const result = await pool.query( + `SELECT id, value FROM ${TABLE_NAME} ORDER BY id` + ); + + res.render("index", { + table_name: TABLE_NAME, + rows: result.rows, + editRow, + version: VERSION, + }); + } catch (err) { + res.status(500).send(err.message); + } +}); + +app.post("/", async (req, res) => { + try { + const { action, value, id } = req.body; + + if (action === "create") { + await pool.query(`INSERT INTO ${TABLE_NAME} (value) VALUES ($1)`, [value || ""]); + } else if (action === "update") { + await pool.query(`UPDATE ${TABLE_NAME} SET value = $1 WHERE id = $2`, [ + value || "", + parseInt(id || 0), + ]); + } else if (action === "delete") { + await pool.query(`DELETE FROM ${TABLE_NAME} WHERE id = $1`, [ + parseInt(id || 0), + ]); + } + + res.redirect("/"); + } catch (err) { + res.status(500).send(err.message); + } +}); + +// --- Старт --- +initDB().then(() => { + app.listen(PORT, () => { + console.log(`Node.js CRUD v${VERSION} — http://0.0.0.0:${PORT}`); + }); +}); diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..2472a23 --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,90 @@ + + + + + + CRUD Node.js Example — <%= table_name %> + + + + +
+ +
+ Nubes +

CRUD Node.js Example

+
+ +
+
Записи
+
+ + + + + + + + + + <% rows.forEach(function(row) { %> + + + + + + + <% }); %> + +
IDVALUE
<%= row.id %><%= row.value %> + ✏️ + +
+ + + +
+
+ <% if (rows.length === 0) { %> +
Нет записей. Создайте первую.
+ <% } %> +
+
+ +
+ <% if (editRow) { %> +
Редактировать #<%= editRow.id %>
+
+
+ + +
+ + + Отмена +
+
+
+ <% } else { %> +
Добавить
+
+
+ +
+ + +
+
+
+ <% } %> +
+ +
+ + + +