commit 0a58373d8600945f4ec181cb5532474d274009cb Author: Brazing Technology Date: Wed Apr 29 11:20:19 2026 +0530 chore: initial repo, copy source from assignment 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e54bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.log +.DS_Store +Thumbs.db +.idea/ +.vscode/ +__pycache__/ +*.pyc +.env diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..8bf6059 --- /dev/null +++ b/backend/app.py @@ -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/", 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/", 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) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..e223192 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +flask==3.1.1 +psycopg2-binary==2.9.10 +gunicorn==23.0.0 diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..afc2ce3 --- /dev/null +++ b/frontend/app.js @@ -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(); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..118cbb8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,21 @@ + + + + + + Task Manager + + + +
+

Task Manager

+
+ + +
+
    +

    No tasks yet. Add one above!

    +
    + + + diff --git a/frontend/style.css b/frontend/style.css new file mode 100644 index 0000000..2bb64a3 --- /dev/null +++ b/frontend/style.css @@ -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; +}