From cbef48d866df4669890411f1a699a6683b79b4ca Mon Sep 17 00:00:00 2001 From: Rashed-alothman Date: Wed, 18 Feb 2026 13:12:56 +0300 Subject: [PATCH] feat: ui redesign, blueprint url_for fixes, new templates --- app.py | 241 +------ models/__init__.py | 1 + models/database.py | 46 ++ routes/__init__.py | 0 routes/task_routes.py | 159 +++++ static/css/style.css | 1458 ++++++++++++++++++++++++-------------- templates/about.html | 320 ++++++--- templates/add_users.html | 158 +++-- templates/base.html | 156 ++-- templates/homepage.html | 678 ++++++++++-------- templates/index.html | 51 -- templates/landing.html | 243 +++++-- templates/login.html | 193 ++++- views/__init__.py | 0 views/page_views.py | 33 + 15 files changed, 2426 insertions(+), 1311 deletions(-) create mode 100644 models/__init__.py create mode 100644 models/database.py create mode 100644 routes/__init__.py create mode 100644 routes/task_routes.py delete mode 100644 templates/index.html create mode 100644 views/__init__.py create mode 100644 views/page_views.py diff --git a/app.py b/app.py index d1225bc7..bedb0d9b 100644 --- a/app.py +++ b/app.py @@ -1,224 +1,49 @@ -# Date: 21/12/2025--dd/mm/yyyy. -# Auther: Rashed Alothman. - -# Description: This is a simple Task Management System (TMS) web application built using Flask and SQLAlchemy. -# The application allows users to add, delete, and view tasks through both HTML pages and RESTful API endpoints. - -# Routes: -# '/' → landing/diagnostic page -# '/homepage' → main dashboard -# '/homepage/tasks/add_tasks' → add a task -# '/homepage/task/delete_task' → delete a task -# '/homepage/AddUsersToAccount' → placeholder -# '/homepage/User/about' → about‑me page -# '/login' → login page +# app.py +# The controller — this file's only job is to wire everything together. +# It creates the app, loads config, registers blueprints, and starts the server. import logging -from datetime import datetime -from flask import Flask, request, render_template, redirect, url_for, jsonify -from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.exc import SQLAlchemyError -from uuid import uuid4 -from typing import Optional import os +from flask import Flask +from models.database import db +from routes.task_routes import task_api +from views.page_views import pages -answer_for_data_not_found = 'Invalid or missing data' -error_massage_for_try_except_Exception_in_jsonify_fromat='An unexpected error occurred' -error_massage_for_database = 'Database error occurred' -# Initialize Flask app -app = Flask(__name__, template_folder='templates', static_folder='static') -# Database configuration -app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///tms.db" -# if you tracks changes to objects and sends signals before and after modifications just change the value to True; it may have a performance impact -app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False -# Initialize SQLAlchemy -db = SQLAlchemy(app) - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Define the base model -class Task(db.Model): - __tablename__ = 'tasks' - id: Mapped[str] = mapped_column(String(8), primary_key=True, default=lambda: str(uuid4())[:8]) - description: Mapped[str] = mapped_column(String(255), nullable=False) - due_date: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) - completed: Mapped[bool] = mapped_column(db.Boolean, default=False) - priority: Mapped[str] = mapped_column(String(10),default="low") - created_at: Mapped[datetime] = mapped_column(db.DateTime, default=datetime.utcnow) - - - # Representation method for debugging - def __repr__(self) -> str: - due_date_str = self.due_date.strftime('%Y-%m-%d %H:%M:%S') if self.due_date else 'None' - return f"Task(id={self.id}, description={self.description}, due_date={due_date_str}, completed={self.completed})" - - # Convert to dictionary - def to_dict(self): - return { - 'id': self.id, - 'description': self.description, - 'Due Date': self.due_date.strftime('%Y-%m-%d %H:%M:%S') if self.due_date else None, - 'completed': self.completed, - 'priority': self.priority, - 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S')} - -@app.route('/') -def landing(): - """Landing page.""" - return render_template('landing.html') - -@app.route('/homepage') -def homepage(): - """Dashboard - displays all tasks.""" - return render_template('homepage.html') - -@app.route('/login') -def login(): - """Login page.""" - return render_template('login.html') - -@app.route('/about') -def about(): - """About page.""" - return render_template('about.html') - -@app.route('/homepage/api/tasks', methods=['GET']) -def get_tasks(): - try: - completed_param = request.args.get('completed') - - priority_param = request.args.get('priority') - - sort_by = request.args.get('sort','created_at') - - query = Task.query - if completed_param is not None: - completed_bool = completed_param.lower() == 'true' - query = query.filter_by(completed=completed_bool) +def create_app(): + """ + Application factory function. + Calling this function creates and configures a fresh Flask app. + This pattern makes testing much easier — you can create isolated app instances per test. + """ + app = Flask(__name__, template_folder="templates", static_folder="static") + # --- Configuration --- + app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get( + "DATABASE_URL", "sqlite:///tms.db" + ) + app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False - if priority_param : - query = query.filter_by(priority=priority_param) + # --- Initialize extensions with this app --- + db.init_app(app) - if sort_by == 'created_at': - query = query.order_by(Task.created_at.desc()) - elif sort_by == 'due_date': - query = query.order_by(Task.due_date.asc().nullslast()) - elif sort_by == 'priority': - priority_order = ['urgent', 'high', 'medium', 'low'] - query = query.order_by(db.case({p: i for i, p in enumerate(priority_order)}, value=Task.priority)) + # --- Register Blueprints (route groups) --- + app.register_blueprint(pages) # HTML pages: /, /homepage, /login, /about + app.register_blueprint(task_api) # REST API: /homepage/api/tasks/... - tasks = query.all() - return jsonify({'Tasks': [task.to_dict() for task in tasks]}) - except SQLAlchemyError as e: - logger.error(f"Database error while Looking and sorting the tasks: {str(e)}") - return jsonify({'error': error_massage_for_database}), 500 - except Exception as e: - logger.error(f'there was error in showing Tasks: {str(e)}') - return jsonify({'error': error_massage_for_try_except_Exception_in_jsonify_fromat}), 500 + # --- Create database tables if they don't exist --- + with app.app_context(): + db.create_all() -@app.route('/homepage/api/tasks/add_Tasks',methods=['POST']) -def add_task_api(): - try: - data = request.get_json() + return app - if not data or 'description' not in data: - return jsonify({'error': answer_for_data_not_found}), 400 - description = data.get('description') - - priority = data.get('priority', 'low').lower() - - valid_priorities = ['low', 'medium', 'high', 'urgent'] - - if priority not in valid_priorities: - return jsonify({'error': f'Priority must be one of: {valid_priorities}'}), 400 - - new_task = Task( - description = description, - due_date = datetime.now(), - priority = priority - ) - db.session.add(new_task) - db.session.commit() - return jsonify({'message':'task added','task':new_task.to_dict()}),201 - except SQLAlchemyError as e: - db.session.rollback() - logger.error(f"Database error while adding task: {str(e)}") - return jsonify({'error': error_massage_for_database}), 500 - except Exception as e: - logger.error(f"Unexpected error while adding task: {str(e)}") - return jsonify({'error': error_massage_for_try_except_Exception_in_jsonify_fromat}), 500 - -@app.route('/homepage/api/tasks/delete_task',methods=['DELETE']) -def delete_task(): - try: - data = request.get_json() - - if not data: - return jsonify({'error': answer_for_data_not_found}), 400 - - task_id = data.get('id') - - if not task_id: - return jsonify({'error': 'Task ID is required'}), 400 - - task = Task.query.filter_by(id=task_id).first() - - if not task: - return jsonify({'message': 'Task not found'}), 404 - - db.session.delete(task) - db.session.commit() - logger.info(f"Task deleted: {task_id}") - return jsonify({'message': 'Task deleted successfully'}), 200 - - except SQLAlchemyError as e: - db.session.rollback() - logger.error(f"Database error while deleting task: {str(e)}") - return jsonify({'error': error_massage_for_database}), 500 - except Exception as e: - logger.error(f"Unexpected error while deleting task: {str(e)}") - return jsonify({'error': error_massage_for_try_except_Exception_in_jsonify_fromat}), 500 - - -@app.route('/homepage/api/tasks/updated_task',methods=["PATCH"]) -def updated_task(): - - data = request.get_json() - if not data: - return jsonify({'error': answer_for_data_not_found}), 400 - - task_id = data.get('id') - if not task_id: - return jsonify({'error': 'Task ID is required'}), 400 - - task = Task.query.filter_by(id=task_id).first() - - if not task: - return jsonify({'message': 'Task not found'}), 404 - - new_description = data.get('description') - completed = data.get('completed') - - if new_description is not None: - task.description = new_description - if completed is not None: - task.completed = completed - - db.session.commit() - - return jsonify({'message': 'The Task has been updated', 'task': task.to_dict()}), 200 +# Configure logging once, at startup +logging.basicConfig(level=logging.INFO) -with app.app_context(): - db.create_all() +app = create_app() -if __name__ == '__main__': +if __name__ == "__main__": debug_mode = os.environ.get("FLASK_DEBUG", "0") == "1" - app.run(debug=debug_mode, host='0.0.0.0', port=5000) \ No newline at end of file + app.run(debug=debug_mode, host="0.0.0.0", port=5000) diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 00000000..e4a719d3 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1 @@ +from .database import Task, db diff --git a/models/database.py b/models/database.py new file mode 100644 index 00000000..5ad66797 --- /dev/null +++ b/models/database.py @@ -0,0 +1,46 @@ +# models/database.py +# Handles all database setup and model definitions. +# This is the single source of truth for your data structure. + +from datetime import datetime +from typing import Optional +from uuid import uuid4 + +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import String +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Mapped, mapped_column + +# Create the db instance here — import it everywhere else +db = SQLAlchemy() + + +class Task(db.Model): + __tablename__ = "tasks" + + id: Mapped[str] = mapped_column( + String(8), primary_key=True, default=lambda: str(uuid4())[:8] + ) + description: Mapped[str] = mapped_column(String(255), nullable=False) + due_date: Mapped[Optional[datetime]] = mapped_column(db.DateTime, nullable=True) + completed: Mapped[bool] = mapped_column(db.Boolean, default=False) + priority: Mapped[str] = mapped_column(String(10), default="low") + created_at: Mapped[datetime] = mapped_column(db.DateTime, default=datetime.utcnow) + + def __repr__(self) -> str: + due_date_str = ( + self.due_date.strftime("%Y-%m-%d %H:%M:%S") if self.due_date else "None" + ) + return f"Task(id={self.id}, description={self.description}, due_date={due_date_str}, completed={self.completed})" + + def to_dict(self): + return { + "id": self.id, + "description": self.description, + "Due Date": self.due_date.strftime("%Y-%m-%d %H:%M:%S") + if self.due_date + else None, + "completed": self.completed, + "priority": self.priority, + "created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"), + } diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/routes/task_routes.py b/routes/task_routes.py new file mode 100644 index 00000000..957aecd2 --- /dev/null +++ b/routes/task_routes.py @@ -0,0 +1,159 @@ +# routes/task_routes.py +# Handles all REST API endpoints for tasks. +# Each function here does ONE thing: receive a request, call logic, return JSON. + +import logging +from datetime import datetime + +from flask import Blueprint, jsonify, request +from models.database import Task, db +from sqlalchemy.exc import SQLAlchemyError + +logger = logging.getLogger(__name__) + +# A Blueprint is like a mini-app. We register it in app.py. +# The url_prefix means every route here starts with /homepage/api/tasks +task_api = Blueprint("task_api", __name__, url_prefix="/homepage/api/tasks") + +# --- Reusable error messages --- +ERR_INVALID_DATA = "Invalid or missing data" +ERR_UNEXPECTED = "An unexpected error occurred" +ERR_DATABASE = "Database error occurred" + + +@task_api.route("", methods=["GET"]) +def get_tasks(): + """Return all tasks, with optional filtering and sorting.""" + try: + completed_param = request.args.get("completed") + priority_param = request.args.get("priority") + sort_by = request.args.get("sort", "created_at") + + query = Task.query + + if completed_param is not None: + completed_bool = completed_param.lower() == "true" + query = query.filter_by(completed=completed_bool) + + if priority_param: + query = query.filter_by(priority=priority_param) + + if sort_by == "created_at": + query = query.order_by(Task.created_at.desc()) + elif sort_by == "due_date": + query = query.order_by(Task.due_date.asc().nullslast()) + elif sort_by == "priority": + priority_order = ["urgent", "high", "medium", "low"] + query = query.order_by( + db.case( + {p: i for i, p in enumerate(priority_order)}, value=Task.priority + ) + ) + + tasks = query.all() + return jsonify({"Tasks": [task.to_dict() for task in tasks]}) + + except SQLAlchemyError as e: + logger.error(f"Database error while fetching tasks: {str(e)}") + return jsonify({"error": ERR_DATABASE}), 500 + except Exception as e: + logger.error(f"Unexpected error while fetching tasks: {str(e)}") + return jsonify({"error": ERR_UNEXPECTED}), 500 + + +@task_api.route("/add_tasks", methods=["POST"]) +def add_task(): + """Create a new task.""" + try: + data = request.get_json() + + if not data or "description" not in data: + return jsonify({"error": ERR_INVALID_DATA}), 400 + + priority = data.get("priority", "low").lower() + valid_priorities = ["low", "medium", "high", "urgent"] + + if priority not in valid_priorities: + return jsonify( + {"error": f"Priority must be one of: {valid_priorities}"} + ), 400 + + new_task = Task( + description=data["description"], due_date=datetime.now(), priority=priority + ) + db.session.add(new_task) + db.session.commit() + return jsonify({"message": "Task added", "task": new_task.to_dict()}), 201 + + except SQLAlchemyError as e: + db.session.rollback() + logger.error(f"Database error while adding task: {str(e)}") + return jsonify({"error": ERR_DATABASE}), 500 + except Exception as e: + logger.error(f"Unexpected error while adding task: {str(e)}") + return jsonify({"error": ERR_UNEXPECTED}), 500 + + +@task_api.route("/delete_task", methods=["DELETE"]) +def delete_task(): + """Delete a task by ID.""" + try: + data = request.get_json() + + if not data: + return jsonify({"error": ERR_INVALID_DATA}), 400 + + task_id = data.get("id") + if not task_id: + return jsonify({"error": "Task ID is required"}), 400 + + task = Task.query.filter_by(id=task_id).first() + if not task: + return jsonify({"message": "Task not found"}), 404 + + db.session.delete(task) + db.session.commit() + logger.info(f"Task deleted: {task_id}") + return jsonify({"message": "Task deleted successfully"}), 200 + + except SQLAlchemyError as e: + db.session.rollback() + logger.error(f"Database error while deleting task: {str(e)}") + return jsonify({"error": ERR_DATABASE}), 500 + except Exception as e: + logger.error(f"Unexpected error while deleting task: {str(e)}") + return jsonify({"error": ERR_UNEXPECTED}), 500 + + +@task_api.route("/update_task", methods=["PATCH"]) +def update_task(): + """Update a task's description or completion status.""" + try: + data = request.get_json() + + if not data: + return jsonify({"error": ERR_INVALID_DATA}), 400 + + task_id = data.get("id") + if not task_id: + return jsonify({"error": "Task ID is required"}), 400 + + task = Task.query.filter_by(id=task_id).first() + if not task: + return jsonify({"message": "Task not found"}), 404 + + if data.get("description") is not None: + task.description = data["description"] + if data.get("completed") is not None: + task.completed = data["completed"] + + db.session.commit() + return jsonify({"message": "Task updated", "task": task.to_dict()}), 200 + + except SQLAlchemyError as e: + db.session.rollback() + logger.error(f"Database error while updating task: {str(e)}") + return jsonify({"error": ERR_DATABASE}), 500 + except Exception as e: + logger.error(f"Unexpected error while updating task: {str(e)}") + return jsonify({"error": ERR_UNEXPECTED}), 500 diff --git a/static/css/style.css b/static/css/style.css index 8cf1f760..56431499 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,685 +1,1093 @@ +/* ============================================================ + TMS — Task Master Design System + Theme: Refined Dark · Amber Accent · Syne + DM Sans + ============================================================ */ + +/* ── Reset & Base ─────────────────────────────────────────── */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + :root { - /* Color Palette */ - --bg-dark: #0a0e27; - --bg-secondary: #151b2f; - --bg-tertiary: #1e293b; - --bg-gradient: linear-gradient(135deg, #0f172a 0%, #1a1f3a 50%, #0d1429 100%); - - --glass-bg: rgba(255, 255, 255, 0.06); - --glass-border: rgba(255, 255, 255, 0.12); - --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.05); - - --primary: #6366f1; - --primary-hover: #4f46e5; - --primary-dark: #4338ca; - --primary-light: rgba(99, 102, 241, 0.12); - - --secondary: #a855f7; - --secondary-light: rgba(168, 85, 247, 0.12); - - --danger: #ef4444; - --danger-hover: #dc2626; - --danger-light: rgba(239, 68, 68, 0.12); - - --success: #10b981; - --success-hover: #059669; - --success-light: rgba(16, 185, 129, 0.12); - - --warning: #f59e0b; - --warning-light: rgba(245, 158, 11, 0.12); - - --text-main: #f1f5f9; - --text-muted: #94a3b8; - --text-light: #64748b; - - --radius: 12px; - --radius-lg: 16px; - --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Reset & Base */ -*, *::before, *::after { - box-sizing: border-box; + /* Core palette */ + --bg: #080d1a; + --bg-1: #0d1526; + --bg-2: #111d33; + --bg-3: #162240; + --border: rgba(255, 255, 255, 0.07); + --border-hover: rgba(255, 255, 255, 0.14); + + /* Amber accent system */ + --amber: #f59e0b; + --amber-bright: #fbbf24; + --amber-dim: rgba(245, 158, 11, 0.12); + --amber-glow: rgba(245, 158, 11, 0.25); + + /* Text */ + --text: #f1f5f9; + --text-2: #94a3b8; + --text-3: #475569; + + /* Semantic */ + --success: #10b981; + --success-dim: rgba(16, 185, 129, 0.1); + --danger: #ef4444; + --danger-dim: rgba(239, 68, 68, 0.1); + --warning: #f59e0b; + --info: #6366f1; + + /* Typography */ + --font-display: "Syne", sans-serif; + --font-body: "DM Sans", sans-serif; + + /* Geometry */ + --radius-sm: 6px; + --radius: 10px; + --radius-lg: 16px; + --radius-xl: 24px; + + /* Transitions */ + --ease: cubic-bezier(0.16, 1, 0.3, 1); + --fast: 0.15s var(--ease); + --med: 0.3s var(--ease); + + /* Shadows */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + --shadow: 0 4px 16px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.6); + --shadow-amber: 0 0 30px rgba(245, 158, 11, 0.15); } html { - scroll-behavior: smooth; + scroll-behavior: smooth; } body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; - background: var(--bg-dark); - background-image: - radial-gradient(at 20% 50%, rgba(99, 102, 241, 0.15) 0px, transparent 50%), - radial-gradient(at 80% 80%, rgba(168, 85, 247, 0.1) 0px, transparent 50%), - radial-gradient(at 40% 0%, rgba(99, 102, 241, 0.1) 0px, transparent 50%); - color: var(--text-main); - line-height: 1.6; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.page-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -/* Navbar / Header */ + font-family: var(--font-body); + background: var(--bg); + color: var(--text); + line-height: 1.6; + min-height: 100vh; + display: flex; + flex-direction: column; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; +} + +/* Grain texture overlay */ +.grain { + position: fixed; + inset: 0; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E"); + background-size: 200px 200px; + pointer-events: none; + z-index: 0; + opacity: 0.6; +} + +/* ── Navigation ───────────────────────────────────────────── */ .nav { - padding: 16px 0; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-bottom: 1px solid var(--glass-border); - position: sticky; - top: 0; - z-index: 100; - background: rgba(15, 23, 42, 0.8); - box-shadow: var(--glass-shadow); + position: sticky; + top: 0; + z-index: 100; + background: rgba(8, 13, 26, 0.7); + backdrop-filter: blur(20px) saturate(1.8); + -webkit-backdrop-filter: blur(20px) saturate(1.8); + border-bottom: 1px solid transparent; + transition: + border-color var(--med), + background var(--med); +} + +.nav.scrolled { + background: rgba(8, 13, 26, 0.92); + border-bottom-color: var(--border); } .nav-inner { - max-width: 900px; - margin: 0 auto; - padding: 0 20px; - display: flex; - justify-content: space-between; - align-items: center; - gap: 20px; + max-width: 1100px; + margin: 0 auto; + padding: 0 24px; + height: 64px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; } .brand { - font-weight: 700; - font-size: 1.3rem; - background: linear-gradient(135deg, #818cf8 0%, #c084fc 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - text-decoration: none; - letter-spacing: -0.5px; - transition: var(--transition); - display: flex; - align-items: center; - gap: 8px; + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: var(--text); + font-family: var(--font-display); + font-size: 1.1rem; + letter-spacing: -0.01em; +} + +.brand strong { + color: var(--amber); + font-weight: 800; } -.brand:hover { - transform: translateY(-2px); +.brand-icon { + width: 34px; + height: 34px; + background: var(--amber-dim); + border: 1px solid var(--amber-glow); + border-radius: var(--radius-sm); + display: grid; + place-items: center; + color: var(--amber); + flex-shrink: 0; } .nav-links { - display: flex; - gap: 12px; - align-items: center; - margin-left: auto; + display: flex; + align-items: center; + gap: 4px; } -/* Layout */ -.container { - max-width: 700px; - margin: 0 auto; - padding: 0 20px; - width: 100%; - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; +.nav-link { + padding: 6px 14px; + color: var(--text-2); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + border-radius: var(--radius-sm); + transition: + color var(--fast), + background var(--fast); } -.content-wrapper { - flex: 1; - display: flex; - flex-direction: column; - padding: 40px 20px; +.nav-link:hover, +.nav-link.active { + color: var(--text); + background: var(--bg-3); } -/* Card Styling */ -.card { - background: var(--glass-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--glass-border); - border-radius: var(--radius-lg); - padding: 40px; - box-shadow: var(--glass-shadow); - animation: fadeInUp 0.6s ease-out; - position: relative; - overflow: hidden; -} - -.card::before { - content: ''; - position: absolute; - top: -40%; - right: -40%; - width: 300px; - height: 300px; - background: radial-gradient(circle, rgba(99, 102, 241, 0.08) 0%, transparent 70%); - border-radius: 50%; - pointer-events: none; -} - -.card::after { - content: ''; - position: absolute; - bottom: -20%; - left: -20%; - width: 200px; - height: 200px; - background: radial-gradient(circle, rgba(168, 85, 247, 0.05) 0%, transparent 70%); - border-radius: 50%; - pointer-events: none; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } +.nav-link.active { + color: var(--amber); } -.card-header { - margin-bottom: 32px; - text-align: center; - position: relative; - z-index: 1; +.nav-cta { + padding: 7px 18px; + background: var(--amber-dim); + border: 1px solid var(--amber-glow); + color: var(--amber); + text-decoration: none; + font-size: 0.875rem; + font-weight: 600; + border-radius: var(--radius-sm); + transition: + background var(--fast), + box-shadow var(--fast); + font-family: var(--font-display); + letter-spacing: 0.01em; } -.card-title { - font-size: 2.2rem; - font-weight: 800; - margin: 0 0 12px 0; - letter-spacing: -0.03em; - background: linear-gradient(135deg, #f1f5f9 0%, #cbd5e1 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - line-height: 1.1; +.nav-cta:hover { + background: rgba(245, 158, 11, 0.2); + box-shadow: 0 0 16px var(--amber-glow); } -.card-subtitle { - color: var(--text-muted); - font-size: 1rem; - margin: 0; +/* Mobile menu */ +.mobile-menu-btn { + display: none; + flex-direction: column; + gap: 5px; + background: none; + border: none; + cursor: pointer; + padding: 4px; } -/* Typography */ -h1, h2, h3, h4, h5, h6 { - margin: 0; - font-weight: 700; - letter-spacing: -0.025em; +.mobile-menu-btn span { + display: block; + width: 22px; + height: 2px; + background: var(--text-2); + border-radius: 2px; + transition: var(--med); + transform-origin: center; } -p { - margin: 0; +.mobile-menu-btn.open span:nth-child(1) { + transform: translateY(7px) rotate(45deg); +} +.mobile-menu-btn.open span:nth-child(2) { + opacity: 0; +} +.mobile-menu-btn.open span:nth-child(3) { + transform: translateY(-7px) rotate(-45deg); } -.text-center { text-align: center; } -.text-muted { color: var(--text-muted); } -.text-light { color: var(--text-light); } +.mobile-menu { + display: none; + flex-direction: column; + border-top: 1px solid var(--border); + padding: 8px 16px 16px; +} -/* Forms & Inputs */ -.form-group { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 16px; +.mobile-menu.open { + display: flex; } -.form-label { - font-size: 0.9rem; - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; +.mobile-link { + padding: 12px 8px; + color: var(--text-2); + text-decoration: none; + font-size: 0.95rem; + border-bottom: 1px solid var(--border); } -input[type="text"], -input[type="email"], -input[type="password"], -input[type="date"], -select, -textarea { - width: 100%; - padding: 12px 16px; - background: rgba(0, 0, 0, 0.3); - border: 1px solid var(--glass-border); - border-radius: var(--radius); - color: var(--text-main); - font-family: inherit; - font-size: 1rem; - outline: none; - transition: var(--transition); -} - -input[type="text"]:focus, -input[type="email"]:focus, -input[type="password"]:focus, -input[type="date"]:focus, -select:focus, -textarea:focus { - border-color: var(--primary); - background: rgba(0, 0, 0, 0.6); - box-shadow: 0 0 0 4px var(--primary-light), inset 0 2px 4px rgba(0, 0, 0, 0.2); +.mobile-link:last-child { + border-bottom: none; } -input::placeholder { - color: var(--text-light); +/* ── Layout ───────────────────────────────────────────────── */ +.main { + flex: 1; + position: relative; + z-index: 1; } -/* Buttons */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 11px 20px; - border-radius: var(--radius); - font-weight: 600; - font-size: 0.95rem; - cursor: pointer; - border: none; - text-decoration: none; - transition: var(--transition); - gap: 8px; - white-space: nowrap; - user-select: none; +.container { + max-width: 1100px; + margin: 0 auto; + padding: 0 24px; } -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; +.section { + padding: 80px 0; } -.btn.primary { - background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); - color: white; - box-shadow: 0 8px 24px rgba(99, 102, 241, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.2); +/* ── Footer ───────────────────────────────────────────────── */ +.footer { + border-top: 1px solid var(--border); + background: var(--bg-1); + position: relative; + z-index: 1; } -.btn.primary:hover:not(:disabled) { - background: linear-gradient(135deg, var(--primary-hover) 0%, #9333ea 100%); - transform: translateY(-3px); - box-shadow: 0 12px 32px rgba(99, 102, 241, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2); +.footer-inner { + max-width: 1100px; + margin: 0 auto; + padding: 24px; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; } -.btn.primary:active:not(:disabled) { - transform: translateY(0); +.footer-brand { + font-family: var(--font-display); + font-size: 0.95rem; + color: var(--text-2); } -.btn.secondary { - background: linear-gradient(135deg, rgba(168, 85, 247, 0.2) 0%, rgba(99, 102, 241, 0.15) 100%); - color: var(--text-main); - border: 1px solid rgba(168, 85, 247, 0.4); +.footer-brand strong { + color: var(--amber); +} +.footer-copy { + font-size: 0.825rem; + color: var(--text-3); } -.btn.secondary:hover:not(:disabled) { - background: linear-gradient(135deg, rgba(168, 85, 247, 0.3) 0%, rgba(99, 102, 241, 0.25) 100%); - border-color: rgba(168, 85, 247, 0.6); - transform: translateY(-3px); - box-shadow: 0 8px 24px rgba(168, 85, 247, 0.2); +.footer-links { + display: flex; + gap: 20px; } -.btn.danger { - background: var(--danger-light); - color: #fca5a5; - border: 1px solid rgba(239, 68, 68, 0.3); +.footer-links a { + font-size: 0.825rem; + color: var(--text-3); + text-decoration: none; + transition: color var(--fast); } -.btn.danger:hover:not(:disabled) { - background: var(--danger); - color: white; - border-color: var(--danger); +.footer-links a:hover { + color: var(--amber); } -.btn.success { - background: var(--success-light); - color: #6ee7b7; - border: 1px solid rgba(16, 185, 129, 0.3); +/* ── Cards ────────────────────────────────────────────────── */ +.card { + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; } -.btn.success:hover:not(:disabled) { - background: var(--success); - color: white; - border-color: var(--success); +.card-header { + padding: 32px 32px 0; } -.btn.ghost { - background: transparent; - color: var(--text-muted); - border: none; +.card-body { + padding: 24px 32px 32px; } -.btn.ghost:hover:not(:disabled) { - color: var(--text-main); - background: rgba(255, 255, 255, 0.08); +.card-title { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 700; + color: var(--text); + letter-spacing: -0.02em; + margin-bottom: 4px; } -.btn.small { - padding: 8px 12px; - font-size: 0.875rem; +.card-subtitle { + color: var(--text-2); + font-size: 0.9rem; + font-weight: 300; } -.btn.small svg { - width: 16px; - height: 16px; +/* ── Buttons ──────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 20px; + font-family: var(--font-body); + font-size: 0.875rem; + font-weight: 500; + border: 1px solid transparent; + border-radius: var(--radius-sm); + cursor: pointer; + text-decoration: none; + transition: all var(--fast); + white-space: nowrap; + letter-spacing: 0.01em; } .btn svg { - width: 20px; - height: 20px; - flex-shrink: 0; + width: 16px; + height: 16px; + flex-shrink: 0; } -/* Task Add Form */ -.task-input-group { - display: flex; - gap: 8px; - margin-bottom: 24px; - animation: slideDown 0.4s ease-out; +.btn-primary { + background: var(--amber); + color: #0a0f1e; + font-weight: 700; + border-color: var(--amber); } -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; +.btn-primary:hover { + background: var(--amber-bright); + box-shadow: 0 0 20px var(--amber-glow); + transform: translateY(-1px); +} + +.btn-primary:active { transform: translateY(0); - } } -.task-input-group input { - flex: 1; +.btn-secondary { + background: var(--bg-3); + color: var(--text); + border-color: var(--border-hover); } -/* Task List */ -.task-list { - display: flex; - flex-direction: column; - gap: 10px; - margin-top: 24px; +.btn-secondary:hover { + background: var(--bg-2); + border-color: var(--border-hover); } -.task-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: var(--radius); - transition: var(--transition); - animation: slideUp 0.4s ease-out; - overflow: hidden; - position: relative; -} - -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } +.btn-ghost { + background: transparent; + color: var(--text-2); + border-color: var(--border); } -.task-item:hover { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.15); - transform: translateX(4px); - box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1); +.btn-ghost:hover { + background: var(--bg-2); + color: var(--text); + border-color: var(--border-hover); } -.task-item.completed { - opacity: 0.6; +.btn-danger { + background: var(--danger-dim); + color: var(--danger); + border-color: rgba(239, 68, 68, 0.2); } -.task-content { - display: flex; - align-items: center; - gap: 12px; - flex: 1; - min-width: 0; +.btn-danger:hover { + background: rgba(239, 68, 68, 0.2); + box-shadow: 0 0 12px rgba(239, 68, 68, 0.15); } -.task-checkbox { - width: 20px; - height: 20px; - border: 2px solid var(--text-muted); - border-radius: 50%; - background: transparent; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: var(--transition); - flex-shrink: 0; +.btn-success { + background: var(--success-dim); + color: var(--success); + border-color: rgba(16, 185, 129, 0.2); } -.task-checkbox:hover { - border-color: var(--primary); +.btn-sm { + padding: 6px 12px; + font-size: 0.8rem; } -.task-checkbox input { - width: 100%; - height: 100%; - cursor: pointer; - opacity: 0; - margin: 0; +.btn-icon { + padding: 8px; + background: transparent; + border-color: var(--border); + color: var(--text-2); } -.task-item.completed .task-checkbox { - background: var(--success); - border-color: var(--success); - color: white; +.btn-icon:hover { + background: var(--bg-3); + color: var(--text); +} +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; } -.task-text { - font-size: 1rem; - color: var(--text-main); - word-break: break-word; +/* ── Form Elements ────────────────────────────────────────── */ +.form-group { + display: flex; + flex-direction: column; + gap: 16px; } -.task-item.completed .task-text { - text-decoration: line-through; - color: var(--text-muted); +.form-field { + display: flex; + flex-direction: column; + gap: 8px; } -.task-actions { - display: flex; - gap: 6px; - flex-shrink: 0; +.form-label { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-3); + text-transform: uppercase; + letter-spacing: 0.08em; } -.task-actions .btn { - padding: 6px 10px; +input[type="text"], +input[type="email"], +input[type="password"], +select, +textarea { + width: 100%; + padding: 12px 16px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font-body); + font-size: 0.95rem; + outline: none; + transition: + border-color var(--fast), + box-shadow var(--fast); +} + +input:focus, +select:focus, +textarea:focus { + border-color: var(--amber); + box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.12); } -/* Footer */ -.footer { - text-align: center; - padding: 32px 20px; - color: var(--text-muted); - font-size: 0.85rem; - margin-top: auto; - border-top: 1px solid rgba(255, 255, 255, 0.08); - background: linear-gradient(to bottom, transparent, rgba(99, 102, 241, 0.03)); - letter-spacing: 0.5px; - font-weight: 500; +input::placeholder { + color: var(--text-3); } -/* Add Task Form specific */ -.add-form { - position: relative; - display: flex; - gap: 8px; +select option { + background: var(--bg-1); } -.empty-state { - text-align: center; - padding: 60px 20px; - color: var(--text-muted); +/* ── Alerts ───────────────────────────────────────────────── */ +.alert { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + border-radius: var(--radius-sm); + font-size: 0.875rem; + font-weight: 500; + border: 1px solid; + animation: slideIn 0.25s var(--ease); +} + +.alert svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.alert-success { + background: var(--success-dim); + color: var(--success); + border-color: rgba(16, 185, 129, 0.25); +} +.alert-error { + background: var(--danger-dim); + color: var(--danger); + border-color: rgba(239, 68, 68, 0.25); +} +.alert-warning { + background: rgba(245, 158, 11, 0.1); + color: var(--amber); + border-color: rgba(245, 158, 11, 0.25); +} + +/* ── Priority Badges ──────────────────────────────────────── */ +.badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 9px; + border-radius: 99px; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.badge-urgent { + background: rgba(239, 68, 68, 0.15); + color: #f87171; + border: 1px solid rgba(239, 68, 68, 0.25); +} +.badge-high { + background: rgba(245, 158, 11, 0.15); + color: var(--amber); + border: 1px solid rgba(245, 158, 11, 0.25); +} +.badge-medium { + background: rgba(99, 102, 241, 0.15); + color: #a5b4fc; + border: 1px solid rgba(99, 102, 241, 0.25); +} +.badge-low { + background: rgba(148, 163, 184, 0.1); + color: var(--text-2); + border: 1px solid rgba(148, 163, 184, 0.15); +} + +/* ── Task List ────────────────────────────────────────────── */ +.task-list { + display: flex; + flex-direction: column; + gap: 1px; } -.empty-state-icon { - width: 60px; - height: 60px; - margin: 0 auto 20px; - opacity: 0.4; - stroke: currentColor; +.task-item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + background: var(--bg-1); + border-radius: var(--radius-sm); + border: 1px solid var(--border); + transition: + background var(--fast), + border-color var(--fast), + transform var(--fast); + animation: taskIn 0.25s var(--ease); } -.empty-state-title { - font-size: 1.25rem; - font-weight: 600; - margin-bottom: 8px; - color: var(--text-main); +.task-item:hover { + background: var(--bg-2); + border-color: var(--border-hover); + transform: translateX(2px); } -.empty-state-text { - font-size: 0.95rem; - margin: 0; +.task-item.completed { + opacity: 0.5; +} +.task-item.completed .task-text { + text-decoration: line-through; + color: var(--text-3); } -/* Loading & Status */ -.loading { - display: inline-flex; - align-items: center; - gap: 8px; +.task-check { + width: 22px; + height: 22px; + border-radius: 6px; + border: 2px solid var(--border-hover); + background: transparent; + cursor: pointer; + display: grid; + place-items: center; + flex-shrink: 0; + transition: all var(--fast); + color: transparent; } -.spinner { - width: 16px; - height: 16px; - border: 2px solid var(--primary-light); - border-top-color: var(--primary); - border-radius: 50%; - animation: spin 0.6s linear infinite; +.task-check:hover { + border-color: var(--amber); +} +.task-check.checked { + background: var(--amber); + border-color: var(--amber); + color: #0a0f1e; +} +.task-check svg { + width: 12px; + height: 12px; + stroke-width: 3; } -@keyframes spin { - to { transform: rotate(360deg); } +.task-content { + flex: 1; + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} +.task-text { + font-size: 0.95rem; + color: var(--text); + flex: 1; + min-width: 0; + word-break: break-word; +} +.task-meta { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} +.task-actions { + display: flex; + align-items: center; + gap: 6px; + opacity: 0; + transition: opacity var(--fast); +} +.task-item:hover .task-actions { + opacity: 1; +} +.task-date { + font-size: 0.75rem; + color: var(--text-3); } -.alert { - padding: 12px 16px; - border-radius: var(--radius); - margin-bottom: 16px; - display: flex; - align-items: center; - gap: 12px; - animation: slideDown 0.3s ease-out; +/* ── Stats Bar ────────────────────────────────────────────── */ +.stats-bar { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1px; + background: var(--border); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; } -.alert.error { - background: var(--danger-light); - border: 1px solid rgba(239, 68, 68, 0.3); - color: #fca5a5; +.stat-item { + background: var(--bg-1); + padding: 20px; + text-align: center; } -.alert.success { - background: var(--success-light); - border: 1px solid rgba(16, 185, 129, 0.3); - color: #6ee7b7; +.stat-value { + font-family: var(--font-display); + font-size: 2rem; + font-weight: 800; + color: var(--text); + line-height: 1; + letter-spacing: -0.03em; + transition: color 0.3s; } -.alert.warning { - background: rgba(245, 158, 11, 0.1); - border: 1px solid rgba(245, 158, 11, 0.3); - color: #fcd34d; +.stat-item:nth-child(2) .stat-value { + color: var(--amber); +} +.stat-item:nth-child(3) .stat-value { + color: var(--success); } -.alert.info { - background: var(--primary-light); - border: 1px solid rgba(99, 102, 241, 0.3); - color: #a5b4fc; +.stat-label { + font-size: 0.75rem; + color: var(--text-3); + margin-top: 4px; + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 600; } -.alert-icon { - width: 20px; - height: 20px; - flex-shrink: 0; +/* ── Task Input Group ─────────────────────────────────────── */ +.task-input-row { + display: flex; + gap: 10px; + align-items: stretch; } -/* Stats Bar */ -.stats-bar { - display: flex; - gap: 24px; - padding: 16px 0; - margin-bottom: 24px; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); +.task-input-row input { + flex: 1; } -.stat-item { - flex: 1; - text-align: center; +.priority-select { + width: auto !important; + min-width: 110px; + flex-shrink: 0; } -.stat-value { - font-size: 1.75rem; - font-weight: 700; - color: var(--primary); +/* ── Empty State ──────────────────────────────────────────── */ +.empty-state { + text-align: center; + padding: 60px 20px; + color: var(--text-3); } -.stat-label { - font-size: 0.85rem; - color: var(--text-muted); - margin-top: 4px; +.empty-state-icon { + width: 48px; + height: 48px; + margin: 0 auto 16px; + opacity: 0.3; + display: block; } -/* Utility Classes */ -.mb-0 { margin-bottom: 0; } -.mb-8 { margin-bottom: 8px; } -.mb-16 { margin-bottom: 16px; } -.mb-24 { margin-bottom: 24px; } +.empty-state-title { + font-family: var(--font-display); + font-size: 1.1rem; + color: var(--text-2); + margin-bottom: 6px; +} +.empty-state-text { + font-size: 0.9rem; +} + +/* ── Filter Bar ───────────────────────────────────────────── */ +.filter-bar { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.filter-btn { + padding: 5px 14px; + border-radius: 99px; + font-size: 0.8rem; + font-weight: 500; + border: 1px solid var(--border); + background: transparent; + color: var(--text-2); + cursor: pointer; + transition: all var(--fast); +} + +.filter-btn:hover { + border-color: var(--border-hover); + color: var(--text); +} +.filter-btn.active { + background: var(--amber-dim); + border-color: var(--amber-glow); + color: var(--amber); +} + +/* ── Page sections ────────────────────────────────────────── */ +.page-hero { + padding: 80px 0 60px; + text-align: center; + position: relative; +} + +.page-hero::before { + content: ""; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 600px; + height: 300px; + background: radial-gradient( + ellipse at center top, + rgba(245, 158, 11, 0.08) 0%, + transparent 70% + ); + pointer-events: none; +} + +.hero-tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 14px; + background: var(--amber-dim); + border: 1px solid var(--amber-glow); + border-radius: 99px; + font-size: 0.78rem; + font-weight: 600; + color: var(--amber); + letter-spacing: 0.05em; + text-transform: uppercase; + margin-bottom: 24px; +} + +.hero-title { + font-family: var(--font-display); + font-size: clamp(2.5rem, 6vw, 4.5rem); + font-weight: 800; + line-height: 1.05; + letter-spacing: -0.04em; + color: var(--text); + margin-bottom: 20px; +} + +.hero-title em { + font-style: normal; + color: var(--amber); +} + +.hero-subtitle { + font-size: 1.1rem; + color: var(--text-2); + font-weight: 300; + max-width: 520px; + margin: 0 auto 40px; + line-height: 1.7; +} + +.hero-actions { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} + +/* ── Feature Grid ─────────────────────────────────────────── */ +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; +} -.mt-8 { margin-top: 8px; } -.mt-16 { margin-top: 16px; } -.mt-24 { margin-top: 24px; } +.feature-card { + padding: 28px; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + transition: + border-color var(--med), + transform var(--med), + box-shadow var(--med); +} -.hidden { display: none !important; } -.opacity-50 { opacity: 0.5; } +.feature-card:hover { + border-color: var(--border-hover); + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.feature-icon { + width: 44px; + height: 44px; + border-radius: var(--radius-sm); + display: grid; + place-items: center; + margin-bottom: 18px; +} + +.feature-icon svg { + width: 22px; + height: 22px; +} + +.feature-title { + font-family: var(--font-display); + font-size: 1.05rem; + font-weight: 700; + color: var(--text); + margin-bottom: 8px; +} + +.feature-desc { + font-size: 0.875rem; + color: var(--text-2); + line-height: 1.65; +} -/* Responsive */ -@media (max-width: 640px) { - .card { - padding: 24px; - } +/* ── Tech Stack Pills ─────────────────────────────────────── */ +.tech-grid { + display: flex; + flex-wrap: wrap; + gap: 10px; +} - .card-title { - font-size: 1.5rem; - } +.tech-pill { + padding: 8px 16px; + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 0.85rem; + font-weight: 500; + color: var(--text-2); + display: flex; + align-items: center; + gap: 8px; +} - .task-input-group { - flex-direction: column; - } +.tech-pill-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--amber); +} - .task-input-group input, - .task-input-group .btn { - width: 100%; - } +/* ── Dividers & Spacing ───────────────────────────────────── */ +.divider { + height: 1px; + background: var(--border); + margin: 32px 0; +} - .nav-inner { - padding: 0 16px; - } +.section-label { + font-size: 0.72rem; + font-weight: 700; + color: var(--text-3); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 16px; +} - .stats-bar { - gap: 16px; - } +/* ── Animations ───────────────────────────────────────────── */ +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} - .task-actions { - gap: 4px; - } +@keyframes taskIn { + from { + opacity: 0; + transform: translateX(-8px); + } + to { + opacity: 1; + transform: translateX(0); + } +} - .task-actions .btn.small { - padding: 6px 8px; - } -} \ No newline at end of file +@keyframes fadeUp { + from { + opacity: 0; + transform: translateY(24px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes float { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-8px); + } +} + +@keyframes fadeOut { + to { + opacity: 0; + transform: translateY(-8px); + } +} + +.animate-fade-up { + animation: fadeUp 0.6s var(--ease) both; +} +.animate-fade-up-1 { + animation: fadeUp 0.6s 0.1s var(--ease) both; +} +.animate-fade-up-2 { + animation: fadeUp 0.6s 0.2s var(--ease) both; +} +.animate-fade-up-3 { + animation: fadeUp 0.6s 0.3s var(--ease) both; +} +.animate-fade-up-4 { + animation: fadeUp 0.6s 0.4s var(--ease) both; +} + +/* ── Scrollbar ────────────────────────────────────────────── */ +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: var(--bg); +} +::-webkit-scrollbar-thumb { + background: var(--bg-3); + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--text-3); +} + +/* ── Responsive ───────────────────────────────────────────── */ +@media (max-width: 768px) { + .nav-links { + display: none; + } + .mobile-menu-btn { + display: flex; + } + + .hero-title { + font-size: 2.5rem; + } + + .task-input-row { + flex-direction: column; + } + + .priority-select { + width: 100% !important; + min-width: unset; + } + + .stats-bar { + grid-template-columns: repeat(3, 1fr); + } + + .filter-bar { + gap: 6px; + } + + .card-header, + .card-body { + padding-left: 20px; + padding-right: 20px; + } + + .task-actions { + opacity: 1; + } + + .footer-inner { + flex-direction: column; + text-align: center; + } +} + +@media (max-width: 480px) { + .stats-bar { + grid-template-columns: 1fr; + gap: 0; + } + .stat-item { + padding: 16px; + } +} diff --git a/templates/about.html b/templates/about.html index 849e2e90..10bedc8e 100644 --- a/templates/about.html +++ b/templates/about.html @@ -1,92 +1,248 @@ -{% extends 'base.html' %} - -{% block title %}About - Task Master{% endblock %} - -{% block content %} -
-
-

