feat(newsletter): Add subscription confirmation email functionality
- Implemented `send_subscription_confirmation` function to send a confirmation email upon subscription. - Added a default HTML template for the confirmation email in settings. - Updated the newsletter management page to handle subscription and unsubscription actions. - Created new templates for embedding contact and newsletter forms. - Added styles for the new templates and unified CSS styles across the application. - Updated tests to reflect changes in the newsletter management API endpoints.
This commit is contained in:
48
README.md
48
README.md
@@ -61,11 +61,12 @@ Access the admin interface at `http://127.0.0.1:5002/auth/login` using the confi
|
||||
- `GET /api/contact`: retrieves contact form submissions (admin only, requires authentication). Supports pagination (`page`, `per_page`), filtering (`email`, `date_from`, `date_to`), and sorting (`sort_by`, `sort_order`).
|
||||
- `GET /api/contact/<id>`: retrieves a specific contact submission by ID (admin only).
|
||||
- `DELETE /api/contact/<id>`: deletes a contact submission by ID (admin only).
|
||||
- `POST /api/newsletter`: subscribes an address and optional metadata to the newsletter list.
|
||||
- `POST /api/newsletter`: subscribes an address and optional metadata to the newsletter list. Sends a confirmation email if SMTP is configured.
|
||||
- `DELETE /api/newsletter`: unsubscribes an email address from the newsletter list.
|
||||
- `PUT /api/newsletter`: updates a subscriber's email address (requires `old_email` and `new_email`).
|
||||
- `GET /api/newsletter/manage`: displays HTML form for newsletter subscription management.
|
||||
- `POST /api/newsletter/manage`: processes subscription management actions (subscribe, unsubscribe, update).
|
||||
- `GET /newsletter/unsubscribe/confirmation`: displays unsubscription confirmation page.
|
||||
- `GET /health`: lightweight database connectivity check used for container health monitoring.
|
||||
- `GET /metrics`: Prometheus-compatible metrics endpoint (requires `ENABLE_REQUEST_LOGS` for detailed tracing).
|
||||
- `GET /admin/api/settings`: retrieves all application settings (admin only).
|
||||
@@ -77,6 +78,41 @@ Access the admin interface at `http://127.0.0.1:5002/auth/login` using the confi
|
||||
- `POST /admin/api/newsletters/<id>/send`: sends a newsletter to all subscribers (admin only).
|
||||
- `GET /admin/api/contact`: retrieves contact form submissions with pagination, filtering, and sorting (admin only).
|
||||
- `DELETE /admin/api/contact/<id>`: deletes a contact submission by ID (admin only).
|
||||
- `GET /embed/contact`: serves an HTML page with a contact form that can be embedded in an iframe on external sites.
|
||||
- `GET /embed/newsletter`: serves an HTML page with a newsletter subscription form that can be embedded in an iframe on external sites.
|
||||
|
||||
## Embeddable Forms
|
||||
|
||||
The application provides embeddable contact and newsletter subscription forms that can be integrated into other websites via iframe.
|
||||
|
||||
- `GET /embed/contact`: serves an HTML page with a contact form that can be embedded in an iframe on external sites.
|
||||
- `GET /embed/newsletter`: serves an HTML page with a newsletter subscription form that can be embedded in an iframe on external sites.
|
||||
|
||||
To embed the contact form on another website, use the following HTML code:
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://your-server-domain/embed/contact"
|
||||
width="600"
|
||||
height="400"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
```
|
||||
|
||||
To embed the newsletter subscription form:
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://your-server-domain/embed/newsletter"
|
||||
width="600"
|
||||
height="300"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
```
|
||||
|
||||
Replace `https://your-server-domain` with your actual server URL. The iframe codes are available in the admin settings page for easy copying.
|
||||
|
||||
## Running With Docker
|
||||
|
||||
@@ -203,3 +239,13 @@ SMTP integration tests are skipped unless `RUN_SMTP_INTEGRATION_TEST=1` and vali
|
||||
- When the default branch (`main`) runs and registry secrets (`REGISTRY_URL`, `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`) are configured in Gitea, the workflow logs in and pushes both `latest` and commit-specific image tags.
|
||||
- Import or mirror the required reusable actions (`actions/checkout`, `actions/setup-python`, and the Docker actions) into your Gitea instance so that the workflow can resolve them.
|
||||
- For production use, deploy the container behind a load balancer or reverse proxy and supply the appropriate environment variables.
|
||||
|
||||
## Email Templates
|
||||
|
||||
The application supports customizable email templates for newsletter confirmations. The confirmation email template can be edited through the admin settings page under the dynamic settings management. The template uses HTML format and should include an unsubscribe link for compliance with email regulations.
|
||||
|
||||
Default confirmation email includes:
|
||||
|
||||
- Welcome message
|
||||
- Unsubscribe instructions with link to `/newsletter/manage`
|
||||
- Customizable HTML content
|
||||
|
||||
1
iframe_contact.html
Normal file
1
iframe_contact.html
Normal file
@@ -0,0 +1 @@
|
||||
<iframe src="http://your-server-domain/embed/contact" width="600" height="400" frameborder="0" allowfullscreen></iframe>
|
||||
1
iframe_newsletter.html
Normal file
1
iframe_newsletter.html
Normal file
@@ -0,0 +1 @@
|
||||
<iframe src="http://your-server-domain/embed/newsletter" width="600" height="300" frameborder="0" allowfullscreen></iframe>
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from . import admin, auth, contact, monitoring, newsletter
|
||||
from . import admin, auth, contact, embed, monitoring, newsletter
|
||||
|
||||
|
||||
def register_blueprints(app: Flask) -> None:
|
||||
@@ -13,3 +13,4 @@ def register_blueprints(app: Flask) -> None:
|
||||
app.register_blueprint(monitoring.bp)
|
||||
app.register_blueprint(auth.bp)
|
||||
app.register_blueprint(admin.bp)
|
||||
app.register_blueprint(embed.bp)
|
||||
|
||||
@@ -82,6 +82,13 @@ def submissions():
|
||||
return render_template("admin_submissions.html")
|
||||
|
||||
|
||||
@bp.route("/embeds")
|
||||
@auth.login_required
|
||||
def embeds():
|
||||
"""Display embeddable forms management page."""
|
||||
return render_template("admin_embeds.html")
|
||||
|
||||
|
||||
@bp.route("/api/settings", methods=["GET"])
|
||||
@auth.login_required
|
||||
def get_settings_api():
|
||||
|
||||
26
server/routes/embed.py
Normal file
26
server/routes/embed.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Embeddable forms routes."""
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, render_template, send_from_directory
|
||||
import os
|
||||
|
||||
bp = Blueprint("embed", __name__)
|
||||
|
||||
|
||||
@bp.route("/embed/contact", methods=["GET"])
|
||||
def contact_form():
|
||||
"""Serve the embeddable contact form."""
|
||||
return render_template("embed_contact.html")
|
||||
|
||||
|
||||
@bp.route("/embed/newsletter", methods=["GET"])
|
||||
def newsletter_form():
|
||||
"""Serve the embeddable newsletter subscription form."""
|
||||
return render_template("embed_newsletter.html")
|
||||
|
||||
|
||||
@bp.route("/static/css/styles.css", methods=["GET"])
|
||||
def serve_css():
|
||||
"""Serve the unified CSS file."""
|
||||
static_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'static')
|
||||
return send_from_directory(static_dir, 'css/styles.css')
|
||||
@@ -7,10 +7,10 @@ from flask import Blueprint, jsonify, request, render_template
|
||||
|
||||
from ..services import newsletter
|
||||
|
||||
bp = Blueprint("newsletter", __name__, url_prefix="/api")
|
||||
bp = Blueprint("newsletter", __name__)
|
||||
|
||||
|
||||
@bp.route("/newsletter", methods=["POST"])
|
||||
@bp.route("/api/newsletter", methods=["POST"])
|
||||
def subscribe():
|
||||
payload = request.form or request.get_json(silent=True) or {}
|
||||
email = (payload.get("email") or "").strip()
|
||||
@@ -29,10 +29,16 @@ def subscribe():
|
||||
return jsonify({"status": "error", "message": "Email is already subscribed."}), 409
|
||||
|
||||
logging.info("New newsletter subscription: %s", email)
|
||||
|
||||
# Send confirmation email
|
||||
email_sent = newsletter.send_subscription_confirmation(email)
|
||||
if not email_sent:
|
||||
logging.warning("Confirmation email not sent for %s", email)
|
||||
|
||||
return jsonify({"status": "ok", "message": "Subscribed successfully."}), 201
|
||||
|
||||
|
||||
@bp.route("/newsletter", methods=["DELETE"])
|
||||
@bp.route("/api/newsletter", methods=["DELETE"])
|
||||
def unsubscribe():
|
||||
payload = request.form or request.get_json(silent=True) or {}
|
||||
email = (payload.get("email") or "").strip()
|
||||
@@ -55,7 +61,7 @@ def unsubscribe():
|
||||
return jsonify({"status": "ok", "message": "Unsubscribed successfully."}), 200
|
||||
|
||||
|
||||
@bp.route("/newsletter", methods=["PUT"])
|
||||
@bp.route("/api/newsletter", methods=["PUT"])
|
||||
def update_subscription():
|
||||
payload = request.form or request.get_json(silent=True) or {}
|
||||
old_email = (payload.get("old_email") or "").strip()
|
||||
@@ -104,8 +110,7 @@ def manage_subscription():
|
||||
elif action == "unsubscribe":
|
||||
deleted = newsletter.unsubscribe(email)
|
||||
if deleted:
|
||||
message = "Successfully unsubscribed from newsletter."
|
||||
message_type = "success"
|
||||
return render_template("unsubscribe_confirmation.html")
|
||||
else:
|
||||
message = "This email is not currently subscribed."
|
||||
message_type = "info"
|
||||
@@ -131,3 +136,9 @@ def manage_subscription():
|
||||
message_type = "error"
|
||||
|
||||
return render_template("newsletter_manage.html", message=message, message_type=message_type)
|
||||
|
||||
|
||||
@bp.route("/newsletter/unsubscribe/confirmation", methods=["GET"])
|
||||
def unsubscribe_confirmation():
|
||||
"""Display newsletter unsubscription confirmation page."""
|
||||
return render_template("unsubscribe_confirmation.html")
|
||||
|
||||
@@ -28,6 +28,61 @@ def update_email(old_email: str, new_email: str) -> bool:
|
||||
return update_subscriber(old_email, new_email)
|
||||
|
||||
|
||||
def send_subscription_confirmation(email: str) -> bool:
|
||||
"""Send a confirmation email to the subscriber."""
|
||||
import logging
|
||||
from .. import settings
|
||||
|
||||
if not settings.SMTP_SETTINGS["host"]:
|
||||
logging.info("SMTP not configured; skipping confirmation email")
|
||||
return False
|
||||
|
||||
# Get template from settings or default
|
||||
template = getattr(settings, 'NEWSLETTER_CONFIRMATION_TEMPLATE', """
|
||||
<html>
|
||||
<body>
|
||||
<h2>Welcome to our Newsletter!</h2>
|
||||
<p>Thank you for subscribing to our newsletter. You're now part of our community and will receive updates on our latest news and offers.</p>
|
||||
<p>If you wish to unsubscribe at any time, you can do so by visiting our <a href="https://your-domain.com/newsletter/manage">subscription management page</a>.</p>
|
||||
<p>Best regards,<br>The Team</p>
|
||||
</body>
|
||||
</html>
|
||||
""").strip()
|
||||
|
||||
try:
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
sender = settings.SMTP_SETTINGS["sender"] or "noreply@example.com"
|
||||
|
||||
# Create message
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = "Newsletter Subscription Confirmation"
|
||||
msg['From'] = sender
|
||||
msg['To'] = email
|
||||
|
||||
# Add HTML content
|
||||
html_part = MIMEText(template, 'html')
|
||||
msg.attach(html_part)
|
||||
|
||||
# Send email
|
||||
with smtplib.SMTP(settings.SMTP_SETTINGS["host"], settings.SMTP_SETTINGS["port"], timeout=15) as server:
|
||||
if settings.SMTP_SETTINGS["use_tls"]:
|
||||
server.starttls()
|
||||
if settings.SMTP_SETTINGS["username"]:
|
||||
server.login(
|
||||
settings.SMTP_SETTINGS["username"], settings.SMTP_SETTINGS["password"] or "")
|
||||
server.send_message(msg)
|
||||
|
||||
logging.info("Confirmation email sent to %s", email)
|
||||
return True
|
||||
|
||||
except Exception as exc:
|
||||
logging.exception("Failed to send confirmation email to %s: %s", email, exc)
|
||||
return False
|
||||
|
||||
|
||||
def send_newsletter_to_subscribers(subject: str, content: str, emails: list[str], sender_name: str | None = None) -> int:
|
||||
"""Send newsletter to list of email addresses. Returns count of successful sends."""
|
||||
import logging
|
||||
|
||||
@@ -63,3 +63,14 @@ if not SMTP_SETTINGS["sender"] and SMTP_SETTINGS["username"]:
|
||||
|
||||
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
|
||||
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin")
|
||||
|
||||
NEWSLETTER_CONFIRMATION_TEMPLATE = os.getenv("NEWSLETTER_CONFIRMATION_TEMPLATE", """
|
||||
<html>
|
||||
<body>
|
||||
<h2>Welcome to our Newsletter!</h2>
|
||||
<p>Thank you for subscribing to our newsletter. You're now part of our community and will receive updates on our latest news and offers.</p>
|
||||
<p>If you wish to unsubscribe at any time, you can do so by visiting our <a href="https://your-domain.com/newsletter/manage">subscription management page</a>.</p>
|
||||
<p>Best regards,<br>The Team</p>
|
||||
</body>
|
||||
</html>
|
||||
""").strip()
|
||||
|
||||
335
static/css/styles.css
Normal file
335
static/css/styles.css
Normal file
@@ -0,0 +1,335 @@
|
||||
/* Unified CSS Styles */
|
||||
|
||||
/* Base styles */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #555;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.form-container,
|
||||
.contact-form,
|
||||
.confirmation-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group,
|
||||
.setting {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
button,
|
||||
.btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
/* Message styles */
|
||||
.message {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.message.info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
/* Admin dashboard */
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-card h2 {
|
||||
color: #555;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dashboard-card p {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: #e9ecef;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
min-width: 150px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
margin: 0;
|
||||
color: #495057;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.stat-card p {
|
||||
margin: 5px 0 0 0;
|
||||
color: #6c757d;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.logout {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.logout a {
|
||||
color: #dc3545;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logout a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Admin settings */
|
||||
.setting-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.setting strong {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.settings-management {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.setting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.setting-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.form-row .form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Newsletter manage */
|
||||
.form-section {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Navigation links */
|
||||
nav a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .current {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mb-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -4,91 +4,13 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin Dashboard</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.dashboard-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.dashboard-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.dashboard-card h2 {
|
||||
color: #555;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.dashboard-card p {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.dashboard-card a {
|
||||
display: inline-block;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.dashboard-card a:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.logout {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
.logout a {
|
||||
color: #dc3545;
|
||||
text-decoration: none;
|
||||
}
|
||||
.logout a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stat-card {
|
||||
background-color: #e9ecef;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
min-width: 150px;
|
||||
margin: 5px;
|
||||
}
|
||||
.stat-card h3 {
|
||||
margin: 0;
|
||||
color: #495057;
|
||||
font-size: 2em;
|
||||
}
|
||||
.stat-card p {
|
||||
margin: 5px 0 0 0;
|
||||
color: #6c757d;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -138,6 +60,15 @@
|
||||
<p>Create and send newsletters to your subscribers.</p>
|
||||
<a href="/admin/newsletter/create">Create Newsletter</a>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h2>Embeddable Forms</h2>
|
||||
<p>
|
||||
Instructions for embedding contact and newsletter forms on other
|
||||
websites.
|
||||
</p>
|
||||
<a href="/admin/embeds">View Embed Codes</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logout">
|
||||
|
||||
112
templates/admin_embeds.html
Normal file
112
templates/admin_embeds.html
Normal file
@@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Embeddable Forms</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
<style>
|
||||
.iframe-code {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
font-family: monospace;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="logout">
|
||||
<a
|
||||
href="/admin/"
|
||||
style="color: #007bff; text-decoration: none; margin-right: 20px"
|
||||
>Dashboard</a
|
||||
>
|
||||
<a
|
||||
href="/admin/submissions"
|
||||
style="color: #007bff; text-decoration: none; margin-right: 20px"
|
||||
>View Submissions</a
|
||||
>
|
||||
<a
|
||||
href="/admin/settings"
|
||||
style="color: #007bff; text-decoration: none; margin-right: 20px"
|
||||
>Settings</a
|
||||
>
|
||||
<a
|
||||
href="{{ url_for('auth.logout') }}"
|
||||
style="color: #007bff; text-decoration: none"
|
||||
>Logout</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<h1>Embeddable Forms</h1>
|
||||
|
||||
<div class="settings-management">
|
||||
<h2>Contact Form</h2>
|
||||
<p>
|
||||
Use the following HTML code to embed the contact form on other websites:
|
||||
</p>
|
||||
<textarea id="iframeCode" class="iframe-code" readonly>
|
||||
<iframe src="http://your-server-domain/embed/contact" width="600" height="400" frameborder="0" allowfullscreen></iframe>
|
||||
</textarea>
|
||||
<button class="btn btn-secondary" onclick="copyIframeCode()">
|
||||
Copy Contact Iframe
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-management">
|
||||
<h2>Newsletter Subscription Form</h2>
|
||||
<p>
|
||||
Use the following HTML code to embed the newsletter subscription form on
|
||||
other websites:
|
||||
</p>
|
||||
<textarea id="iframeNewsletterCode" class="iframe-code" readonly>
|
||||
<iframe src="http://your-server-domain/embed/newsletter" width="600" height="300" frameborder="0" allowfullscreen></iframe>
|
||||
</textarea>
|
||||
<button class="btn btn-secondary" onclick="copyNewsletterIframeCode()">
|
||||
Copy Newsletter Iframe
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-management">
|
||||
<p>
|
||||
Replace <code>http://your-server-domain</code> with your actual server
|
||||
domain and port (e.g., https://yourdomain.com).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyIframeCode() {
|
||||
const textarea = document.getElementById("iframeCode");
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
showMessage("Contact iframe code copied to clipboard!", "success");
|
||||
}
|
||||
|
||||
function copyNewsletterIframeCode() {
|
||||
const textarea = document.getElementById("iframeNewsletterCode");
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
showMessage("Newsletter iframe code copied to clipboard!", "success");
|
||||
}
|
||||
|
||||
function showMessage(text, type) {
|
||||
// Create message div if it doesn't exist
|
||||
let messageDiv = document.getElementById("message");
|
||||
if (!messageDiv) {
|
||||
messageDiv = document.createElement("div");
|
||||
messageDiv.id = "message";
|
||||
document.body.insertBefore(messageDiv, document.body.firstChild);
|
||||
}
|
||||
messageDiv.className = `message ${type}`;
|
||||
messageDiv.textContent = text;
|
||||
messageDiv.style.display = "block";
|
||||
|
||||
setTimeout(() => {
|
||||
messageDiv.style.display = "none";
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,10 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Newsletter Subscribers</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Create Newsletter</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
|
||||
@@ -4,134 +4,20 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin Settings</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
h2 {
|
||||
color: #555;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.setting-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.setting {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.setting strong {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
.logout {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.logout a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.logout a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.settings-management {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.settings-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.setting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.setting-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.setting-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.btn {
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
.btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
.iframe-code {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
height: 100px;
|
||||
font-family: monospace;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
.form-row .form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.message {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.edit-form {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -146,6 +32,11 @@
|
||||
style="color: #007bff; text-decoration: none; margin-right: 20px"
|
||||
>View Submissions</a
|
||||
>
|
||||
<a
|
||||
href="/admin/embeds"
|
||||
style="color: #007bff; text-decoration: none; margin-right: 20px"
|
||||
>Embeds</a
|
||||
>
|
||||
<a
|
||||
href="{{ url_for('auth.logout') }}"
|
||||
style="color: #007bff; text-decoration: none"
|
||||
@@ -203,6 +94,20 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="settings-management">
|
||||
<h2>Embeddable Forms</h2>
|
||||
<p>
|
||||
Manage embeddable forms on the <a href="/admin/embeds">Embeds page</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-management">
|
||||
<h2>Email Templates</h2>
|
||||
<div id="emailTemplates">
|
||||
<p>Loading email templates...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let appSettings = {};
|
||||
|
||||
@@ -257,11 +162,21 @@
|
||||
}
|
||||
|
||||
const settingsHtml = Object.entries(appSettings)
|
||||
.map(
|
||||
([key, value]) => `
|
||||
.map(([key, value]) => {
|
||||
const isTemplate = key === "newsletter_confirmation_template";
|
||||
const inputType = isTemplate ? "textarea" : "input";
|
||||
const inputAttrs = isTemplate
|
||||
? 'rows="10" cols="50"'
|
||||
: 'type="text"';
|
||||
return `
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<strong>${escapeHtml(key)}:</strong> ${escapeHtml(value)}
|
||||
<strong>${escapeHtml(key)}:</strong> ${
|
||||
isTemplate
|
||||
? "<em>HTML template</em>"
|
||||
: escapeHtml(value.substring(0, 50)) +
|
||||
(value.length > 50 ? "..." : "")
|
||||
}
|
||||
</div>
|
||||
<div class="setting-actions">
|
||||
<button class="btn btn-secondary" onclick="editSetting('${escapeHtml(
|
||||
@@ -280,9 +195,9 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>New Value:</label>
|
||||
<input type="text" id="edit-value-${escapeHtml(
|
||||
<${inputType} id="edit-value-${escapeHtml(
|
||||
key
|
||||
)}" value="${escapeHtml(value)}" required>
|
||||
)}" ${inputAttrs} required>${escapeHtml(value)}</${inputType}>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" onclick="updateSetting('${escapeHtml(
|
||||
@@ -294,8 +209,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
container.innerHTML = settingsHtml;
|
||||
|
||||
@@ -4,29 +4,13 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Contact Submissions</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.nav {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.nav a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.filters {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
@@ -39,124 +23,6 @@
|
||||
flex-wrap: wrap;
|
||||
align-items: end;
|
||||
}
|
||||
.filters label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.filters input,
|
||||
.filters select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.filters button {
|
||||
padding: 8px 15px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.filters button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.filters .clear-btn {
|
||||
background: #6c757d;
|
||||
}
|
||||
.filters .clear-btn:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
th:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
th.sort-asc::after {
|
||||
content: " ↑";
|
||||
}
|
||||
th.sort-desc::after {
|
||||
content: " ↓";
|
||||
}
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.message {
|
||||
padding: 8px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.message.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.pagination button {
|
||||
padding: 8px 12px;
|
||||
margin: 0 2px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pagination button:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.pagination button.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
.pagination button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.delete-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.delete-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
.submission-details {
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
60
templates/embed_contact.html
Normal file
60
templates/embed_contact.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Contact Form</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="contact-form">
|
||||
<h2>Contact Us</h2>
|
||||
<form id="contactForm" method="post" action="/api/contact">
|
||||
<div class="form-group">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">Message:</label>
|
||||
<textarea id="message" name="message" required></textarea>
|
||||
</div>
|
||||
<button type="submit">Send Message</button>
|
||||
</form>
|
||||
<div id="responseMessage"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document
|
||||
.getElementById("contactForm")
|
||||
.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
fetch("/api/contact", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const messageDiv = document.getElementById("responseMessage");
|
||||
if (data.status === "ok") {
|
||||
messageDiv.innerHTML =
|
||||
'<div class="message success">Thank you for your message! We will get back to you soon.</div>';
|
||||
this.reset();
|
||||
} else {
|
||||
messageDiv.innerHTML =
|
||||
'<div class="message error">There was an error sending your message. Please try again.</div>';
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
document.getElementById("responseMessage").innerHTML =
|
||||
'<div class="message error">There was an error sending your message. Please try again.</div>';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
55
templates/embed_newsletter.html
Normal file
55
templates/embed_newsletter.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Newsletter Subscription</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="form-container">
|
||||
<h2>Subscribe to Our Newsletter</h2>
|
||||
<form id="newsletterForm" method="post" action="/api/newsletter">
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" required />
|
||||
</div>
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
<div id="responseMessage"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document
|
||||
.getElementById("newsletterForm")
|
||||
.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
fetch("/api/newsletter", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
const messageDiv = document.getElementById("responseMessage");
|
||||
if (data.status === "ok") {
|
||||
messageDiv.innerHTML =
|
||||
'<div class="message success">Thank you for subscribing! Please check your email for confirmation.</div>';
|
||||
this.reset();
|
||||
} else {
|
||||
messageDiv.innerHTML =
|
||||
'<div class="message error">' +
|
||||
(data.message ||
|
||||
"There was an error subscribing. Please try again.") +
|
||||
"</div>";
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
document.getElementById("responseMessage").innerHTML =
|
||||
'<div class="message error">There was an error subscribing. Please try again.</div>';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,27 +4,14 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin Login</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
}
|
||||
form {
|
||||
.login-form {
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
margin: 40px auto;
|
||||
}
|
||||
input {
|
||||
display: block;
|
||||
.login-form input {
|
||||
margin: 10px 0;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
button {
|
||||
padding: 10px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flash {
|
||||
color: red;
|
||||
@@ -34,13 +21,22 @@
|
||||
<body>
|
||||
<h1>Admin Login</h1>
|
||||
{% with messages = get_flashed_messages() %} {% if messages %}
|
||||
<div class="flash">
|
||||
<div class="message error">
|
||||
{% for message in messages %} {{ message }} {% endfor %}
|
||||
</div>
|
||||
{% endif %} {% endwith %}
|
||||
<form method="post">
|
||||
<form method="post" class="login-form">
|
||||
<div class="form-group">
|
||||
<input type="text" name="username" placeholder="Username" required />
|
||||
<input type="password" name="password" placeholder="Password" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
@@ -4,69 +4,18 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Newsletter Management</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
.message {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.message.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.message.info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
.form-section {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.form-section h2 {
|
||||
margin-top: 0;
|
||||
color: #555;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
input[type="email"] {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button {
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.unsubscribe-btn {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
@@ -93,12 +42,14 @@
|
||||
<h2>Subscribe to Newsletter</h2>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="subscribe" />
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Enter your email address"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -107,12 +58,14 @@
|
||||
<h2>Unsubscribe from Newsletter</h2>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="unsubscribe" />
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Enter your email address"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="unsubscribe-btn">Unsubscribe</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
38
templates/unsubscribe_confirmation.html
Normal file
38
templates/unsubscribe_confirmation.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Unsubscription Confirmed</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css" />
|
||||
<style>
|
||||
.confirmation-container {
|
||||
text-align: center;
|
||||
}
|
||||
.icon {
|
||||
font-size: 48px;
|
||||
color: #28a745;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.back-link {
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="confirmation-container">
|
||||
<div class="icon">✓</div>
|
||||
<h1>Unsubscription Confirmed</h1>
|
||||
<p>
|
||||
You have been successfully unsubscribed from our newsletter. We're sorry
|
||||
to see you go, but you can always subscribe again if you change your
|
||||
mind.
|
||||
</p>
|
||||
<p>
|
||||
If you unsubscribed by mistake or have any questions, please feel free
|
||||
to contact us.
|
||||
</p>
|
||||
<a href="/" class="back-link">Return to Homepage</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
15
test_embed.html
Normal file
15
test_embed.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Embed</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Test Embedding Contact Form</h1>
|
||||
<iframe src="http://localhost:5001/embed/contact" width="600" height="400" frameborder="0" allowfullscreen></iframe>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
15
test_embed_newsletter.html
Normal file
15
test_embed_newsletter.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Embed Newsletter</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Test Embedding Newsletter Subscription Form</h1>
|
||||
<iframe src="http://localhost:5001/embed/newsletter" width="600" height="300" frameborder="0" allowfullscreen></iframe>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -76,13 +76,13 @@ def test_newsletter_update_email_not_found(client):
|
||||
|
||||
|
||||
def test_newsletter_manage_page_get(client):
|
||||
resp = client.get("/api/newsletter/manage")
|
||||
resp = client.get("/newsletter/manage")
|
||||
assert resp.status_code == 200
|
||||
assert b"Newsletter Subscription Management" in resp.data
|
||||
|
||||
|
||||
def test_newsletter_manage_subscribe(client):
|
||||
resp = client.post("/api/newsletter/manage",
|
||||
resp = client.post("/newsletter/manage",
|
||||
data={"email": "manage@example.com", "action": "subscribe"})
|
||||
assert resp.status_code == 200
|
||||
assert b"Successfully subscribed" in resp.data
|
||||
@@ -90,29 +90,29 @@ def test_newsletter_manage_subscribe(client):
|
||||
|
||||
def test_newsletter_manage_unsubscribe(client):
|
||||
# Subscribe first
|
||||
client.post("/api/newsletter/manage",
|
||||
client.post("/newsletter/manage",
|
||||
data={"email": "manage@example.com", "action": "subscribe"})
|
||||
# Unsubscribe
|
||||
resp = client.post("/api/newsletter/manage",
|
||||
resp = client.post("/newsletter/manage",
|
||||
data={"email": "manage@example.com", "action": "unsubscribe"})
|
||||
assert resp.status_code == 200
|
||||
assert b"Successfully unsubscribed" in resp.data
|
||||
assert b"Unsubscription Confirmed" in resp.data
|
||||
|
||||
|
||||
def test_newsletter_manage_update(client):
|
||||
# Subscribe first
|
||||
client.post("/api/newsletter/manage",
|
||||
client.post("/newsletter/manage",
|
||||
data={"email": "old@example.com", "action": "subscribe"})
|
||||
# Update
|
||||
resp = client.post("/api/newsletter/manage", data={
|
||||
"old_email": "old@example.com", "new_email": "updated@example.com", "action": "update"})
|
||||
resp = client.post("/newsletter/manage", data={
|
||||
"old_email": "old@example.com", "email": "updated@example.com", "action": "update"})
|
||||
assert resp.status_code == 200
|
||||
# Check that some success message is displayed
|
||||
assert b"success" in resp.data.lower() or b"updated" in resp.data.lower()
|
||||
assert b"updated successfully" in resp.data.lower()
|
||||
|
||||
|
||||
def test_newsletter_manage_invalid_email(client):
|
||||
resp = client.post("/api/newsletter/manage",
|
||||
resp = client.post("/newsletter/manage",
|
||||
data={"email": "invalid-email", "action": "subscribe"})
|
||||
assert resp.status_code == 200
|
||||
assert b"Please enter a valid email address" in resp.data
|
||||
|
||||
Reference in New Issue
Block a user