⚡ TECHNO-TODO v1.0: 90s cyberpunk todo app with PostgreSQL

This commit is contained in:
Hermes Agent
2026-08-11 19:32:55 +00:00
commit 7088e5e879
4 changed files with 405 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
+14
View File
@@ -0,0 +1,14 @@
{
"name": "techno-todo",
"version": "1.0.0",
"description": "⚡ 90s Techno-Style Todo App",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.21.0",
"pg": "^8.13.0",
"ejs": "^3.1.10"
}
}
+78
View File
@@ -0,0 +1,78 @@
const express = require('express');
const { Pool } = require('pg');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
// PostgreSQL connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
});
// Initialize DB table
async function initDB() {
const client = await pool.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS todos (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
done BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
)
`);
console.log('⚡ DATABASE ONLINE ⚡');
} finally {
client.release();
}
}
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'));
// Routes
app.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM todos ORDER BY created_at DESC');
res.render('index', { todos: result.rows, error: null });
} catch (err) {
res.render('index', { todos: [], error: 'DATABASE CONNECTION LOST ⚠' });
}
});
app.post('/add', async (req, res) => {
const { text } = req.body;
if (text && text.trim()) {
await pool.query('INSERT INTO todos (text) VALUES ($1)', [text.trim()]);
}
res.redirect('/');
});
app.post('/toggle/:id', async (req, res) => {
await pool.query('UPDATE todos SET done = NOT done WHERE id = $1', [req.params.id]);
res.redirect('/');
});
app.post('/delete/:id', async (req, res) => {
await pool.query('DELETE FROM todos WHERE id = $1', [req.params.id]);
res.redirect('/');
});
// Health check
app.get('/health', (req, res) => res.json({ status: 'ONLINE', db: 'CONNECTED' }));
initDB().then(() => {
app.listen(PORT, () => {
console.log(`╔══════════════════════════════════════╗`);
console.log(`║ ⚡ TECHNO-TODO v1.0 ONLINE ⚡ ║`);
console.log(`║ PORT: ${PORT} ║`);
console.log(`╚══════════════════════════════════════╝`);
});
}).catch(err => {
console.error('Failed to init DB:', err);
process.exit(1);
});
+301
View File
@@ -0,0 +1,301 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚡ TECHNO-TODO v1.0</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
:root {
--neon-green: #00ff41;
--neon-pink: #ff00ff;
--neon-blue: #00ffff;
--neon-yellow: #ffff00;
--dark-bg: #0a0a0a;
--panel-bg: #111;
--border-glow: #00ff41;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--dark-bg);
color: var(--neon-green);
font-family: 'Share Tech Mono', 'Courier New', monospace;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-image:
repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,255,65,0.03) 2px, rgba(0,255,65,0.03) 4px),
repeating-linear-gradient(90deg, transparent, transparent 2px, rgba(0,255,65,0.02) 2px, rgba(0,255,65,0.02) 4px);
}
/* CRT scanlines */
body::before {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: repeating-linear-gradient(0deg, rgba(0,0,0,0.15) 0px, rgba(0,0,0,0.15) 1px, transparent 1px, transparent 3px);
pointer-events: none;
z-index: 9999;
}
.container {
width: 90%;
max-width: 600px;
position: relative;
z-index: 1;
}
/* Header */
.header {
text-align: center;
margin-bottom: 20px;
border: 2px solid var(--neon-pink);
padding: 20px;
background: var(--panel-bg);
box-shadow: 0 0 20px rgba(255, 0, 255, 0.3), inset 0 0 20px rgba(255, 0, 255, 0.05);
animation: borderPulse 2s infinite;
}
@keyframes borderPulse {
0%, 100% { border-color: var(--neon-pink); box-shadow: 0 0 20px rgba(255,0,255,0.3), inset 0 0 20px rgba(255,0,255,0.05); }
50% { border-color: var(--neon-blue); box-shadow: 0 0 30px rgba(0,255,255,0.4), inset 0 0 30px rgba(0,255,255,0.08); }
}
.header h1 {
font-size: 2.2em;
color: var(--neon-pink);
text-shadow: 0 0 10px var(--neon-pink), 0 0 30px var(--neon-pink), 0 0 50px var(--neon-pink);
letter-spacing: 4px;
animation: glitch 3s infinite;
}
@keyframes glitch {
0%, 100% { transform: translate(0); }
2% { transform: translate(-2px, 1px); }
4% { transform: translate(2px, -1px); }
6% { transform: translate(0); }
}
.header .subtitle {
color: var(--neon-green);
font-size: 0.9em;
margin-top: 5px;
text-shadow: 0 0 5px var(--neon-green);
}
.blink { animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
/* Add form */
.add-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.add-form input {
flex: 1;
background: #000;
border: 2px solid var(--neon-green);
color: var(--neon-green);
padding: 12px 15px;
font-family: inherit;
font-size: 1em;
outline: none;
box-shadow: 0 0 10px rgba(0, 255, 65, 0.2);
transition: box-shadow 0.3s;
}
.add-form input:focus {
box-shadow: 0 0 20px rgba(0, 255, 65, 0.5), 0 0 40px rgba(0, 255, 65, 0.2);
}
.add-form input::placeholder {
color: rgba(0, 255, 65, 0.3);
}
.btn {
background: #000;
border: 2px solid var(--neon-blue);
color: var(--neon-blue);
padding: 12px 20px;
font-family: inherit;
font-size: 1em;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 5px var(--neon-blue);
box-shadow: 0 0 10px rgba(0, 255, 255, 0.2);
transition: all 0.3s;
}
.btn:hover {
background: var(--neon-blue);
color: #000;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.6);
}
.btn-sm {
padding: 6px 12px;
font-size: 0.75em;
letter-spacing: 1px;
}
.btn-done {
border-color: var(--neon-green);
color: var(--neon-green);
text-shadow: 0 0 5px var(--neon-green);
}
.btn-done:hover {
background: var(--neon-green);
color: #000;
}
.btn-del {
border-color: #ff3333;
color: #ff3333;
text-shadow: 0 0 5px #ff3333;
}
.btn-del:hover {
background: #ff3333;
color: #000;
}
/* Todo list */
.todo-list {
list-style: none;
}
.todo-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
margin-bottom: 8px;
background: var(--panel-bg);
border: 1px solid rgba(0, 255, 65, 0.2);
transition: all 0.3s;
}
.todo-item:hover {
border-color: var(--neon-green);
box-shadow: 0 0 15px rgba(0, 255, 65, 0.15);
}
.todo-item.done {
opacity: 0.4;
border-color: rgba(0, 255, 65, 0.1);
}
.todo-item.done .todo-text {
text-decoration: line-through;
color: rgba(0, 255, 65, 0.4);
}
.todo-text {
flex: 1;
font-size: 1em;
word-break: break-word;
}
.todo-actions {
display: flex;
gap: 5px;
}
.empty {
text-align: center;
padding: 40px;
color: rgba(0, 255, 65, 0.3);
border: 1px dashed rgba(0, 255, 65, 0.2);
font-size: 0.9em;
}
.count {
text-align: right;
margin-top: 15px;
font-size: 0.8em;
color: rgba(0, 255, 65, 0.5);
}
.error-msg {
background: rgba(255, 0, 0, 0.1);
border: 1px solid #ff3333;
color: #ff3333;
padding: 10px;
margin-bottom: 15px;
text-align: center;
text-shadow: 0 0 5px #ff3333;
}
/* Footer */
.footer {
text-align: center;
margin-top: 30px;
padding: 15px;
border-top: 1px solid rgba(0, 255, 65, 0.2);
color: rgba(0, 255, 65, 0.3);
font-size: 0.7em;
}
.footer .dot {
color: var(--neon-green);
animation: blink 0.5s steps(1) infinite;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚡ TECHNO-TODO</h1>
<div class="subtitle">// version 1.0-beta :: neon edition<br><span class="blink">█ SYSTEM ACTIVE █</span></div>
</div>
<% if (error) { %>
<div class="error-msg">⚠ <%= error %> ⚠</div>
<% } %>
<form class="add-form" action="/add" method="POST">
<input type="text" name="text" placeholder="> ENTER NEW TASK..." maxlength="200" required autofocus>
<button type="submit" class="btn">ADD</button>
</form>
<% if (todos.length === 0) { %>
<div class="empty">// NO TASKS IN MEMORY<br>> READY FOR INPUT_</div>
<% } else { %>
<ul class="todo-list">
<% todos.forEach(todo => { %>
<li class="todo-item <%= todo.done ? 'done' : '' %>">
<span class="todo-text">
<%= todo.done ? '✓' : '▸' %> <%= todo.text %>
</span>
<span class="todo-actions">
<form action="/toggle/<%= todo.id %>" method="POST" style="display:inline">
<button class="btn btn-done btn-sm"><%= todo.done ? 'UNDO' : 'DONE' %></button>
</form>
<form action="/delete/<%= todo.id %>" method="POST" style="display:inline">
<button class="btn btn-del btn-sm">DEL</button>
</form>
</span>
</li>
<% }) %>
</ul>
<% } %>
<div class="count">
> TOTAL: <%= todos.length %> | DONE: <%= todos.filter(t => t.done).length %> | PENDING: <%= todos.filter(t => !t.done).length %>
</div>
<div class="footer">
⚡ TECHNO-TODO v1.0 <span class="dot">█</span> Powered by Dokploy <span class="dot">█</span> PostgreSQL <span class="dot">█</span> Node.js<br>
EST. 2026 // THE FUTURE IS NOW
</div>
</div>
</body>
</html>