About Task Master

-

A modern task management system

-
+{% extends 'base.html' %} {% block title %}About — Task Master{% endblock %} {% +block content %} +
+
+
Project
+

+ About TaskMaster +

+

+ A lightweight task management system built with Python and Flask. + Designed as a real learning project — not a tutorial clone — with a + clear roadmap toward production-ready features. +

+
-
-

What is Task Master?

-

- Task Master is a minimalist task management system built with Python Flask and modern web technologies. - It's designed to help you stay focused on what matters by providing a simple, distraction-free interface - for managing your daily tasks. -

-
+
-
-

Key Features

-
-
- - - -
-

Add & Manage Tasks

-

Quickly add tasks and manage them with ease

+ +
+ +
+
+ + + +
+
+
+ Rashed Alothman +
+
+ Backend Developer · Fedora Linux · Started Dec 2025 +
+
-
-
- - - -
-

Track Progress

-

Monitor completed and pending tasks at a glance

-
-
-
- - - -
-

REST API

-

Powerful API for third-party integrations

+
+ + +
+ +
+ {% for feature in [ ('REST API', 'Full CRUD with GET, POST, PATCH, + DELETE endpoints'), ('Priority System', 'Four levels: low, medium, + high, urgent — with sorting'), ('Filtering', 'Filter tasks by + completion status and priority'), ('Error Handling', 'Proper + validation and meaningful error responses'), ('Modular Structure', + 'Blueprints, Application Factory, separated layers'), ('SQLAlchemy + ORM', 'Clean model definitions with type-annotated columns'), ] %} +
+
+
+
+ {{ feature[0] }} +
+
+ {{ feature[1] }} +
+
+
+ {% endfor %}
-
-
-
-

