diff --git a/Dockerfile b/Dockerfile index 7d08d00..5d0ac86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,18 @@ -# Use an official Python runtime as a base FROM python:3.11-slim-buster -# Set working directory -WORKDIR /app +# Install build dependencies (build-essential provides gcc and other tools) +RUN apt-get update && apt-get install -y build-essential -# Install system dependencies -RUN apt-get update && apt-get install -y \ - build-essential \ - libpq-dev \ - && rm -rf /var/lib/apt/lists/* +WORKDIR /rideaware_landing -# Copy requirements first to leverage Docker cache COPY requirements.txt . -# Install Python dependencies RUN pip install --no-cache-dir -r requirements.txt -# Copy application code COPY . . -# Environment variables -ENV FLASK_APP=app.py -ENV FLASK_ENV=production -ENV ENVIRONMENT=production +ENV FLASK_APP=server.py EXPOSE 5001 -# Use Gunicorn as production server -CMD ["gunicorn", "--bind", "0.0.0.0:5001", "--workers", "4", "--timeout", "120", "app:app"] \ No newline at end of file +CMD ["gunicorn", "--bind", "0.0.0.0:5001", "app:app"] diff --git a/app.py b/app.py index 85c05bd..8d6188e 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,7 @@ import os +import logging import smtplib from email.mime.text import MIMEText -from functools import wraps -from urllib.parse import urlparse, urljoin - from flask import ( Flask, render_template, @@ -13,222 +11,135 @@ from flask import ( flash, session, ) -from markupsafe import escape from dotenv import load_dotenv from werkzeug.security import check_password_hash - -from database import ( - get_connection, - init_db, - get_all_emails, - get_admin, - create_default_admin, -) +from functools import wraps # Import wraps +from database import get_connection, init_db, get_all_emails, get_admin, create_default_admin load_dotenv() - app = Flask(__name__) app.secret_key = os.getenv("SECRET_KEY") -base_url = os.getenv("BASE_URL", "").strip().strip("/") +base_url = os.getenv("BASE_URL") +# SMTP settings (for sending update emails) SMTP_SERVER = os.getenv("SMTP_SERVER") SMTP_PORT = int(os.getenv("SMTP_PORT", 465)) SMTP_USER = os.getenv("SMTP_USER") SMTP_PASSWORD = os.getenv("SMTP_PASSWORD") -SENDER_EMAIL = os.getenv("SENDER_EMAIL", SMTP_USER) +SENDER_EMAIL = os.getenv("SENDER_EMAIL", SMTP_USER) # Use SENDER_EMAIL +# Logging setup +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +# Initialize the database and create default admin user if necessary. init_db() create_default_admin() - +# Decorator for requiring login def login_required(f): - @wraps(f) + @wraps(f) # Use wraps to preserve function metadata def decorated_function(*args, **kwargs): if "username" not in session: - next_url = request.full_path if request.query_string else request.path - return redirect(url_for("login", next=next_url)) + return redirect(url_for("login")) return f(*args, **kwargs) return decorated_function -def get_dashboard_counts(): - """Return dict of counts: total subscribers, total newsletters, sent today.""" - counts = {"total_subscribers": 0, "total_newsletters": 0, "sent_today": 0} +def send_update_email(subject, body, email): + """Sends email, returns True on success, False on failure.""" try: - conn = get_connection() - cur = conn.cursor() - cur.execute("SELECT COUNT(*) FROM subscribers") - counts["total_subscribers"] = cur.fetchone()[0] or 0 + server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) + server.set_debuglevel(False) # Keep debug level at False for production + server.login(SMTP_USER, SMTP_PASSWORD) - cur.execute("SELECT COUNT(*) FROM newsletters") - counts["total_newsletters"] = cur.fetchone()[0] or 0 - - cur.execute( - """ - SELECT COUNT(*) - FROM newsletters - WHERE sent_at::date = CURRENT_DATE - """ - ) - counts["sent_today"] = cur.fetchone()[0] or 0 - - cur.close() - conn.close() - except Exception: - pass - return counts - - -def is_safe_url(target: str) -> bool: - if not target: - return False - ref = urlparse(request.host_url) - test = urlparse(urljoin(request.host_url, target)) - return test.scheme in ("http", "https") and ref.netloc == test.netloc - - -def send_update_email(subject: str, body_html: str, email: str) -> bool: - """Send a single HTML email with retries.""" - max_retries = 3 - retry_count = 0 - - unsub_link = "" - if base_url: unsub_link = f"https://{base_url}/unsubscribe?email={email}" - - if unsub_link: custom_body = ( - f"{body_html}" - f"

