How Flask, Jinja2, HTML, CSS, and JavaScript Work Together in Modern Web Applications
Modern web applications are built from multiple layers that each serve a very specific purpose. Instead of relying on a single technology to handle everything, developers combine a backend framework, a templating engine, and frontend technologies to create systems that are both dynamic and scalable. One of the most effective and widely used lightweight stacks for this purpose is Flask combined with Jinja2, HTML, CSS, and JavaScript. This combination is powerful because it cleanly separates logic, structure, styling, and interactivity while still allowing them to work seamlessly together as one system.
{% 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 }}</td>
<td>—</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">
<div class="card">
<h3>Loyalty Discount</h3>
<p style="color:var(--tm);font-size:.9rem;margin-bottom:1rem">
{% if session.loyalty_discount %}
✅ <strong>Active:</strong> You're getting 10% off at checkout!
{% else %}
Get 10% off every order when you activate loyalty rewards.
{% endif %}
</p>
<form method="POST" action="{{ url_for('toggle_loyalty') }}">
<button type="submit" class="sbtn">
{% if session.loyalty_discount %}Deactivate{% else %}Activate now{% endif %}
</button>
</form>
</div>
{% if loyalty %}
<div class="card">
<h3>Points Balance</h3>
<div style="font-family:'Lora',serif;font-size:2.5rem;font-weight:600;color:var(--gd);margin:1rem 0">{{ loyalty.points_balance }}</div>
<p style="font-size:.86rem;color:var(--tl)">Points earned from purchases</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 %}
Flask is a Python-based backend framework that is responsible for handling the core logic of a web application. It manages incoming requests from users, processes data, interacts with databases, and determines what response should be sent back to the browser. However, Flask itself does not focus on how pages look or how content is displayed. Instead, it acts as the brain of the application. When a user visits a page, Flask executes Python code to gather the necessary information such as user details, product lists, or order history. Once this data is prepared, it is passed forward to the templating system so it can be converted into a readable webpage.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{% block title %}Greenfield Local Hub{% endblock %}</title>
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet"/>
<style>
/* GLOBAL VARIABLES — shared across every page */
:root {
--g: #2d5a3d;
--gd: #1a3828;
--gl: #e8f2eb;
--cr: #faf7f2;
--w: #ffffff;
--t: #1a2e1e;
--tm: #4a6050;
--tl: #849a89;
--b: #ddd6c8;
}
*,*::before,*::after { box-sizing:border-box; margin:0; padding:0; }
body { font-family:'Nunito',sans-serif; background:var(--cr); color:var(--t); line-height:1.65; overflow-x:hidden; }
a { text-decoration:none; color:inherit; }
img { display:block; max-width:100%; }
/* SHARED CLASSES used across multiple pages */
.eyebrow { font-size:.74rem; font-weight:700; letter-spacing:.15em; text-transform:uppercase; color:var(--g); margin-bottom:.5rem; }
.btn { display:inline-block; padding:.72rem 1.6rem; border-radius:50px; font-weight:700; font-size:.93rem; transition:transform .18s,background .18s; white-space:nowrap; }
.btn:hover { transform:translateY(-2px); }
.btn--white { background:var(--w); color:var(--gd); box-shadow:0 4px 14px rgba(0,0,0,.13); }
.btn--white:hover { background:var(--cr); }
.btn--outline { border:2px solid rgba(255,255,255,.55); color:#fff; }
.btn--outline:hover { border-color:#fff; background:rgba(255,255,255,.1); }
.btn--green { background:var(--g); color:#fff; }
.btn--green:hover { background:var(--gd); }
/* NAV */
.nav { display:flex; align-items:center; justify-content:space-between; padding:.85rem 5%; background:var(--gd); position:sticky; top:0; z-index:200; box-shadow:0 2px 10px rgba(0,0,0,.18); }
.nav__logo { font-family:'Lora',serif; font-size:1.2rem; font-weight:600; color:#fff; }
.nav__links { display:flex; align-items:center; gap:.25rem; }
.nl { color:rgba(255,255,255,.7); font-size:.87rem; font-weight:600; padding:.4rem .78rem; border-radius:6px; transition:all .2s; }
.nl:hover,.nl.on { color:#fff; background:rgba(255,255,255,.1); }
.ndiv { width:1px; height:20px; background:rgba(255,255,255,.18); margin:0 .4rem; }
.nbtn { padding:.38rem 1rem; border-radius:50px; font-size:.82rem; font-weight:700; transition:all .2s; white-space:nowrap; }
.nbtn.out { border:1.5px solid rgba(255,255,255,.38); color:rgba(255,255,255,.88); }
.nbtn.out:hover { border-color:#fff; background:rgba(255,255,255,.08); }
.nbtn.fill { background:#fff; color:var(--gd); }
.nbtn.fill:hover { background:var(--cr); }
/* FOOTER */
.footer { background:var(--gd); color:rgba(255,255,255,.55); padding:1.7rem 5%; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:1rem; font-size:.84rem; margin-top:4rem; }
.footer a { color:rgba(255,255,255,.55); transition:color .2s; }
.footer a:hover { color:#fff; }
.footer__links { display:flex; gap:1.4rem; }
@media(max-width:700px) {
.nav__links { display:none; }
.footer { flex-direction:column; text-align:center; }
.footer__links { justify-content:center; }
}
</style>
{# Each page can inject its own styles here #}
{% block extra_style %}{% endblock %}
</head>
<body>
{# NAV — active link highlighted using request.endpoint #}
<nav class="nav">
<a href="{{ url_for('home') }}" class="nav__logo">🌱 Greenfield</a>
<div class="nav__links">
<a href="{{ url_for('home') }}" class="nl {% if request.endpoint == 'home' %}on{% endif %}">Home</a>
<a href="{{ url_for('products') }}" class="nl {% if request.endpoint == 'products' %}on{% endif %}">Shop</a>
<div class="ndiv"></div>
{% if session.customer_name %}
<span class="nl">👋 {{ session.customer_name }}</span>
<a href="{{ url_for('view_cart') }}" class="nbtn fill" style="position:relative">
🛒 Cart
{% set cart_count = session.get('cart', {})|length %}
{% if cart_count > 0 %}<span style="background:#e74c3c;color:#fff;border-radius:50%;font-size:.65rem;padding:.1rem .38rem;margin-left:.3rem">{{ cart_count }}</span>{% endif %}
</a>
<a href="{{ url_for('account') }}" class="nbtn out">Account</a>
<a href="{{ url_for('logout') }}" class="nbtn out">Log out</a>
{% elif session.producer_name %}
<a href="{{ url_for('producer_dashboard') }}" class="nl {% if request.endpoint == 'producer_dashboard' %}on{% endif %}">Dashboard</a>
<a href="{{ url_for('logout') }}" class="nbtn out">Log out</a>
{% else %}
<a href="{{ url_for('customer_login') }}" class="nbtn out {% if request.endpoint in ['customer_login','customer_signup'] %}on{% endif %}">Customer login</a>
<a href="{{ url_for('producer_login') }}" class="nbtn fill {% if request.endpoint in ['producer_login','producer_signup'] %}on{% endif %}">Producer login</a>
{% endif %}
</div>
</nav>
{# PAGE CONTENT #}
{% block content %}{% endblock %}
{# FOOTER #}
<footer class="footer">
<span>🌱 Greenfield Local Hub — © <span id="yr"></span></span>
<div class="footer__links">
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('products') }}">Shop</a>
<a href="{{ url_for('customer_login') }}">Login</a>
</div>
</footer>
<script>document.getElementById('yr').textContent = new Date().getFullYear();</script>
</body>
</html>
This is where Jinja2 becomes essential. Jinja2 is a templating engine that acts as the bridge between Python and HTML. Without Jinja2, Flask would only be able to send static HTML files or raw data responses. With Jinja2, however, HTML becomes dynamic. It allows Python variables, loops, and conditional logic to be embedded directly into HTML templates. This means that instead of manually writing separate pages for every user or every dataset, a single template can adapt itself based on the data it receives. For example, a single page can display different names, different lists of orders, or different interface elements depending on the user’s state. Jinja2 essentially transforms static HTML into a dynamic rendering system that responds to backend data in real time.
One of the most powerful features of Jinja2 is its ability to handle loops and conditionals. Loops allow developers to dynamically generate repeated elements such as lists of products, orders, or messages without manually writing each item in HTML. Conditionals allow the interface to change based on logic defined in the backend, such as showing different content for logged-in users versus guests or displaying different status indicators for completed or pending tasks. This makes web applications feel intelligent and responsive because the content adapts automatically to the data being processed by the backend.
{% 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 %}
{% block content %}
<div class="dashboard">
<div class="header">
<h1>Welcome back, {{ producer_name }} 👋</h1>
<p>Manage your products, stock, and orders</p>
</div>
<div class="tabs">
<button class="tab on" onclick="showTab('products', this)">📦 Products & Stock</button>
<button class="tab" onclick="showTab('orders', this)">🧾 Orders</button>
<button class="tab" onclick="showTab('account', this)">👤 Account</button>
</div>
{# ── PRODUCTS & STOCK ── #}
<div class="panel on" id="panel-products">
<div class="grid">
<!-- PRODUCTS -->
<div class="card">
<h2>Your Products</h2>
<input type="text" class="search" placeholder="Search products..." onkeyup="filterTable(this, 'productsTable')">
<table class="table" id="productsTable">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Available</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
{% for p in products %}
<tr>
<td>{{ p.name }}</td>
<td>£{{ p.price }}</td>
<td>{{ 'Yes' if p.is_available else 'No' }}</td>
<td>
<form method="POST" action="{{ url_for('remove_product') }}" onsubmit="return confirm('Remove this product?')">
<input type="hidden" name="product_id" value="{{ p.product_id }}">
<button class="btn-small" style="background:#e74c3c">Remove</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h3 style="margin-top:1.2rem;margin-bottom:.7rem;font-size:1rem">Add New Product</h3>
<form method="POST" action="{{ url_for('add_product') }}" style="display:grid;grid-template-columns:1fr 1fr;gap:.6rem">
<input class="search" style="margin:0" type="text" name="name" placeholder="Product name" required>
<input class="search" style="margin:0" type="number" name="price" placeholder="Price (£)" step="0.01" required>
<input class="search" style="margin:0" type="text" name="description" placeholder="Description">
<input class="search" style="margin:0" type="number" name="quantity" placeholder="Initial stock" required>
<button class="btn-small" style="grid-column:span 2;padding:.5rem">Add Product</button>
</form>
</div>
<!-- STOCK -->
<div class="card">
<h2>Stock Management</h2>
<table class="table">
<thead>
<tr>
<th>Product</th>
<th>Qty</th>
<th>Update</th>
</tr>
</thead>
<tbody>
{% for s in stock %}
<tr>
<td>{{ s.name }}</td>
<td>{{ s.quantity_available }}</td>
<td>
<form method="POST" action="{{ url_for('update_stock') }}">
<input type="hidden" name="product_id" value="{{ s.product_id }}">
<input class="input" type="number" name="quantity" value="{{ s.quantity_available }}" min="0" required>
<button class="btn-small">Save</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- ORDERS -->
<div class="card" style="grid-column: span 2;">
<h2>Orders</h2>
<input type="text" class="search" placeholder="Search orders..." onkeyup="filterTable(this, 'ordersTable')">
<table class="table" id="ordersTable">
<thead>
<tr>
<th>ID</th>
<th>Customer</th>
<th>Product</th>
<th>Qty</th>
<th>Status</th>
<th>Update</th>
</tr>
</thead>
<tbody>
{% for o in orders %}
<tr>
<td>#{{ o.order_id }}</td>
<td>{{ o.first_name }}</td>
<td>{{ o.name }}</td>
<td>{{ o.quantity }}</td>
<td>
<span class="badge {{ o.status|lower }}">{{ o.status }}</span>
</td>
<td>
<form method="POST" action="{{ url_for('update_order_status') }}">
<input type="hidden" name="order_id" value="{{ o.order_id }}">
<select name="status" class="input">
<option>Pending</option>
<option>Completed</option>
<option>Cancelled</option>
</select>
<button class="btn-small">Update</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>{# end panel-products #}
{# ── ORDERS ── #}
<div class="panel" id="panel-orders">
<div class="card">
<h2>Orders</h2>
<input type="text" class="search" placeholder="Search orders..." onkeyup="filterTable(this, 'ordersTable2')">
<table class="table" id="ordersTable2">
<thead>
<tr><th>ID</th><th>Customer</th><th>Product</th><th>Qty</th><th>Date</th><th>Status</th><th>Update</th></tr>
</thead>
<tbody>
{% for o in orders %}
<tr>
<td>#{{ o.order_id }}</td>
<td>{{ o.first_name }}</td>
<td>{{ o.name }}</td>
<td>{{ o.quantity }}</td>
<td>{{ o.placed_at }}</td>
<td><span class="badge {{ o.status|lower }}">{{ o.status }}</span></td>
<td>
<form method="POST" action="{{ url_for('update_order_status') }}">
<input type="hidden" name="order_id" value="{{ o.order_id }}">
<select name="status" class="input" style="width:100px">
<option {% if o.status == 'Pending' %}selected{% endif %}>Pending</option>
<option {% if o.status == 'Completed' %}selected{% endif %}>Completed</option>
<option {% if o.status == 'Cancelled' %}selected{% endif %}>Cancelled</option>
</select>
<button class="btn-small">Update</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="7" style="text-align:center;color:#888;padding:1.5rem">No orders yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# ── ACCOUNT ── #}
<div class="panel" id="panel-account">
<div class="card">
<h2>Business Details</h2>
<form method="POST" action="{{ url_for('update_producer') }}">
<div class="fg">
<div class="field"><label>Business name</label><input type="text" name="business_name" value="{{ producer.business_name }}" required></div>
<div class="field"><label>Email</label><input type="email" name="email" value="{{ producer.email }}" required></div>
</div>
<button type="submit" class="sbtn">Save changes</button>
</form>
</div>
<div class="card" style="margin-top:1rem">
<h2>Change Password</h2>
<form method="POST" action="{{ url_for('change_producer_password') }}">
<div class="fg">
<div class="field full"><label>Current password</label><input type="password" name="current_password" required></div>
<div class="field"><label>New password</label><input type="password" name="new_password" required></div>
<div class="field"><label>Confirm new password</label><input type="password" name="confirm_password" required></div>
</div>
<button type="submit" class="sbtn">Update password</button>
</form>
</div>
<div class="danger-card">
<h3>Delete Account</h3>
<p>Permanently deletes your account and all your products. This cannot be undone.</p>
<form method="POST" action="{{ url_for('delete_producer') }}" onsubmit="return confirm('Are you sure? This cannot be undone.')">
<button type="submit" class="dbtn">Delete my account</button>
</form>
</div>
</div>
</div>
<script>
function showTab(name, btn) {
document.querySelectorAll('.panel').forEach(p => p.classList.remove('on'));
document.querySelectorAll('.tab').forEach(b => b.classList.remove('on'));
document.getElementById('panel-' + name).classList.add('on');
btn.classList.add('on');
}
function filterTable(input, tableId) {
let filter = input.value.toLowerCase();
document.querySelectorAll(`#${tableId} tbody tr`).forEach(row => {
row.style.display = row.innerText.toLowerCase().includes(filter) ? '' : 'none';
});
}
</script>
{% endblock %}
Another major advantage of Jinja2 is template inheritance. In most web applications, many pages share the same structure, such as navigation bars, footers, and general layout styles. Instead of repeating this code across every page, Jinja2 allows developers to define a base template that contains the shared structure. Individual pages then extend this base template and only define the parts that are unique to them. This significantly reduces duplication, improves maintainability, and ensures consistency across the entire application. Any change made to the base template automatically applies to all pages that inherit from it.
While Flask and Jinja2 handle the backend logic and dynamic rendering, HTML is responsible for the structure of the webpage. HTML defines the elements that appear on the screen, such as headings, paragraphs, forms, tables, and buttons. It acts as the skeleton of the user interface. However, HTML alone is not visually appealing or interactive. Without additional layers, it would result in a very plain and static experience.
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 * FROM product WHERE is_available = 1")
products = cur.fetchall()
cur.close()
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')
cur = mysql.connection.cursor()
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[0]
session['customer_name'] = customer[1]
return redirect(url_for('home'))
else:
error = 'Incorrect email or password.'
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)
)
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')
cur = mysql.connection.cursor()
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[0]
session['producer_name'] = producer[1]
return redirect(url_for('producer_dashboard'))
else:
error = 'Incorrect email or password.'
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))
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')
cur = mysql.connection.cursor()
cur.execute("SELECT password_hash FROM producer WHERE producer_id = %s", (producer_id,))
row = cur.fetchone()
if row and row[0] == current_password and new_password == confirm_password:
cur.execute("UPDATE producer SET password_hash = %s WHERE producer_id = %s", (new_password, producer_id))
mysql.connection.commit()
cur.close()
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():
if 'customer_id' not in session:
return redirect(url_for('customer_login'))
customer_id = session['customer_id']
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
cur.execute("SELECT * FROM customer WHERE customer_id = %s", (customer_id,))
customer = cur.fetchone()
cur.close()
cur = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
cur.execute("SELECT * FROM orders WHERE customer_id = %s ORDER BY placed_at DESC", (customer_id,))
orders = cur.fetchall()
cur.execute("SELECT * FROM loyalty WHERE customer_id = %s", (customer_id,))
loyalty = cur.fetchone()
cur.close()
return render_template('account.html', customer=customer, orders=orders, loyalty=loyalty)
@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')
cur = mysql.connection.cursor()
cur.execute("SELECT password_hash FROM customer WHERE customer_id = %s", (customer_id,))
row = cur.fetchone()
if not row or row[0] != current_password:
cur.close()
return redirect(url_for('account'))
if new_password == confirm_password:
cur.execute("UPDATE customer SET password_hash = %s WHERE customer_id = %s", (new_password, customer_id))
mysql.connection.commit()
cur.close()
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)
CSS is responsible for transforming this structure into a visually engaging interface. It controls the appearance of the application, including layout, spacing, colours, typography, and responsiveness. Through CSS, developers can create modern user interfaces that are consistent and visually appealing. It also allows for responsive design, ensuring that the application works properly across different screen sizes, from desktops to mobile devices. CSS essentially turns raw HTML into a polished and user-friendly experience.
JavaScript adds the final layer of interactivity to the system. While Flask handles logic on the server and Jinja2 renders dynamic content before it reaches the browser, JavaScript runs directly in the user’s browser and enhances the experience without requiring page reloads. It enables features such as tab switching, live search filtering, form validation, animations, and dynamic content updates. This creates a smoother and more application-like experience for the user, reducing friction and improving usability.
When all these technologies are combined, they form a complete full-stack system with clearly defined responsibilities. Flask handles the backend logic and data processing, Jinja2 converts that data into dynamic HTML templates, HTML provides the structure, CSS handles visual design, and JavaScript adds interactivity. The real strength of this stack comes from how well these layers integrate without overlapping responsibilities. Each layer focuses on its own role, which keeps the system clean, scalable, and easy to maintain.
A typical request in such a system follows a clear flow. When a user visits a page, Flask processes the request and gathers the required data from the database or business logic. This data is then passed into a Jinja2 template, which dynamically generates an HTML page by replacing placeholders and executing any embedded logic. The final HTML is sent to the browser, where CSS styles it and JavaScript activates interactive features. This separation ensures that the server handles computation and data, while the client handles presentation and interaction.
This architecture remains widely used because of its simplicity and effectiveness. It does not require complex build tools or heavy frontend frameworks, yet it is powerful enough to support real-world applications such as dashboards, marketplaces, and content platforms. It is especially valuable for developers who want full control over both backend logic and frontend presentation without introducing unnecessary complexity.
Ultimately, the combination of Flask, Jinja2, HTML, CSS, and JavaScript represents a balanced approach to web development. It allows developers to build applications that are dynamic, maintainable, and scalable while keeping each layer focused on a single responsibility. This clarity of structure is what makes the stack both beginner-friendly and powerful enough for production-level systems.
Top comments (0)