Technology Stack

-
-
-

Flask

-

Backend Framework

-
-
-

SQLAlchemy

-

Database ORM

-
-
-

Modern CSS

-

Responsive Design

-
-
-

JavaScript

-

Vanilla JS

-
+ +
+ +
+
+ Python 3.14 +
+
+ Flask 3.1 +
+
+ SQLAlchemy 2.0 +
+
+ Flask-SQLAlchemy +
+
+ SQLite +
+
+ Vanilla JS +
+
+ Gunicorn +
+
+ Docker +
+
-
-
-

Version Info

-

- Task Master v1.0 • Built by Rashed Alothman • © 2025 -

-
+ +
+ +
+ {% for item in [ ('Q1 2026', 'User auth, Flask-Migrate, PostgreSQL, + full test suite', True), ('Q2 2026', 'Multi-user support, task + sharing, Docker deployment', False), ('Q3 2026', 'Calendar + integration, notifications, API docs', False), ('Q4 2026', + 'Production release, mobile app, analytics', False), ] %} +
+
+ {{ item[0] }} +
+
+ {{ item[1] }} +
+
+ {% endfor %} +
+
- +
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/add_users.html b/templates/add_users.html index a5f118ab..4eaffab2 100644 --- a/templates/add_users.html +++ b/templates/add_users.html @@ -1,47 +1,125 @@ -{% extends 'base.html' %} +{% extends 'base.html' %} {% block title %}Invite Team — Task Master{% endblock +%} {% block content %} +
+
+
+
Team
+