" - f"If you ever wish to unsubscribe, please click " - f"here." + f"{body}

" + f"If you ever wish to unsubscribe, please click here" ) - else: - custom_body = body_html - while retry_count < max_retries: - try: - server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) - server.set_debuglevel(0) - server.login(SMTP_USER, SMTP_PASSWORD) + msg = MIMEText(custom_body, "html", "utf-8") + msg["Subject"] = subject + msg["From"] = SENDER_EMAIL # Use sender email + msg["To"] = email - msg = MIMEText(custom_body, "html", "utf-8") - msg["Subject"] = subject - msg["From"] = SENDER_EMAIL - msg["To"] = email + server.sendmail(SENDER_EMAIL, email, msg.as_string()) # Use sender email - server.sendmail(SENDER_EMAIL, [email], msg.as_string()) - server.quit() - return True - except Exception: - retry_count += 1 - if retry_count >= max_retries: - break - import time - - time.sleep(1.0) - - return False + server.quit() + logger.info(f"Update email sent to: {email}") + return True + except Exception as e: + logger.error(f"Failed to send email to {email}: {e}") + return False -def process_send_update_email(subject: str, body_html: str) -> str: - """Send update email to all subscribers and log newsletter content.""" +def process_send_update_email(subject, body): + """Helper function to send an update email to all subscribers.""" + subscribers = get_all_emails() + if not subscribers: + return "No subscribers found." try: - subscribers = get_all_emails() - if not subscribers: - return "No subscribers found." - - failures = [] for email in subscribers: - if not send_update_email(subject, body_html, email): - failures.append(email) + if not send_update_email(subject, body, email): + return f"Failed to send to {email}" # Specific failure message - try: - conn = get_connection() - cursor = conn.cursor() - cursor.execute( - "INSERT INTO newsletters (subject, body) VALUES (%s, %s)", - (subject, body_html), - ) - conn.commit() - cursor.close() - conn.close() - except Exception: - pass + # Log newsletter content for audit purposes + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "INSERT INTO newsletters (subject, body) VALUES (%s, %s)", (subject, body) + ) + conn.commit() + cursor.close() + conn.close() - if failures: - return f"Sent with failures: {len(failures)} recipients failed." return "Email has been sent to all subscribers." except Exception as e: + logger.exception("Error processing sending updates") return f"Failed to send email: {e}" -@app.route("/", methods=["GET"]) +@app.route("/") @login_required def index(): - """Dashboard: list subscriber emails and show widgets.""" - emails = [] - try: - emails = get_all_emails() - except Exception: - flash("Could not load subscribers right now.", "danger") - - counts = get_dashboard_counts() - return render_template("admin_index.html", emails=emails, counts=counts) - + """Displays all subscriber emails""" + emails = get_all_emails() + return render_template("admin_index.html", emails=emails) @app.route("/send_update", methods=["GET", "POST"]) @login_required def send_update(): + """Display a form to send an update email; process submission on POST.""" if request.method == "POST": - subject = (request.form.get("subject") or "").strip() - body_html = request.form.get("body") or "" - - if not subject or not body_html: - flash("Subject and body are required", "danger") - return redirect(url_for("send_update")) - - result_message = process_send_update_email(subject, body_html) - flash(escape(result_message)) + subject = request.form["subject"] + body = request.form["body"] + result_message = process_send_update_email(subject, body) + flash(result_message) return redirect(url_for("send_update")) - return render_template("send_update.html") @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": - username = (request.form.get("username") or "").strip() - password = (request.form.get("password") or "").strip() - - if not username or not password: - flash("Username and password are required", "danger") - return redirect(url_for("login")) - + username = request.form.get("username") + password = request.form.get("password") admin = get_admin(username) if admin and check_password_hash(admin[1], password): session["username"] = username - session.permanent = True - app.config["SESSION_COOKIE_HTTPONLY"] = True - app.config["SESSION_COOKIE_SECURE"] = True - app.config["SESSION_COOKIE_SAMESITE"] = "Lax" - - next_url = request.args.get("next") - if next_url and is_safe_url(next_url): - flash("Logged in successfully", "success") - return redirect(next_url) - flash("Logged in successfully", "success") return redirect(url_for("index")) else: flash("Invalid username or password", "danger") return redirect(url_for("login")) - return render_template("login.html") -@app.route("/logout", methods=["GET"]) +@app.route("/logout") def logout(): session.pop("username", None) flash("Logged out successfully", "success") @@ -236,9 +147,4 @@ def logout(): if __name__ == "__main__": - is_prod = os.getenv("ENVIRONMENT", "development").lower() == "production" - app.config["PREFERRED_URL_SCHEME"] = "https" if is_prod else "http" - if is_prod: - app.run(host="0.0.0.0", port=5001, debug=False, use_reloader=False) - else: - app.run(host="0.0.0.0", port=5001, debug=True, use_reloader=True) \ No newline at end of file + app.run(port=5001, debug=True) diff --git a/database.py b/database.py index e5a85fb..b90e0ef 100644 --- a/database.py +++ b/database.py @@ -1,53 +1,54 @@ import os +import logging import psycopg2 -from psycopg2 import IntegrityError, pool, OperationalError +from psycopg2 import IntegrityError from dotenv import load_dotenv from werkzeug.security import generate_password_hash load_dotenv() -try: - DB_MIN_CONN = int(os.getenv("DB_MIN_CONN", 1)) - DB_MAX_CONN = int(os.getenv("DB_MAX_CONN", 10)) - - conn_pool = pool.ThreadedConnectionPool( - minconn=DB_MIN_CONN, - maxconn=DB_MAX_CONN, - host=os.getenv("PG_HOST"), - port=os.getenv("PG_PORT"), - dbname=os.getenv("PG_DATABASE"), - user=os.getenv("PG_USER"), - password=os.getenv("PG_PASSWORD"), - connect_timeout=10, - ) -except OperationalError: - raise -except Exception: - raise +# Logging setup +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) def get_connection(): - """Get a connection from the connection pool.""" - return conn_pool.getconn() + """Return a new connection to the PostgreSQL database.""" + try: + conn = psycopg2.connect( + host=os.getenv("PG_HOST"), + port=os.getenv("PG_PORT"), + dbname=os.getenv("PG_DATABASE"), + user=os.getenv("PG_USER"), + password=os.getenv("PG_PASSWORD"), + connect_timeout=10, + ) + return conn + except Exception as e: + logger.error(f"Database connection error: {e}") + raise def init_db(): - """Initialize database tables with connection pool.""" + """Initialize the database tables.""" conn = None - cursor = None try: conn = get_connection() cursor = conn.cursor() + # Create subscribers table (if not exists) cursor.execute( """ CREATE TABLE IF NOT EXISTS subscribers ( id SERIAL PRIMARY KEY, email TEXT UNIQUE NOT NULL ) - """ + """ ) + # Create admin_users table (if not exists) cursor.execute( """ CREATE TABLE IF NOT EXISTS admin_users ( @@ -55,96 +56,94 @@ def init_db(): username TEXT UNIQUE NOT NULL, password TEXT NOT NULL ) - """ + """ ) + # Newsletter storage cursor.execute( """ - CREATE TABLE IF NOT EXISTS newsletters ( - id SERIAL PRIMARY KEY, - subject TEXT NOT NULL, - body TEXT NOT NULL, - sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP - ) - """ + CREATE TABLE IF NOT EXISTS newsletters ( + id SERIAL PRIMARY KEY, + subject TEXT NOT NULL, + body TEXT NOT NULL, + sent_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ) + """ ) conn.commit() - except Exception: + logger.info("Database initialized successfully.") + except Exception as e: + logger.error(f"Database initialization error: {e}") if conn: - conn.rollback() + conn.rollback() # Rollback if there's an error + raise finally: - if cursor: - cursor.close() if conn: - conn_pool.putconn(conn) + cursor.close() + conn.close() def get_all_emails(): """Return a list of all subscriber emails.""" - conn = None - cursor = None try: conn = get_connection() cursor = conn.cursor() cursor.execute("SELECT email FROM subscribers") results = cursor.fetchall() - return [row[0] for row in results] - except Exception: + emails = [row[0] for row in results] + logger.debug(f"Retrieved emails: {emails}") + return emails + except Exception as e: + logger.error(f"Error retrieving emails: {e}") return [] finally: - if cursor: - cursor.close() if conn: - conn_pool.putconn(conn) + cursor.close() + conn.close() def add_email(email): """Insert an email into the subscribers table.""" conn = None - cursor = None try: conn = get_connection() cursor = conn.cursor() cursor.execute("INSERT INTO subscribers (email) VALUES (%s)", (email,)) conn.commit() + logger.info(f"Email {email} added successfully.") return True except IntegrityError: - if conn: - conn.rollback() + logger.warning(f"Attempted to add duplicate email: {email}") return False - except Exception: - if conn: - conn.rollback() + except Exception as e: + logger.error(f"Error adding email {email}: {e}") return False finally: - if cursor: - cursor.close() if conn: - conn_pool.putconn(conn) + cursor.close() + conn.close() def remove_email(email): """Remove an email from the subscribers table.""" conn = None - cursor = None try: conn = get_connection() cursor = conn.cursor() cursor.execute("DELETE FROM subscribers WHERE email = %s", (email,)) rowcount = cursor.rowcount conn.commit() + logger.info(f"Email {email} removed successfully.") return rowcount > 0 - except Exception: - if conn: - conn.rollback() + except Exception as e: + logger.error(f"Error removing email {email}: {e}") return False finally: - if cursor: - cursor.close() if conn: - conn_pool.putconn(conn) + cursor.close() + conn.close() def get_admin(username): @@ -152,7 +151,6 @@ def get_admin(username): Returns a tuple (username, password_hash) if found, otherwise None. """ conn = None - cursor = None try: conn = get_connection() cursor = conn.cursor() @@ -160,14 +158,15 @@ def get_admin(username): "SELECT username, password FROM admin_users WHERE username = %s", (username,), ) - return cursor.fetchone() - except Exception: + result = cursor.fetchone() + return result # (username, password_hash) + except Exception as e: + logger.error(f"Error retrieving admin: {e}") return None finally: - if cursor: - cursor.close() if conn: - conn_pool.putconn(conn) + cursor.close() + conn.close() def create_default_admin(): @@ -175,60 +174,30 @@ def create_default_admin(): default_username = os.getenv("ADMIN_USERNAME", "admin") default_password = os.getenv("ADMIN_PASSWORD", "changeme") hashed_password = generate_password_hash(default_password, method="pbkdf2:sha256") - conn = None - cursor = None try: conn = get_connection() cursor = conn.cursor() + # Check if the admin already exists cursor.execute( "SELECT id FROM admin_users WHERE username = %s", (default_username,) ) - exists = cursor.fetchone() - if exists is None: + if cursor.fetchone() is None: cursor.execute( "INSERT INTO admin_users (username, password) VALUES (%s, %s)", (default_username, hashed_password), ) conn.commit() - except Exception: + logger.info("Default admin created successfully") + else: + logger.info("Default admin already exists") + except Exception as e: + logger.error(f"Error creating default admin: {e}") if conn: conn.rollback() finally: - if cursor: - cursor.close() if conn: - conn_pool.putconn(conn) + cursor.close() + conn.close() - -def close_pool(): - """Close the database connection pool.""" - try: - conn_pool.closeall() - except Exception: - pass - - -class DatabaseContext: - """Optional context manager for manual transactions.""" - def __init__(self): - self.conn = None - self.cursor = None - - def __enter__(self): - self.conn = get_connection() - self.cursor = self.conn.cursor() - return self.cursor - - def __exit__(self, exc_type, exc_val, exc_tb): - try: - if exc_type: - self.conn.rollback() - else: - self.conn.commit() - finally: - if self.cursor: - self.cursor.close() - if self.conn: - conn_pool.putconn(self.conn) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3d2a2e9..26fcaf5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,3 @@ flask python-dotenv Werkzeug psycopg2-binary -psycopg2-pool -python-decouple -markupsafe \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index 9dca122..b9c3f88 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,306 +1,55 @@ -:root { - --primary: #2563eb; - --primary-hover: #1d4ed8; - --bg: #f5f7fb; - --bg-grad-1: #f8fbff; - --bg-grad-2: #eef3fb; - --surface: #ffffff; - --text: #0f172a; - --muted: #64748b; - --border: #e5e7eb; - --ring: rgba(37, 99, 235, 0.25); - --radius: 14px; - --shadow-1: 0 8px 24px rgba(15, 23, 42, 0.08); - --shadow-2: 0 14px 38px rgba(15, 23, 42, 0.12); -} - -*, -*::before, -*::after { - box-sizing: border-box; -} -html, body { - margin: 0; - padding: 0; -} body { - font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; - color: var(--text); - background: radial-gradient(1000px 650px at 0% 0%, var(--bg-grad-1), transparent 60%), - radial-gradient(900px 600px at 100% 0%, var(--bg-grad-2), transparent 55%), - var(--bg); - min-height: 100vh; -} - -.navbar { - position: sticky; - top: 0; - z-index: 1000; - background: rgba(255, 255, 255, 0.8); - backdrop-filter: blur(10px); - border-bottom: 1px solid rgba(15, 23, 42, 0.06); -} -.navbar-content { - max-width: 1100px; - margin: 0 auto; - padding: 14px 20px; - display: flex; - align-items: center; - justify-content: space-between; -} -.brand { - color: var(--text); - font-weight: 700; - text-decoration: none; - letter-spacing: 0.2px; -} -.navbar-links a { - color: var(--muted); - text-decoration: none; - margin-left: 14px; - padding: 8px 12px; - border-radius: 10px; - transition: all 0.2s ease; -} -.navbar-links a:hover { - color: var(--text); - background: rgba(15, 23, 42, 0.05); -} -.navbar-links .logout { - color: #b91c1c; -} -.navbar-links .logout:hover { - background: rgba(185, 28, 28, 0.08); -} - -.container { - max-width: 1100px; - margin: 28px auto 48px; - padding: 0 20px; - padding-bottom: 64px; -} - -.footer { - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; - border-top: 1px solid rgba(15, 23, 42, 0.06); - background: rgba(255, 255, 255, 0.92); - backdrop-filter: blur(8px); -} - -.footer-inner { - max-width: 1100px; - margin: 0 auto; - padding: 14px 20px; - color: var(--muted); - font-size: 14px; -} - -.page-header { - margin-bottom: 18px; - display: flex; - align-items: end; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; -} -.page-title { - margin: 0 0 4px 0; - font-size: 26px; - font-weight: 700; -} -.page-subtitle { - margin: 0; - color: var(--muted); - font-size: 14px; -} -.page-actions { - display: flex; - gap: 10px; -} - -.card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - box-shadow: var(--shadow-1); - padding: 20px; -} -.empty-state { - text-align: center; - color: var(--muted); -} - -.widgets { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px; - margin-bottom: 18px; -} -.widget-card { - background: linear-gradient(180deg, #fff, #fafcff); - border: 1px solid var(--border); - border-radius: var(--radius); - box-shadow: var(--shadow-1); - padding: 18px; -} -.widget-label { - color: var(--muted); - font-weight: 600; - font-size: 13px; - margin-bottom: 8px; -} -.widget-value { - font-size: 28px; - font-weight: 800; - letter-spacing: 0.3px; - color: var(--text); -} - -.table-wrap { - overflow: hidden; - border-radius: 12px; - border: 1px solid var(--border); -} -.table { - width: 100%; - border-collapse: collapse; - background: transparent; -} -.table thead th { - text-align: left; - font-weight: 600; - color: var(--muted); - font-size: 13px; - letter-spacing: 0.3px; - background: #f9fafb; - padding: 12px 14px; - border-bottom: 1px solid var(--border); -} -.table tbody td { - padding: 14px; - border-bottom: 1px solid #f1f5f9; -} -.table tbody tr:hover td { - background: #f9fbff; - transition: background 0.15s ease; -} - -.form { - display: grid; - gap: 16px; -} -.form-group { - display: grid; - gap: 8px; -} -.form-group label { - font-weight: 600; - color: #334155; - font-size: 14px; -} -.form-group input, -.form-group textarea { - width: 100%; - color: var(--text); - background: #ffffff; - border: 1px solid var(--border); - border-radius: 12px; - padding: 12px 14px; - font-size: 15px; - transition: box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease; -} -.form-group textarea { - resize: vertical; - min-height: 160px; -} -.form-group input:focus, -.form-group textarea:focus { - outline: none; - border-color: #a7c2ff; - box-shadow: 0 0 0 4px var(--ring); - background: #ffffff; -} -.form-actions { - display: flex; - gap: 10px; - justify-content: flex-end; -} - -.button { - appearance: none; - border: 1px solid var(--border); - background: #ffffff; - color: var(--text); - border-radius: 12px; - padding: 10px 14px; - cursor: pointer; - font-weight: 600; - transition: transform 0.05s ease, background 0.2s ease, border 0.2s ease; -} -.button:hover { - background: #f5f7fb; -} -.button:active { - transform: translateY(1px); -} -.button-primary { - color: #ffffff; - background: linear-gradient(180deg, #3b82f6, var(--primary)); - border-color: rgba(37, 99, 235, 0.4); -} -.button-primary:hover { - background: linear-gradient(180deg, #2f74ed, var(--primary-hover)); -} -.button-secondary { - background: #ffffff; -} - -.flash-stack { - display: grid; - gap: 10px; - margin-bottom: 18px; -} -.flash { - border-radius: 12px; - padding: 12px 14px; - font-weight: 600; - border: 1px solid var(--border); - background: #ffffff; -} -.flash-success { - border-color: #a7f3d0; - background: #ecfdf5; - color: #065f46; -} -.flash-danger, -.flash-error { - border-color: #fecaca; - background: #fef2f2; - color: #991b1b; -} -.flash-warning { - border-color: #fde68a; - background: #fffbeb; - color: #92400e; -} - -.auth-wrapper { - display: grid; - place-items: center; - min-height: calc(100vh - 120px); - padding-top: 40px; -} -.auth-card { - max-width: 420px; - width: 100%; -} - -@media (max-width: 900px) { - .widgets { - grid-template-columns: 1fr; + font-family: Arial, sans-serif; + padding: 20px; } -} \ No newline at end of file + + table { + border-collapse: collapse; + width: 100%; + } + + th, + td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; + } + + th { + background-color: #f2f2f2; + } + + a { + margin-right: 10px; + } + + form { + max-width: 600px; + margin: 0 auto; + } + + label { + display: block; + margin-top: 15px; + } + + input[type="text"], + input[type="password"], + textarea { + width: 100%; + padding: 8px; + } + + button { + margin-top: 15px; + padding: 10px 20px; + } + + .flash { + background-color: #f8d7da; + color: #721c24; + padding: 10px; + margin-bottom: 10px; + text-align: center; + } + \ No newline at end of file diff --git a/templates/admin_index.html b/templates/admin_index.html index bf57258..c4620af 100644 --- a/templates/admin_index.html +++ b/templates/admin_index.html @@ -1,53 +1,44 @@ -{% extends "base.html" %} -{% block title %}Dashboard{% endblock %} -{% block content %} - + + + + + + Admin Center - Subscribers + + + -
-
-
Total Subscribers
-
{{ counts.total_subscribers }}
-
-
-
Newsletters Sent
-
{{ counts.total_newsletters }}
-
-
-
Sent Today
-
{{ counts.sent_today }}
-
-
+

