Building a Dynamic Dashboard with Flask, Jinja2, HTML, CSS, and JavaScript
Dashboards are one of the most common and valuable interfaces in modern web applications. They provide users with a centralised view of important information, allowing them to monitor activity, manage data, and interact with a system efficiently. Whether it is a customer account page, an admin control panel, or a business analytics interface, dashboards are designed to present complex data in a clear and structured way. Using a combination of Flask, Jinja2, HTML, CSS, and JavaScript offers a powerful yet straightforward approach to building such systems.
from flask import Flask, render_template, request, redirect, url_for, session
from flask_mysqldb import MySQL
import MySQLdb.cursors
app = Flask(__name__)
# ── SECRET KEY (needed for session) ──
app.secret_key = 'secret-key'
# ── MYSQL CONFIG ── change these if yours are different
app.config['MYSQL_HOST'] = 'localhost'
app.config['MYSQL_USER'] = 'root'
app.config['MYSQL_PASSWORD'] = 'root'
app.config['MYSQL_DB'] = 'greenfield'
mysql = MySQL(app)
'''
try:
conn = mysql.connection
print("CONNECTED SUCCESSFULLY")
except Exception as e:
print(e)
'''
# PUBLIC PAGES
@app.route('/')
def home():
return render_template('index.html')
@app.route('/products')
def products():
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
cur.execute("""
SELECT p.*, COALESCE(s.quantity_available, 0) as stock
FROM product p
LEFT JOIN stock s ON p.product_id = s.product_id
WHERE p.is_available = 1
""")
products = cur.fetchall()
cur.close()
print(f"DEBUG: Found {len(products)} products") # Debug line
for p in products:
print(f" - {p.get('name')} (ID: {p.get('product_id')}, Stock: {p.get('stock')})") # Debug line
return render_template('products.html', products=products)
# ════════════════════════════
# CUSTOMER AUTH
# ════════════════════════════
@app.route('/login/customer', methods=['GET', 'POST'])
def customer_login():
error = None
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
try:
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor) # Use DictCursor
cur.execute("SELECT * FROM customer WHERE email = %s AND password_hash = %s", (email, password))
customer = cur.fetchone()
cur.close()
if customer:
session['customer_id'] = customer['customer_id'] # Use column name
session['customer_name'] = customer['first_name'] # Use column name
print(f"DEBUG: Customer logged in - ID: {customer['customer_id']}, Name: {customer['first_name']}")
return redirect(url_for('home'))
else:
error = 'Incorrect email or password.'
except Exception as e:
print(f"ERROR in customer_login: {e}")
error = 'Database connection error.'
return render_template('login.html', role='Customer', role_key='customer',
signup_url=url_for('customer_signup'), error=error)
@app.route('/signup/customer', methods=['GET', 'POST'])
def customer_signup():
error = None
if request.method == 'POST':
name = request.form.get('name')
email = request.form.get('email')
password = request.form.get('password')
confirm = request.form.get('confirm')
if not all([name, email, password, confirm]):
error = 'Please fill in all fields.'
elif password != confirm:
error = 'Passwords do not match.'
else:
cur = mysql.connection.cursor()
cur.execute("SELECT id FROM customer WHERE email = %s", (email,))
existing = cur.fetchone()
if existing:
error = 'An account with that email already exists.'
else:
cur.execute(
"INSERT INTO customer (first_name, email, password_hash) VALUES (%s, %s, %s)",
(name, email, password)
)
customer_id = cur.lastrowid
cur.execute("INSERT INTO loyalty (customer_id, points_balance) VALUES (%s, 0)", (customer_id,))
mysql.connection.commit()
cur.close()
return redirect(url_for('customer_login'))
cur.close()
return render_template('signup.html', role='Customer', role_key='customer',
login_url=url_for('customer_login'), error=error)
# ════════════════════════════
# PRODUCER AUTH
# ════════════════════════════
@app.route('/login/producer', methods=['GET', 'POST'])
def producer_login():
error = None
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
try:
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor) # Use DictCursor
cur.execute("SELECT * FROM producer WHERE email = %s AND password_hash = %s", (email, password))
producer = cur.fetchone()
cur.close()
if producer:
session['producer_id'] = producer['producer_id'] # Use column name
session['producer_name'] = producer['business_name'] # Use column name
return redirect(url_for('producer_dashboard'))
else:
error = 'Incorrect email or password.'
except Exception as e:
print(f"ERROR in producer_login: {e}")
error = 'Database connection error.'
return render_template('login.html', role='Producer', role_key='producer',
signup_url=url_for('producer_signup'), error=error)
@app.route('/signup/producer', methods=['GET', 'POST'])
def producer_signup():
error = None
if request.method == 'POST':
business = request.form.get('business')
email = request.form.get('email')
password = request.form.get('password')
confirm = request.form.get('confirm')
if not all([business, email, password, confirm]):
error = 'Please fill in all fields.'
elif password != confirm:
error = 'Passwords do not match.'
else:
cur = mysql.connection.cursor()
cur.execute("SELECT producer_id FROM producer WHERE email = %s", (email,))
existing = cur.fetchone()
if existing:
error = 'An account with that email already exists.'
else:
cur.execute(
"INSERT INTO producer (business_name, email, password_hash) VALUES (%s, %s, %s)",
(business, email, password)
)
mysql.connection.commit()
cur.close()
return redirect(url_for('producer_login'))
cur.close()
return render_template('signup.html', role='Producer', role_key='producer',
login_url=url_for('producer_login'), error=error)
# ════════════════════════════
# PRODUCER DASHBOARD
# ════════════════════════════
@app.route('/dashboard')
def producer_dashboard():
# Redirect to login if not logged in
if 'producer_id' not in session:
return redirect(url_for('producer_login'))
producer_id = session['producer_id']
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
# Products belonging to this producer
cur.execute("SELECT * FROM product WHERE producer_id = %s", (producer_id,))
products = cur.fetchall()
# Orders for this producer's products
cur.execute("""
SELECT o.order_id, c.first_name, p.name, oi.quantity, o.status, o.placed_at
FROM orders o
JOIN order_item oi ON o.order_id = oi.order_id
JOIN product p ON oi.product_id = p.product_id
JOIN customer c ON o.customer_id = c.customer_id
WHERE p.producer_id = %s
ORDER BY o.placed_at DESC
""", (producer_id,))
orders = cur.fetchall()
# Stock levels
cur.execute("""
SELECT p.product_id, p.name, s.quantity_available, s.reorder_threshold
FROM stock s
JOIN product p ON s.product_id = p.product_id
WHERE p.producer_id = %s
""", (producer_id,))
stock = cur.fetchall()
# Producer account details
cur.execute("SELECT * FROM producer WHERE producer_id = %s", (producer_id,))
producer = cur.fetchone()
cur.close()
return render_template('dashboard.html',
producer_name=session['producer_name'],
producer=producer,
products=products,
orders=orders,
stock=stock)
@app.route('/dashboard/add-product', methods=['POST'])
def add_product():
if 'producer_id' not in session:
return redirect(url_for('producer_login'))
producer_id = session['producer_id']
name = request.form.get('name')
price = request.form.get('price')
description = request.form.get('description')
quantity = request.form.get('quantity')
cur = mysql.connection.cursor()
cur.execute(
"INSERT INTO product (name, price, description, is_available, producer_id) VALUES (%s, %s, %s, 1, %s)",
(name, price, description, producer_id)
)
product_id = cur.lastrowid
cur.execute(
"INSERT INTO stock (product_id, quantity_available, reorder_threshold) VALUES (%s, %s, 5)",
(product_id, quantity)
)
mysql.connection.commit()
cur.close()
return redirect(url_for('producer_dashboard'))
@app.route('/dashboard/remove-product', methods=['POST'])
def remove_product():
if 'producer_id' not in session:
return redirect(url_for('producer_login'))
product_id = request.form.get('product_id')
producer_id = session['producer_id']
cur = mysql.connection.cursor()
cur.execute("DELETE FROM stock WHERE product_id = %s", (product_id,))
cur.execute("DELETE FROM product WHERE product_id = %s AND producer_id = %s", (product_id, producer_id))
mysql.connection.commit()
cur.close()
return redirect(url_for('producer_dashboard'))
@app.route('/dashboard/update-stock', methods=['POST'])
def update_stock():
if 'producer_id' not in session:
return redirect(url_for('producer_login'))
product_id = request.form.get('product_id')
quantity = int(request.form.get('quantity', 0))
if quantity < 0:
return redirect(url_for('producer_dashboard'))
cur = mysql.connection.cursor()
cur.execute("UPDATE stock SET quantity_available = %s WHERE product_id = %s", (quantity, product_id))
# Auto-update availability based on stock
if quantity == 0:
cur.execute("UPDATE product SET is_available = 0 WHERE product_id = %s", (product_id,))
else:
cur.execute("UPDATE product SET is_available = 1 WHERE product_id = %s", (product_id,))
mysql.connection.commit()
cur.close()
return redirect(url_for('producer_dashboard'))
@app.route('/dashboard/update-producer', methods=['POST'])
def update_producer():
if 'producer_id' not in session:
return redirect(url_for('producer_login'))
producer_id = session['producer_id']
business = request.form.get('business_name')
email = request.form.get('email')
cur = mysql.connection.cursor()
cur.execute(
"UPDATE producer SET business_name=%s, email=%s WHERE producer_id=%s",
(business, email, producer_id)
)
mysql.connection.commit()
cur.close()
session['producer_name'] = business
return redirect(url_for('producer_dashboard'))
@app.route('/dashboard/change-producer-password', methods=['POST'])
def change_producer_password():
if 'producer_id' not in session:
return redirect(url_for('producer_login'))
producer_id = session['producer_id']
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
try:
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
cur.execute("SELECT password_hash FROM producer WHERE producer_id = %s", (producer_id,))
row = cur.fetchone()
if not row or row['password_hash'] != current_password:
print("DEBUG: Current password incorrect")
cur.close()
return redirect(url_for('producer_dashboard'))
if new_password != confirm_password:
print("DEBUG: New passwords don't match")
cur.close()
return redirect(url_for('producer_dashboard'))
cur.execute("UPDATE producer SET password_hash = %s WHERE producer_id = %s", (new_password, producer_id))
mysql.connection.commit()
cur.close()
print("DEBUG: Producer password updated successfully")
except Exception as e:
print(f"ERROR in change_producer_password: {e}")
return redirect(url_for('producer_dashboard'))
@app.route('/dashboard/delete-producer', methods=['POST'])
def delete_producer():
if 'producer_id' not in session:
return redirect(url_for('producer_login'))
producer_id = session['producer_id']
cur = mysql.connection.cursor()
cur.execute("DELETE FROM stock WHERE product_id IN (SELECT product_id FROM product WHERE producer_id = %s)", (producer_id,))
cur.execute("DELETE FROM product WHERE producer_id = %s", (producer_id,))
cur.execute("DELETE FROM producer WHERE producer_id = %s", (producer_id,))
mysql.connection.commit()
cur.close()
session.clear()
return redirect(url_for('home'))
@app.route('/dashboard/update-order-status', methods=['POST'])
def update_order_status():
if 'producer_id' not in session:
return redirect(url_for('producer_login'))
order_id = request.form.get('order_id')
status = request.form.get('status')
cur = mysql.connection.cursor()
cur.execute("UPDATE orders SET status = %s WHERE order_id = %s", (status, order_id))
mysql.connection.commit()
cur.close()
return redirect(url_for('producer_dashboard'))
# ════════════════════════════
# ACCOUNT
# ════════════════════════════
@app.route('/account')
def account():
print(f"DEBUG: Session contents: {session}")
print(f"DEBUG: customer_id in session: {'customer_id' in session}")
if 'customer_id' not in session:
print("DEBUG: No customer_id in session, redirecting to login")
return redirect(url_for('customer_login'))
customer_id = session['customer_id']
print(f"DEBUG: Customer ID from session: {customer_id}")
try:
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
cur.execute("SELECT * FROM customer WHERE customer_id = %s", (customer_id,))
customer = cur.fetchone()
if not customer:
print("DEBUG: Customer not found in database")
session.clear()
cur.close()
return redirect(url_for('customer_login'))
print(f"DEBUG: Found customer: {customer.get('first_name')}")
# Get orders with total price
cur.execute("""
SELECT o.*,
COALESCE(SUM(oi.quantity * oi.unit_price), 0) as total
FROM orders o
LEFT JOIN order_item oi ON o.order_id = oi.order_id
WHERE o.customer_id = %s
GROUP BY o.order_id
ORDER BY o.placed_at DESC
""", (customer_id,))
orders = cur.fetchall()
# Don't fail if loyalty doesn't exist - just set to None
try:
cur.execute("SELECT * FROM loyalty WHERE customer_id = %s", (customer_id,))
loyalty = cur.fetchone()
except:
loyalty = None
cur.close()
return render_template('account.html', customer=customer, orders=orders, loyalty=loyalty)
except Exception as e:
print(f"ERROR in account route: {e}")
return redirect(url_for('customer_login'))
@app.route('/account/update', methods=['POST'])
def update_account():
if 'customer_id' not in session:
return redirect(url_for('customer_login'))
customer_id = session['customer_id']
fields = ['first_name', 'last_name', 'email', 'address_line1', 'town', 'postcode']
values = [request.form.get(f) for f in fields]
cur = mysql.connection.cursor()
cur.execute("""
UPDATE customer SET first_name=%s, last_name=%s, email=%s,
address_line1=%s, town=%s, postcode=%s WHERE customer_id=%s
""", (*values, customer_id))
mysql.connection.commit()
cur.close()
return redirect(url_for('account'))
@app.route('/account/password', methods=['POST'])
def change_password():
if 'customer_id' not in session:
return redirect(url_for('customer_login'))
customer_id = session['customer_id']
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
try:
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
cur.execute("SELECT password_hash FROM customer WHERE customer_id = %s", (customer_id,))
row = cur.fetchone()
if not row or row['password_hash'] != current_password:
print("DEBUG: Current password incorrect")
cur.close()
return redirect(url_for('account'))
if new_password != confirm_password:
print("DEBUG: New passwords don't match")
cur.close()
return redirect(url_for('account'))
cur.execute("UPDATE customer SET password_hash = %s WHERE customer_id = %s", (new_password, customer_id))
mysql.connection.commit()
cur.close()
print("DEBUG: Password updated successfully")
except Exception as e:
print(f"ERROR in change_password: {e}")
return redirect(url_for('account'))
@app.route('/account/toggle-loyalty', methods=['POST'])
def toggle_loyalty():
if 'customer_id' not in session:
return redirect(url_for('customer_login'))
session['loyalty_discount'] = not session.get('loyalty_discount', False)
return redirect(url_for('account'))
# ════════════════════════════
# CART & ORDERS
# ════════════════════════════
@app.route('/cart/add', methods=['POST'])
def add_to_cart():
if 'customer_id' not in session:
return redirect(url_for('customer_login'))
product_id = request.form.get('product_id')
quantity = int(request.form.get('quantity', 1))
cart = session.get('cart', {})
cart[product_id] = cart.get(product_id, 0) + quantity
session['cart'] = cart
return redirect(url_for('products'))
@app.route('/cart/remove', methods=['POST'])
def remove_from_cart():
product_id = request.form.get('product_id')
cart = session.get('cart', {})
cart.pop(product_id, None)
session['cart'] = cart
return redirect(url_for('view_cart'))
@app.route('/cart')
def view_cart():
if 'customer_id' not in session:
return redirect(url_for('customer_login'))
cart = session.get('cart', {})
items = []
total = 0
if cart:
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
for product_id, qty in cart.items():
cur.execute("SELECT * FROM product WHERE product_id = %s", (product_id,))
p = cur.fetchone()
if p:
subtotal = float(p['price']) * qty
total += subtotal
items.append({'product': p, 'qty': qty, 'subtotal': subtotal})
cur.close()
discount = total * 0.10 if session.get('loyalty_discount') else 0
final_total = total - discount
return render_template('cart.html', items=items, total=total, discount=discount, final_total=final_total)
@app.route('/cart/place-order', methods=['POST'])
def place_order():
if 'customer_id' not in session:
return redirect(url_for('customer_login'))
cart = session.get('cart', {})
if not cart:
return redirect(url_for('view_cart'))
customer_id = session['customer_id']
order_type = request.form.get('order_type')
scheduled_at = request.form.get('scheduled_at') or None
delivery_address = request.form.get('delivery_address')
notes = request.form.get('notes')
cur = mysql.connection.cursor()
cur.execute(
"INSERT INTO orders (customer_id, status, order_type, scheduled_at, delivery_address, notes) VALUES (%s, 'Pending', %s, %s, %s, %s)",
(customer_id, order_type, scheduled_at, delivery_address, notes)
)
order_id = cur.lastrowid
for product_id, qty in cart.items():
cur.execute("SELECT price FROM product WHERE product_id = %s", (product_id,))
price = cur.fetchone()[0]
cur.execute(
"INSERT INTO order_item (order_id, product_id, quantity, unit_price) VALUES (%s, %s, %s, %s)",
(order_id, product_id, qty, price)
)
cur.execute(
"UPDATE stock SET quantity_available = quantity_available - %s WHERE product_id = %s AND quantity_available >= %s",
(qty, product_id, qty)
)
mysql.connection.commit()
cur.close()
session.pop('cart', None)
return redirect(url_for('order_confirmation', order_id=order_id))
@app.route('/order/confirmation/<int:order_id>')
def order_confirmation(order_id):
if 'customer_id' not in session:
return redirect(url_for('customer_login'))
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
cur.execute("SELECT * FROM orders WHERE order_id = %s AND customer_id = %s", (order_id, session['customer_id']))
order = cur.fetchone()
cur.execute("""
SELECT oi.quantity, p.name, p.price
FROM order_item oi
JOIN product p ON oi.product_id = p.product_id
WHERE oi.order_id = %s
""", (order_id,))
items = cur.fetchall()
cur.close()
return render_template('order_confirmation.html', order=order, items=items)
# ════════════════════════════
# LOGOUT
# ════════════════════════════
@app.route('/logout')
def logout():
session.clear()
return redirect(url_for('home'))
if __name__ == '__main__':
app.run(debug=True)
At the core of any dashboard lies the backend, and in this stack, Flask takes on that responsibility. Flask handles incoming requests, processes data, and determines what information needs to be displayed. When a user accesses a dashboard, Flask is responsible for identifying who the user is, retrieving relevant data such as account details, transactions, or inventory, and preparing it in a format that can be rendered on the page. This step is crucial because a dashboard is only as useful as the data it presents. Flask ensures that the information is accurate, up-to-date, and tailored to the specific user.
{% extends 'base.html' %}
{% block title %}Shop — Greenfield{% endblock %}
{% block extra_style %}
<style>
.page-hdr { background:var(--gd); padding:2.5rem 5%; color:#fff; }
.page-hdr h1 { font-family:'Lora',serif; font-size:clamp(1.6rem,3.5vw,2.4rem); font-weight:600; margin-bottom:.3rem; }
.page-hdr p { color:rgba(255,255,255,.7); font-size:.94rem; margin:0; }
.filter-bar { background:#fff; border-bottom:1px solid var(--b); padding:.9rem 5%; display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; }
.filter-bar p { font-size:.83rem; font-weight:700; color:var(--tl); margin:0 .3rem 0 0; }
.fbtn { padding:.38rem .95rem; border-radius:50px; font-size:.8rem; font-weight:700; font-family:inherit; cursor:pointer; border:1.5px solid var(--b); background:#fff; color:var(--tm); transition:all .2s; }
.fbtn:hover,.fbtn.on { background:var(--g); border-color:var(--g); color:#fff; }
.shop { max-width:1060px; margin:0 auto; padding:2.5rem 5%; }
.product-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:1.3rem; }
.pcard { background:#fff; border:1.5px solid var(--b); border-radius:12px; overflow:hidden; display:flex; flex-direction:column; transition:transform .2s,box-shadow .2s; }
.pcard:hover { transform:translateY(-4px); box-shadow:0 10px 32px rgba(26,56,40,.13); }
.pcard img { width:100%; height:170px; object-fit:cover; }
.pcard__body { padding:1rem; flex:1; display:flex; flex-direction:column; gap:.3rem; }
.pcard__name { font-family:'Lora',serif; font-size:1rem; font-weight:600; color:var(--t); }
.pcard__desc { font-size:.83rem; color:var(--tm); flex:1; margin-top:.2rem; }
.pcard__foot { display:flex; align-items:center; justify-content:space-between; padding:.85rem 1rem; border-top:1px solid var(--b); gap:.5rem; }
.pcard__price { font-family:'Lora',serif; font-size:1.15rem; font-weight:600; color:var(--gd); }
.qty-input { width:52px; padding:.3rem .4rem; border:1.5px solid var(--b); border-radius:6px; font-size:.85rem; text-align:center; }
.add-btn { background:var(--g); color:#fff; border:none; border-radius:8px; padding:.42rem .85rem; font-size:.8rem; font-weight:700; font-family:inherit; cursor:pointer; transition:background .2s; }
.add-btn:hover { background:var(--gd); }
.empty { text-align:center; padding:4rem; color:var(--tl); font-size:.94rem; }
</style>
{% endblock %}
{% block content %}
<div class="page-hdr">
<h1>🛒 Fresh produce</h1>
<p>Browse products from local farmers and producers.</p>
</div>
<div class="filter-bar">
<p>Filter:</p>
<button class="fbtn on" onclick="filterCat('all',this)">All</button>
<button class="fbtn" onclick="filterCat('vegetables',this)">🥦 Vegetables</button>
<button class="fbtn" onclick="filterCat('fruit',this)">🍓 Fruit</button>
<button class="fbtn" onclick="filterCat('dairy',this)">🥛 Dairy</button>
<button class="fbtn" onclick="filterCat('bakery',this)">🍞 Bakery</button>
<button class="fbtn" onclick="filterCat('meat',this)">🥩 Meat</button>
</div>
<div class="shop">
{% if products %}
<div class="product-grid" id="grid">
{% for p in products %}
<div class="pcard" data-cat="{{ p.category | default('other') | lower }}">
<img src="{{ p.image_url or 'https://images.unsplash.com/photo-1542838132-92c53300491e?w=400&q=70' }}" alt="{{ p.name }}"/>
<div class="pcard__body">
<p class="pcard__name">{{ p.name }}</p>
<p class="pcard__desc">{{ p.description or '' }}</p>
<p style="font-size:.78rem;color:var(--tl);margin-top:.3rem">
{% if p.stock > 0 %}
✓ {{ p.stock }} in stock
{% else %}
✗ Out of stock
{% endif %}
</p>
</div>
<div class="pcard__foot">
<span class="pcard__price">£{{ "%.2f"|format(p.price) }}</span>
{% if session.customer_id %}
{% if p.stock > 0 %}
<form method="POST" action="{{ url_for('add_to_cart') }}" style="display:flex;align-items:center;gap:.4rem">
<input type="hidden" name="product_id" value="{{ p.product_id }}">
<input class="qty-input" type="number" name="quantity" value="1" min="1" max="{{ p.stock }}">
<button class="add-btn">Add</button>
</form>
{% else %}
<button class="add-btn" disabled style="background:#ccc;cursor:not-allowed">Out of stock</button>
{% endif %}
{% else %}
<a href="{{ url_for('customer_login') }}" class="add-btn" style="text-decoration:none">Login to buy</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty"><p>No products found. Check back soon!</p></div>
{% endif %}
</div>
<script>
function filterCat(cat, btn) {
document.querySelectorAll('.fbtn').forEach(b => b.classList.remove('on'));
btn.classList.add('on');
document.querySelectorAll('.pcard').forEach(c => {
c.style.display = (cat === 'all' || c.dataset.cat === cat) ? 'flex' : 'none';
});
}
</script>
{% endblock %}
Once the backend has gathered the necessary data, Jinja2 plays a critical role in transforming that data into a visual interface. A dashboard is not a static page; it changes depending on the user, their activity, and the state of the system. Jinja2 allows developers to embed dynamic content directly into HTML templates, enabling the dashboard to adapt automatically. Instead of creating separate pages for each scenario, a single template can display different content depending on the data passed into it. This makes dashboards highly scalable, as they can handle large datasets and varied user states without requiring additional templates for each case.
{% extends 'base.html' %}
{% block title %}My Account — Greenfield{% endblock %}
{% block extra_style %}
<style>
.account { max-width:860px; margin:0 auto; padding:2.5rem 5%; }
/* HEADER */
.acct-hdr { display:flex; align-items:center; gap:1.1rem; margin-bottom:2rem; padding-bottom:1.5rem; border-bottom:1px solid var(--b); }
.acct-avatar { width:54px; height:54px; border-radius:50%; background:var(--g); color:#fff; font-family:'Lora',serif; font-size:1.4rem; font-weight:600; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.acct-hdr h1 { font-family:'Lora',serif; font-size:1.45rem; color:var(--gd); margin-bottom:.1rem; }
.acct-hdr p { font-size:.84rem; color:var(--tl); margin:0; }
/* FLASH */
.flash { background:var(--gl); border:1px solid #a8d5b5; color:var(--gd); border-radius:8px; padding:.6rem 1rem; font-size:.85rem; margin-bottom:1.2rem; display:none; }
.flash.show { display:block; }
/* TABS */
.tabs { display:flex; gap:.3rem; border-bottom:2px solid var(--b); margin-bottom:1.8rem; }
.tab { padding:.58rem 1.05rem; font-size:.86rem; font-weight:700; font-family:inherit; cursor:pointer; border:none; background:none; color:var(--tl); border-bottom:2px solid transparent; margin-bottom:-2px; transition:all .2s; }
.tab:hover { color:var(--g); }
.tab.on { color:var(--g); border-bottom-color:var(--g); }
/* PANELS */
.panel { display:none; }
.panel.on { display:block; }
/* CARD */
.card { background:#fff; border:1px solid var(--b); border-radius:12px; padding:1.5rem; margin-bottom:1.1rem; }
.card h3 { font-family:'Lora',serif; font-size:.97rem; color:var(--gd); margin-bottom:1rem; padding-bottom:.65rem; border-bottom:1px solid var(--b); }
/* FORM */
.fg { display:grid; grid-template-columns:1fr 1fr; gap:.85rem; }
.field { display:flex; flex-direction:column; gap:.25rem; }
.field.full { grid-column:1/-1; }
.field label { font-size:.79rem; font-weight:700; color:var(--tm); }
.field input { padding:.62rem .88rem; border:1.5px solid var(--b); border-radius:8px; font-family:inherit; font-size:.9rem; color:var(--t); outline:none; transition:border-color .2s; }
.field input:focus { border-color:var(--g); }
.field input:disabled { background:#f7f4ef; color:var(--tl); cursor:not-allowed; }
.sbtn { margin-top:.9rem; padding:.62rem 1.4rem; background:var(--g); color:#fff; border:none; border-radius:8px; font-size:.88rem; font-weight:700; font-family:inherit; cursor:pointer; transition:background .2s; }
.sbtn:hover { background:var(--gd); }
/* ORDERS TABLE */
table { width:100%; border-collapse:collapse; }
th,td { padding:.65rem 1rem; text-align:left; font-size:.84rem; }
th { font-size:.71rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--tl); background:#f7f4ef; border-bottom:1px solid var(--b); }
td { border-bottom:1px solid #f0ebe0; color:var(--tm); }
tr:last-child td { border-bottom:none; }
.badge { display:inline-block; padding:.18rem .6rem; border-radius:50px; font-size:.7rem; font-weight:700; }
.badge--green { background:var(--gl); color:var(--gd); }
.badge--amber { background:#fdf3e3; color:#7a4f10; }
.badge--blue { background:#e8f0fb; color:#2563a8; }
.badge--red { background:#fdecea; color:#c0392b; }
/* LOYALTY */
.loy-hero { background:var(--gd); border-radius:12px; padding:1.6rem; color:#fff; display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; margin-bottom:1.1rem; }
.loy-pts { font-family:'Lora',serif; font-size:2.8rem; font-weight:600; line-height:1; }
.loy-pts-label { font-size:.78rem; color:rgba(255,255,255,.6); margin-top:.25rem; }
.loy-tier { text-align:right; }
.loy-tier-name { font-family:'Lora',serif; font-size:1.2rem; }
.loy-tier-sub { font-size:.76rem; color:rgba(255,255,255,.55); margin-top:.15rem; }
.prog-label { display:flex; justify-content:space-between; font-size:.76rem; color:var(--tl); margin-bottom:.35rem; }
.prog-bar { height:7px; background:var(--b); border-radius:4px; overflow:hidden; margin-bottom:1.1rem; }
.prog-bar__fill { height:100%; background:var(--g); border-radius:4px; }
.perks { list-style:none; display:flex; flex-direction:column; gap:.45rem; }
.perks li { font-size:.86rem; color:var(--tm); display:flex; gap:.45rem; }
.perks li::before { content:'✓'; color:var(--g); font-weight:700; }
/* DANGER */
.danger { border-color:#f5c6c2; }
.danger h3 { color:#c0392b; border-bottom-color:#f5c6c2; }
.danger p { font-size:.86rem; color:var(--tm); margin-bottom:.9rem; }
.dbtn { padding:.58rem 1.2rem; background:#fdecea; color:#c0392b; border:1px solid #f5c6c2; border-radius:8px; font-size:.84rem; font-weight:700; font-family:inherit; cursor:pointer; transition:all .2s; }
.dbtn:hover { background:#c0392b; color:#fff; }
@media(max-width:580px) {
.fg { grid-template-columns:1fr; }
.loy-hero { flex-direction:column; }
.loy-tier { text-align:left; }
}
</style>
{% endblock %}
{% block content %}
<div class="account">
{# HEADER #}
<div class="acct-hdr">
<div class="acct-avatar">{{ customer.first_name[0] | upper }}</div>
<div>
<h1>{{ customer.first_name }}{% if customer.last_name %} {{ customer.last_name }}{% endif %}</h1>
<p>{{ customer.email }}</p>
</div>
</div>
<div class="flash" id="flash">✅ Changes saved successfully.</div>
{# TABS #}
<div class="tabs">
<button class="tab on" onclick="showTab('details', this)">👤 Details</button>
<button class="tab" onclick="showTab('orders', this)">📦 Orders</button>
<button class="tab" onclick="showTab('loyalty', this)">⭐ Loyalty</button>
<button class="tab" onclick="showTab('password', this)">🔒 Password</button>
</div>
{# ── DETAILS ── #}
<div class="panel on" id="panel-details">
<div class="card">
<h3>Personal information</h3>
<form method="POST" action="{{ url_for('update_account') }}" onsubmit="showFlash(event)">
<div class="fg">
<div class="field"><label>First name</label><input type="text" name="first_name" value="{{ customer.first_name }}"/></div>
<div class="field"><label>Last name</label><input type="text" name="last_name" value="{{ customer.last_name or '' }}"/></div>
<div class="field full"><label>Email</label><input type="email" name="email" value="{{ customer.email }}"/></div>
</div>
<button type="submit" class="sbtn">Save changes</button>
</form>
</div>
<div class="card">
<h3>Delivery address</h3>
<form method="POST" action="{{ url_for('update_account') }}" onsubmit="showFlash(event)">
<div class="fg">
<div class="field full"><label>Address</label><input type="text" name="address_line1" value="{{ customer.address_line1 or '' }}" placeholder="12 High Street"/></div>
<div class="field"><label>Town</label><input type="text" name="town" value="{{ customer.town or '' }}" placeholder="Bristol"/></div>
<div class="field"><label>Postcode</label><input type="text" name="postcode" value="{{ customer.postcode or '' }}" placeholder="BS1 1AA"/></div>
</div>
<button type="submit" class="sbtn">Save address</button>
</form>
</div>
</div>
{# ── ORDERS ── #}
<div class="panel" id="panel-orders">
<div class="card">
<h3>Order history</h3>
{% if orders %}
<table>
<thead><tr><th>Order #</th><th>Date</th><th>Total</th><th>Type</th><th>Status</th></tr></thead>
<tbody>
{% for o in orders %}
<tr>
<td><strong>#{{ o.order_id }}</strong></td>
<td>{{ o.placed_at.strftime('%Y-%m-%d %H:%M') if o.placed_at else '—' }}</td>
<td>£{{ "%.2f"|format(o.total) }}</td>
<td>{{ o.order_type | capitalize if o.order_type else '—' }}</td>
<td>
{% if o.status == 'Pending' %}<span class="badge badge--amber">Pending</span>
{% elif o.status == 'Dispatched' %}<span class="badge badge--blue">Dispatched</span>
{% elif o.status == 'Delivered' or o.status == 'Completed' %}<span class="badge badge--green">{{ o.status }}</span>
{% else %}<span class="badge badge--red">{{ o.status | capitalize }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p style="color:var(--tl);font-size:.9rem;text-align:center;padding:2rem 0">No orders yet. <a href="{{ url_for('products') }}" style="color:var(--g);font-weight:700">Start shopping →</a></p>
{% endif %}
</div>
</div>
{# ── LOYALTY ── #}
<div class="panel" id="panel-loyalty">
{% if loyalty %}
<div class="loy-hero">
<div>
<div class="loy-pts">{{ loyalty.points_balance }}</div>
<div class="loy-pts-label">Points balance</div>
</div>
<div class="loy-tier">
<div class="loy-tier-name">
{% if loyalty.points_balance >= 2000 %}🌳 Harvest
{% elif loyalty.points_balance >= 500 %}🥇 Grower
{% else %}🥈 Seedling{% endif %}
</div>
<div class="loy-tier-sub">Current tier</div>
</div>
</div>
<div class="card">
{% if loyalty.points_balance < 500 %}
{% set pct = (loyalty.points_balance / 500 * 100) | int %}
<h3>Progress to Grower ({{ 500 - loyalty.points_balance }} pts to go)</h3>
{% elif loyalty.points_balance < 2000 %}
{% set pct = ((loyalty.points_balance - 500) / 1500 * 100) | int %}
<h3>Progress to Harvest ({{ 2000 - loyalty.points_balance }} pts to go)</h3>
{% else %}
{% set pct = 100 %}
<h3>🎉 Maximum tier reached!</h3>
{% endif %}
<div class="prog-bar"><div class="prog-bar__fill" style="width:{{ pct }}%"></div></div>
<ul class="perks">
{% if loyalty.points_balance >= 2000 %}
<li>15 pts per £1 spent</li><li>Free delivery on every order</li><li>Priority support & producer events</li>
{% elif loyalty.points_balance >= 500 %}
<li>12 pts per £1 spent</li><li>Early access to seasonal products</li><li>Free delivery once a month</li>
{% else %}
<li>10 pts per £1 spent</li><li>Member-only offers</li><li>100 pt welcome bonus</li>
{% endif %}
</ul>
</div>
{% else %}
<div class="card"><p style="color:var(--tl);font-size:.9rem">No loyalty account found.</p></div>
{% endif %}
</div>
{# ── PASSWORD ── #}
<div class="panel" id="panel-password">
<div class="card">
<h3>Change password</h3>
<form method="POST" action="{{ url_for('change_password') }}">
<div class="fg">
<div class="field full"><label>Current password</label><input type="password" name="current_password" placeholder="Current password"/></div>
<div class="field"><label>New password</label><input type="password" name="new_password" placeholder="At least 8 characters"/></div>
<div class="field"><label>Confirm new password</label><input type="password" name="confirm_password" placeholder="Repeat new password"/></div>
</div>
<button type="submit" class="sbtn">Update password</button>
</form>
</div>
<div class="card danger">
<h3>Delete account</h3>
<p>Permanently delete your account and all data. This cannot be undone.</p>
<button class="dbtn" onclick="if(confirm('Are you sure? This cannot be undone.')) window.location='{{ url_for('logout') | e }}'">Delete my account</button>
</div>
</div>
</div>
<script>
function showTab(name, btn) {
document.querySelectorAll('.panel').forEach(function(p){ p.classList.remove('on'); });
document.querySelectorAll('.tab').forEach(function(b){ b.classList.remove('on'); });
document.getElementById('panel-' + name).classList.add('on');
btn.classList.add('on');
}
function showFlash(e) {
e.preventDefault();
var f = document.getElementById('flash');
f.classList.add('show');
setTimeout(function(){ f.classList.remove('show'); }, 3000);
e.target.submit();
}
</script>
{% endblock %}
One of the most important aspects of building a dashboard is structuring the layout, and this is where HTML comes into play. HTML defines the sections of the dashboard, such as headers, navigation menus, content panels, and data tables. A well-structured dashboard typically includes a clear hierarchy, with key information placed prominently and secondary details organised into smaller sections. This structure ensures that users can quickly find what they need without feeling overwhelmed. HTML acts as the blueprint of the dashboard, determining how information is organised and presented.
{% extends 'base.html' %}
{% block title %}Producer Dashboard{% endblock %}
{% block extra_style %}
<style>
.dashboard {
padding: 2rem 6%;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 1.8rem;
color: var(--gd);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
/* CARDS */
.card {
background: var(--w);
border-radius: 14px;
padding: 1.4rem;
box-shadow: 0 6px 18px rgba(0,0,0,.08);
}
.card h2 {
margin-bottom: 1rem;
font-size: 1.2rem;
}
/* TABLE */
.table {
width: 100%;
border-collapse: collapse;
font-size: .85rem;
}
.table th {
text-align: left;
padding: .5rem;
border-bottom: 2px solid var(--b);
}
.table td {
padding: .5rem;
border-bottom: 1px solid var(--b);
}
/* BADGES */
.badge {
padding: .25rem .6rem;
border-radius: 20px;
font-size: .75rem;
font-weight: 700;
}
.pending { background:#ffe7b3; }
.completed { background:#c8f7d4; }
.cancelled { background:#ffd2d2; }
/* INPUT */
.input {
width: 70px;
padding: .3rem;
border: 1px solid var(--b);
border-radius: 6px;
}
/* SEARCH */
.search {
margin-bottom: 1rem;
padding: .5rem;
width: 100%;
border-radius: 8px;
border: 1px solid var(--b);
}
/* TABS */
.tabs { display:flex; gap:.3rem; border-bottom:2px solid var(--b); margin-bottom:1.8rem; flex-wrap:wrap; }
.tab { padding:.58rem 1.05rem; font-size:.86rem; font-weight:700; cursor:pointer; border:none; background:none; color:#888; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all .2s; }
.tab:hover { color:var(--g); }
.tab.on { color:var(--g); border-bottom-color:var(--g); }
.panel { display:none; }
.panel.on { display:block; }
/* DANGER */
.danger-card { background:#fff; border:1px solid #f5c6c2; border-radius:12px; padding:1.5rem; margin-top:1rem; }
.danger-card h3 { color:#c0392b; margin-bottom:.5rem; }
.danger-card p { font-size:.86rem; color:#666; margin-bottom:.9rem; }
.dbtn { padding:.58rem 1.2rem; background:#fdecea; color:#c0392b; border:1px solid #f5c6c2; border-radius:8px; font-size:.84rem; font-weight:700; cursor:pointer; transition:all .2s; }
.dbtn:hover { background:#c0392b; color:#fff; }
/* FORM */
.fg { display:grid; grid-template-columns:1fr 1fr; gap:.85rem; margin-bottom:.9rem; }
.field { display:flex; flex-direction:column; gap:.25rem; }
.field.full { grid-column:1/-1; }
.field label { font-size:.79rem; font-weight:700; color:#555; }
.field input { padding:.62rem .88rem; border:1.5px solid var(--b); border-radius:8px; font-family:inherit; font-size:.9rem; outline:none; transition:border-color .2s; }
.field input:focus { border-color:var(--g); }
.sbtn { padding:.62rem 1.4rem; background:var(--g); color:#fff; border:none; border-radius:8px; font-size:.88rem; font-weight:700; cursor:pointer; }
.sbtn:hover { background:var(--gd); }
</style>
{% endblock %}
Top comments (0)