+ Invite a member +

+

+ Add someone to your task board. +

+
-{% block title %}Add Users{% endblock %} +
+
+
+ + +
-{% block content %} -
-
-

Invite Team

-

Add a new member to your task board.

-
+
+ + +
- - -
- - -
+
+ + +
-
- - -
+
+ + Cancel +
+ -
- - -
+
-
- - Cancel +
+
+ Dev Note +
+

+ Multi-user support is planned for Q2 2026. This form will be + wired to a real invitation system once auth is in place. +

+
+
- - - -
-

Dev Note

-

- This form POSTs data to your Flask backend. Check your terminal output to see the printed user details when you click "Send". -

-
- - -{% endblock %} \ No newline at end of file +
+{% endblock %} diff --git a/templates/base.html b/templates/base.html index 67ef600c..a9f64564 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,43 +1,115 @@ - + - - - - {% block title %}Task Management System{% endblock %} - - - - -
- - - - -
-
- {% block content %}{% endblock %} -
-
- - -
-

Task Management System © 2025

-
-
- - {% block scripts %}{% endblock %} - - \ No newline at end of file + + + + {% block title %}TMS — Task Master{% endblock %} + + + + + + + +
+ + + + + +
{% block content %}{% endblock %}
+ + +
+ +
+ + + + {% block scripts %}{% endblock %} + + diff --git a/templates/homepage.html b/templates/homepage.html index bdcd8efe..121978c8 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -1,306 +1,430 @@ -{% extends 'base.html' %} - -{% block title %}Dashboard - Task Master{% endblock %} - +{% extends 'base.html' %} {% block title %}Dashboard — Task Master{% endblock %} {% block content %} -
-
-

