chore: initial repo, copy source from assignment 1

This commit is contained in:
Brazing Technology 2026-04-29 11:20:19 +05:30
commit 0a58373d86
6 changed files with 334 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
*.log
.DS_Store
Thumbs.db
.idea/
.vscode/
__pycache__/
*.pyc
.env

110
backend/app.py Normal file
View File

@ -0,0 +1,110 @@
import os
import time
import psycopg2
from psycopg2.extras import RealDictCursor
from flask import Flask, request, jsonify
app = Flask(__name__)
DB_CONFIG = {
"host": os.environ.get("DB_HOST", "db"),
"database": os.environ.get("DB_NAME", "taskapp"),
"user": os.environ.get("DB_USER", "taskapp"),
"password": os.environ.get("DB_PASSWORD", "taskapp123"),
}
def get_db():
return psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
def init_db():
"""Wait for PostgreSQL and create the tasks table if it doesn't exist."""
for attempt in range(30):
try:
conn = get_db()
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
completed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
cur.close()
conn.close()
print("Database initialized.")
return
except psycopg2.OperationalError:
print(f"Waiting for database... (attempt {attempt + 1}/30)")
time.sleep(2)
raise RuntimeError("Could not connect to the database after 30 attempts.")
@app.route("/api/tasks", methods=["GET"])
def get_tasks():
conn = get_db()
cur = conn.cursor()
cur.execute("SELECT * FROM tasks ORDER BY created_at DESC")
tasks = cur.fetchall()
cur.close()
conn.close()
return jsonify(tasks)
@app.route("/api/tasks", methods=["POST"])
def create_task():
data = request.get_json()
title = data.get("title", "").strip()
if not title:
return jsonify({"error": "Title is required"}), 400
conn = get_db()
cur = conn.cursor()
cur.execute(
"INSERT INTO tasks (title) VALUES (%s) RETURNING *",
(title,),
)
task = cur.fetchone()
conn.commit()
cur.close()
conn.close()
return jsonify(task), 201
@app.route("/api/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
conn = get_db()
cur = conn.cursor()
cur.execute(
"UPDATE tasks SET completed = NOT completed WHERE id = %s RETURNING *",
(task_id,),
)
task = cur.fetchone()
conn.commit()
cur.close()
conn.close()
if task is None:
return jsonify({"error": "Task not found"}), 404
return jsonify(task)
@app.route("/api/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
conn = get_db()
cur = conn.cursor()
cur.execute("DELETE FROM tasks WHERE id = %s RETURNING id", (task_id,))
deleted = cur.fetchone()
conn.commit()
cur.close()
conn.close()
if deleted is None:
return jsonify({"error": "Task not found"}), 404
return jsonify({"result": "ok"})
if __name__ == "__main__":
init_db()
app.run(host="0.0.0.0", port=5000)

3
backend/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
flask==3.1.1
psycopg2-binary==2.9.10
gunicorn==23.0.0

72
frontend/app.js Normal file
View File

@ -0,0 +1,72 @@
const taskList = document.getElementById("task-list");
const taskForm = document.getElementById("task-form");
const taskInput = document.getElementById("task-input");
const emptyMsg = document.getElementById("empty-msg");
async function fetchTasks() {
const res = await fetch("/api/tasks");
const tasks = await res.json();
renderTasks(tasks);
}
function renderTasks(tasks) {
taskList.innerHTML = "";
emptyMsg.style.display = tasks.length === 0 ? "block" : "none";
tasks.forEach(function (task) {
const li = document.createElement("li");
li.className = "task-item" + (task.completed ? " completed" : "");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "task-checkbox";
checkbox.checked = task.completed;
checkbox.addEventListener("change", function () {
toggleTask(task.id);
});
const title = document.createElement("span");
title.className = "task-title";
title.textContent = task.title;
const deleteBtn = document.createElement("button");
deleteBtn.className = "task-delete";
deleteBtn.textContent = "\u00D7";
deleteBtn.title = "Delete task";
deleteBtn.addEventListener("click", function () {
deleteTask(task.id);
});
li.appendChild(checkbox);
li.appendChild(title);
li.appendChild(deleteBtn);
taskList.appendChild(li);
});
}
taskForm.addEventListener("submit", async function (e) {
e.preventDefault();
const title = taskInput.value.trim();
if (!title) return;
await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: title }),
});
taskInput.value = "";
fetchTasks();
});
async function toggleTask(id) {
await fetch("/api/tasks/" + id, { method: "PUT" });
fetchTasks();
}
async function deleteTask(id) {
await fetch("/api/tasks/" + id, { method: "DELETE" });
fetchTasks();
}
fetchTasks();

21
frontend/index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="container">
<h1>Task Manager</h1>
<form id="task-form">
<input type="text" id="task-input" placeholder="Enter a new task..." required>
<button type="submit">Add</button>
</form>
<ul id="task-list"></ul>
<p id="empty-msg" class="empty">No tasks yet. Add one above!</p>
</div>
<script src="/app.js"></script>
</body>
</html>

120
frontend/style.css Normal file
View File

@ -0,0 +1,120 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f0f2f5;
color: #1a1a2e;
min-height: 100vh;
display: flex;
justify-content: center;
padding-top: 60px;
}
.container {
width: 100%;
max-width: 520px;
padding: 0 16px;
}
h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 24px;
text-align: center;
}
#task-form {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
#task-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.2s;
}
#task-input:focus {
border-color: #4a6cf7;
}
#task-form button {
padding: 12px 24px;
background: #4a6cf7;
color: #fff;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
#task-form button:hover {
background: #3a5ce5;
}
#task-list {
list-style: none;
}
.task-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: #fff;
border-radius: 8px;
margin-bottom: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: opacity 0.2s;
}
.task-item.completed .task-title {
text-decoration: line-through;
opacity: 0.5;
}
.task-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #4a6cf7;
}
.task-title {
flex: 1;
font-size: 16px;
}
.task-delete {
background: none;
border: none;
color: #e74c3c;
font-size: 18px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
opacity: 0.6;
transition: opacity 0.2s;
}
.task-delete:hover {
opacity: 1;
}
.empty {
text-align: center;
color: #888;
font-size: 14px;
margin-top: 16px;
}