Subscribers

+

+ Send Update Email| + Logout +

- {% if emails %} -
-
- - - - - - - - {% for email in emails %} - - - - {% endfor %} - + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + + {% if emails %} +
Email Address
{{ email }}
+ + + + + + + {% for email in emails %} + + + + {% endfor %} +
Email Address
{{ email }}
-
-
- {% else %} -
-

No subscribers found.

-
- {% endif %} -{% endblock %} \ No newline at end of file + {% else %} +

No subscribers found.

+ {% endif %} + + diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index 13df2b0..0000000 --- a/templates/base.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - {% block title %}Admin{% endblock %} - - - - - - - -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for category, message in messages %} -
{{ message }}
- {% endfor %} -
- {% endif %} - {% endwith %} - - {% block content %}{% endblock %} -
- - - - \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index f70fb06..97fefdd 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,34 +1,29 @@ -{% extends "base.html" %} -{% block title %}Admin Login{% endblock %} -{% block content %} -
-
-

Welcome back

-

Sign in to manage your subscribers

+ + -
-
- - -
-
- - -
- -
-
-
-{% endblock %} \ No newline at end of file + + + + Admin Login + + + + +

Admin Login

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} +
+ + + + + +
+ + + diff --git a/templates/send_update.html b/templates/send_update.html index e3c16b5..fccd7ff 100644 --- a/templates/send_update.html +++ b/templates/send_update.html @@ -1,32 +1,37 @@ -{% extends "base.html" %} -{% block title %}Send Update{% endblock %} -{% block content %} - + + -
-
-
- - -
+ + + + Admin Center - Send Update + + -
- - -
+ +

Send Update Email

+

+ Back to Subscribers List | + Logout +

+ {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} -
- -
-
-
-{% endblock %} \ No newline at end of file +
+ + + + + + + +
+ + + +