My Tasks

-

Stay organized and focused on what matters

-
- - -
-
-
0
-
Total Tasks
-
-
-
0
-
Pending
+
+ +
+

+ My Tasks +

+

+ Stay organized and focused on what matters. +

-
-
0
-
Completed
+ + +
+
+
0
+
Total
+
+
+
0
+
Pending
+
+
+
0
+
Done
+
-
- - -
- - -
- - -
- - -
-
- - - -

No tasks yet

-

Add a task above to get started!

+ + +
+
+ +
+ + +
+ + + +
+ + +
+
+ + + +
+ + + + +
+ + +
+ + +
+
-
-{% endblock %} - -{% block scripts %} +{% endblock %} {% block scripts %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 3532cd0d..00000000 --- a/templates/index.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends 'base.html' %} - - -{% block title %}To‑Do — Home{% endblock %} - - -{% block content %} -
-
-

Your Tasks

-

Add tasks, mark complete, or delete — no JS required.

-
- - -
- - -
- - -{% if tasks %} -
    -{% for task in tasks %} -
  • -
    -
    - -
    -
    {{ task.title }}
    -
    - - -
    -
    - -
    -
    -
  • -{% endfor %} -
-{% else %} -

You have no tasks yet — add one above!

-{% endif %} -
-{% endblock %} \ No newline at end of file diff --git a/templates/landing.html b/templates/landing.html index 4c298659..476c7323 100644 --- a/templates/landing.html +++ b/templates/landing.html @@ -1,71 +1,192 @@ -{% extends 'base.html' %} +{% extends 'base.html' %} {% block title %}Task Master — Organize. Achieve. +Succeed.{% endblock %} {% block content %} -{% block title %}Welcome - Task Master{% endblock %} + +
+
+
+
+ + + + Now in Active Development +
+
-{% block content %} -
-
-
- - - +

+ Manage tasks
with clarity
and purpose. +

+ +

+ A focused task management system built for developers who value + clean tools. No clutter, no distractions — just what you need to + ship. +

+ +
-

Task Master

-

Organize • Achieve • Succeed

-
+
-

- A minimalist task management system designed to help you focus on what matters most and accomplish your goals with clarity and purpose. -

+ +
+
+
+
+ +
+
- +
+
+
+ + + +
+
Instant CRUD
+
+ Add, complete, and delete tasks with zero page reloads. Full + REST API under the hood. +
+
- -
-

Why Choose Task Master?

- -
-
- - - -

Lightning Fast

-

Instant task management with real-time updates

-
+
+
+ + + +
+
Priority System
+
+ Four priority levels — low, medium, high, urgent — with + smart sorting and filtering. +
+
-
- - - -

Simple & Clean

-

Beautiful distraction-free interface

-
+
+
+ + + +
+
REST API
+
+ Every feature is backed by a clean JSON API. Filter by + status, sort by priority or date. +
+
+
-
- - - -

Secure & Private

-

Your data stays with you, always protected

-
+ +
+ +
+
+ Python 3.14 +
+
+ Flask 3.1 +
+
+ SQLAlchemy 2 +
+
+ SQLite / PostgreSQL +
+
+ Vanilla JS +
+
+ Docker-ready +
+
+ Kubernetes-ready +
+
+
+
-
-

Built with Flask • SQLAlchemy • Modern CSS

-
-
-
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/login.html b/templates/login.html index cae51f5c..2e86b51e 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,32 +1,175 @@ -{% extends 'base.html' %} +{% extends 'base.html' %} {% block title %}Sign In — Task Master{% endblock %} +{% block content %} +
+
+ +
+
+ + + + +
+

+ Welcome back +

+

+ Sign in to manage your tasks +

+
-{% block title %}Login - Task Master{% endblock %} + +
+ +
-{% block content %} -
-
-

Welcome Back

-

Sign in to manage your tasks

-
+
+
+ + +
- -
- - -
+
+ + +
-
- - -
+ +
+ +
- - +
+
+ Dev Note +
+

+ Authentication is on the roadmap. This form is a UI + placeholder — login logic will be added in Q1 2026 with + Flask-Login. +

+
+
-

- Don't have an account? Go back home -

+

+ Not ready to sign in? + Go back home +

+
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/views/__init__.py b/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/views/page_views.py b/views/page_views.py new file mode 100644 index 00000000..30f0ede1 --- /dev/null +++ b/views/page_views.py @@ -0,0 +1,33 @@ +# views/page_views.py +# Handles all HTML page rendering routes. +# These routes just render templates — no business logic here. + +from flask import Blueprint, render_template, request + +pages = Blueprint("pages", __name__) + + +@pages.route("/") +def landing(): + return render_template("landing.html") + + +@pages.route("/homepage") +def homepage(): + return render_template("homepage.html") + + +@pages.route("/login") +def login(): + return render_template("login.html") + + +@pages.route("/about") +def about(): + return render_template("about.html") + + +@pages.route("/homepage/AddUsersToAccount") +def add_users(): + # Placeholder — will require auth before this is wired up properly + return render_template("add_users.html")