What this project covers

This project combines the topics from Tutorials 1 to 6:

Django with Bootstrap for responsive layouts, cards, tables, navbar, alerts, and dashboard UI.

Django with Tailwind CSS for modern utility-first form and page styling.

AJAX in Django using JavaScript fetch() for likes and live interactions without page reload.

Django with HTMX for partial page updates, dynamic article list refresh, and form submission.

Interactive dashboards with statistics, tables, filters, and Chart.js charts.

Django Form UX improvements with labels, placeholders, help text, validation, error messages, loading states, and clean feedback.

1. Create the project folder

mkdir smartdash_project
cd smartdash_project

2. Create and activate virtual environment

python -m venv venv

On Windows:

venv\Scripts\activate

On Linux/Mac:

source venv/bin/activate

3. Install Django

pip install django

4. Create Django project and app

django-admin startproject config .
python manage.py startapp core

5. Project structure

smartdash_project/
│
├── manage.py
├── config/
│   ├── settings.py
│   ├── urls.py
│
├── core/
│   ├── admin.py
│   ├── forms.py
│   ├── models.py
│   ├── urls.py
│   ├── views.py
│
├── templates/
│   ├── base.html
│   └── core/
│       ├── home.html
│       ├── dashboard.html
│       ├── article_list.html
│       ├── contact.html
│       └── partials/
│           ├── article_table.html
│           ├── article_cards.html
│           └── contact_form.html
│
└── static/
    ├── css/
    │   └── style.css
    └── js/
        └── main.js

6. config/settings.py

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = "django-insecure-change-this-key-in-production"

DEBUG = True

ALLOWED_HOSTS = []

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "core",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "config.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "config.wsgi.application"

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

LANGUAGE_CODE = "en-us"

TIME_ZONE = "Africa/Casablanca"

USE_I18N = True

USE_TZ = True

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

7. config/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("core.urls")),
]

8. core/models.py

from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=120, unique=True)

    class Meta:
        verbose_name_plural = "Categories"
        ordering = ["name"]

    def __str__(self):
        return self.name


class Article(models.Model):
    STATUS_CHOICES = [
        ("draft", "Draft"),
        ("published", "Published"),
    ]

    title = models.CharField(max_length=200)
    summary = models.TextField(max_length=500)
    content = models.TextField()
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        related_name="articles",
    )
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="draft")
    views = models.PositiveIntegerField(default=0)
    likes = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

9. core/forms.py

from django import forms
from .models import Article


class ContactForm(forms.Form):
    name = forms.CharField(
        label="Full Name",
        max_length=100,
        widget=forms.TextInput(attrs={
            "class": "form-control form-control-lg",
            "placeholder": "Enter your full name",
        }),
        error_messages={
            "required": "Please enter your full name.",
        },
    )

    email = forms.EmailField(
        label="Email Address",
        widget=forms.EmailInput(attrs={
            "class": "form-control form-control-lg",
            "placeholder": "example@email.com",
        }),
        error_messages={
            "required": "Please enter your email address.",
            "invalid": "Please enter a valid email address.",
        },
    )

    message = forms.CharField(
        label="Message",
        help_text="Minimum 20 characters.",
        widget=forms.Textarea(attrs={
            "class": "form-control form-control-lg",
            "placeholder": "Write your message here...",
            "rows": 5,
        }),
        error_messages={
            "required": "Please write your message.",
        },
    )

    def clean_message(self):
        message = self.cleaned_data["message"]

        if len(message) < 20:
            raise forms.ValidationError("Your message must contain at least 20 characters.")

        return message


class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "summary", "content", "category", "status"]
        widgets = {
            "title": forms.TextInput(attrs={
                "class": "form-control",
                "placeholder": "Article title",
            }),
            "summary": forms.Textarea(attrs={
                "class": "form-control",
                "rows": 3,
                "placeholder": "Short article summary",
            }),
            "content": forms.Textarea(attrs={
                "class": "form-control",
                "rows": 6,
                "placeholder": "Full article content",
            }),
            "category": forms.Select(attrs={
                "class": "form-select",
            }),
            "status": forms.Select(attrs={
                "class": "form-select",
            }),
        }

10. core/admin.py

from django.contrib import admin
from .models import Category, Article


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ["name"]
    search_fields = ["name"]


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ["title", "category", "status", "views", "likes", "created_at"]
    list_filter = ["status", "category", "created_at"]
    search_fields = ["title", "summary", "content"]

11. core/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path("", views.home, name="home"),
    path("dashboard/", views.dashboard, name="dashboard"),
    path("articles/", views.article_list, name="article_list"),
    path("articles/filter/", views.article_filter_htmx, name="article_filter_htmx"),
    path("articles/<int:article_id>/like/", views.like_article_ajax, name="like_article_ajax"),
    path("contact/", views.contact, name="contact"),
    path("contact/htmx/", views.contact_htmx, name="contact_htmx"),
]

12. core/views.py

from django.contrib import messages
from django.db.models import Count, Sum
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render

from .forms import ContactForm
from .models import Article, Category


def home(request):
    latest_articles = Article.objects.filter(status="published")[:3]

    return render(request, "core/home.html", {
        "latest_articles": latest_articles,
    })


def dashboard(request):
    status = request.GET.get("status", "")
    category_id = request.GET.get("category", "")

    articles = Article.objects.select_related("category").all()

    if status:
        articles = articles.filter(status=status)

    if category_id:
        articles = articles.filter(category_id=category_id)

    total_articles = articles.count()
    published_articles = articles.filter(status="published").count()
    draft_articles = articles.filter(status="draft").count()
    total_views = articles.aggregate(total=Sum("views"))["total"] or 0
    total_likes = articles.aggregate(total=Sum("likes"))["total"] or 0

    top_articles = articles.order_by("-views")[:5]

    categories = Category.objects.annotate(
        article_count=Count("articles")
    ).order_by("-article_count")

    category_labels = [category.name for category in categories]
    category_data = [category.article_count for category in categories]

    context = {
        "articles": articles[:10],
        "categories": categories,
        "top_articles": top_articles,
        "total_articles": total_articles,
        "published_articles": published_articles,
        "draft_articles": draft_articles,
        "total_views": total_views,
        "total_likes": total_likes,
        "category_labels": category_labels,
        "category_data": category_data,
        "selected_status": status,
        "selected_category": category_id,
    }

    return render(request, "core/dashboard.html", context)


def article_list(request):
    articles = Article.objects.select_related("category").all()
    categories = Category.objects.all()

    return render(request, "core/article_list.html", {
        "articles": articles,
        "categories": categories,
    })


def article_filter_htmx(request):
    status = request.GET.get("status", "")
    category_id = request.GET.get("category", "")

    articles = Article.objects.select_related("category").all()

    if status:
        articles = articles.filter(status=status)

    if category_id:
        articles = articles.filter(category_id=category_id)

    return render(request, "core/partials/article_cards.html", {
        "articles": articles,
    })


def like_article_ajax(request, article_id):
    if request.method != "POST":
        return JsonResponse({
            "success": False,
            "error": "Invalid request method.",
        }, status=400)

    article = get_object_or_404(Article, id=article_id)
    article.likes += 1
    article.save(update_fields=["likes"])

    return JsonResponse({
        "success": True,
        "likes": article.likes,
    })


def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)

        if form.is_valid():
            messages.success(request, "Your message has been sent successfully.")
            return redirect("contact")
        else:
            messages.error(request, "Please correct the errors below.")
    else:
        form = ContactForm()

    return render(request, "core/contact.html", {
        "form": form,
    })


def contact_htmx(request):
    if request.method == "POST":
        form = ContactForm(request.POST)

        if form.is_valid():
            return render(request, "core/partials/contact_form.html", {
                "form": ContactForm(),
                "success_message": "Your message has been sent successfully.",
            })

        return render(request, "core/partials/contact_form.html", {
            "form": form,
        })

    return render(request, "core/partials/contact_form.html", {
        "form": ContactForm(),
    })

13. templates/base.html

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}SmartDash{% endblock %}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

    <!-- Tailwind CDN for tutorial/demo only -->
    <script src="https://cdn.tailwindcss.com"></script>

    <!-- HTMX -->
    <script src="https://unpkg.com/htmx.org@1.9.12"></script>

    <!-- Custom CSS -->
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body class="bg-light">

<nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm">
    <div class="container">
        <a class="navbar-brand fw-bold" href="{% url 'home' %}">SmartDash</a>

        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div id="mainNavbar" class="collapse navbar-collapse">
            <ul class="navbar-nav ms-auto">
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'home' %}">Home</a>
                </li>

                <li class="nav-item">
                    <a class="nav-link" href="{% url 'dashboard' %}">Dashboard</a>
                </li>

                <li class="nav-item">
                    <a class="nav-link" href="{% url 'article_list' %}">Articles</a>
                </li>

                <li class="nav-item">
                    <a class="nav-link" href="{% url 'contact' %}">Contact</a>
                </li>
            </ul>
        </div>
    </div>
</nav>

<main class="py-5">
    <div class="container">

        {% if messages %}
            {% for message in messages %}
                <div class="alert alert-{{ message.tags }} alert-dismissible fade show shadow-sm">
                    {{ message }}
                    <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
                </div>
            {% endfor %}
        {% endif %}

        {% block content %}{% endblock %}
    </div>
</main>

<footer class="border-top bg-white py-4">
    <div class="container text-center text-muted small">
        © 2026 SmartDash — Django, Bootstrap, Tailwind, AJAX, HTMX and Dashboards
    </div>
</footer>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{% static 'js/main.js' %}"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

{% block extra_js %}{% endblock %}

</body>
</html>

14. templates/core/home.html

{% extends "base.html" %}

{% block title %}Home | SmartDash{% endblock %}

{% block content %}

<section class="text-center py-5">
    <span class="badge bg-primary mb-3">Django Tutorials 71 to 76 Project</span>

    <h1 class="display-4 fw-bold">
        SmartDash Forms & Articles Platform
    </h1>

    <p class="lead text-muted mx-auto" style="max-width: 760px;">
        A complete Django project combining Bootstrap layouts, Tailwind design,
        AJAX interactions, HTMX partial updates, interactive dashboards, and better form UX.
    </p>

    <div class="d-flex justify-content-center gap-3 mt-4">
        <a href="{% url 'dashboard' %}" class="btn btn-primary btn-lg">
            Open Dashboard
        </a>
        <a href="{% url 'article_list' %}" class="btn btn-outline-dark btn-lg">
            View Articles
        </a>
    </div>
</section>

<section class="row g-4 mt-4">
    <div class="col-md-4">
        <div class="card h-100 border-0 shadow-sm">
            <div class="card-body">
                <h5 class="fw-bold">Bootstrap UI</h5>
                <p class="text-muted">
                    Navbar, cards, tables, alerts, buttons, and responsive layouts.
                </p>
            </div>
        </div>
    </div>

    <div class="col-md-4">
        <div class="card h-100 border-0 shadow-sm">
            <div class="card-body">
                <h5 class="fw-bold">AJAX and HTMX</h5>
                <p class="text-muted">
                    Like articles using AJAX and filter content dynamically using HTMX.
                </p>
            </div>
        </div>
    </div>

    <div class="col-md-4">
        <div class="card h-100 border-0 shadow-sm">
            <div class="card-body">
                <h5 class="fw-bold">Better Forms</h5>
                <p class="text-muted">
                    Improved labels, placeholders, validation messages, and feedback.
                </p>
            </div>
        </div>
    </div>
</section>

{% endblock %}

15. templates/core/dashboard.html

{% extends "base.html" %}

{% block title %}Dashboard | SmartDash{% endblock %}

{% block content %}

<div class="d-flex justify-content-between align-items-center mb-4">
    <div>
        <h1 class="fw-bold mb-1">Interactive Dashboard</h1>
        <p class="text-muted mb-0">Statistics, filters, charts, and article performance.</p>
    </div>
    <a href="{% url 'article_list' %}" class="btn btn-primary">Manage Articles</a>
</div>

<div class="row g-4 mb-5">
    <div class="col-md-3">
        <div class="card border-0 shadow-sm stat-card">
            <div class="card-body">
                <p class="text-muted mb-1">Total Articles</p>
                <h2 class="fw-bold">{{ total_articles }}</h2>
            </div>
        </div>
    </div>

    <div class="col-md-3">
        <div class="card border-0 shadow-sm stat-card">
            <div class="card-body">
                <p class="text-muted mb-1">Published</p>
                <h2 class="fw-bold text-success">{{ published_articles }}</h2>
            </div>
        </div>
    </div>

    <div class="col-md-3">
        <div class="card border-0 shadow-sm stat-card">
            <div class="card-body">
                <p class="text-muted mb-1">Drafts</p>
                <h2 class="fw-bold text-warning">{{ draft_articles }}</h2>
            </div>
        </div>
    </div>

    <div class="col-md-3">
        <div class="card border-0 shadow-sm stat-card">
            <div class="card-body">
                <p class="text-muted mb-1">Total Views</p>
                <h2 class="fw-bold text-primary">{{ total_views }}</h2>
            </div>
        </div>
    </div>
</div>

<form method="get" class="card border-0 shadow-sm mb-5">
    <div class="card-body">
        <h5 class="fw-bold mb-3">Dashboard Filters</h5>

        <div class="row g-3">
            <div class="col-md-5">
                <select name="status" class="form-select">
                    <option value="">All Statuses</option>
                    <option value="published" {% if selected_status == "published" %}selected{% endif %}>Published</option>
                    <option value="draft" {% if selected_status == "draft" %}selected{% endif %}>Draft</option>
                </select>
            </div>

            <div class="col-md-5">
                <select name="category" class="form-select">
                    <option value="">All Categories</option>
                    {% for category in categories %}
                        <option value="{{ category.id }}" {% if selected_category == category.id|stringformat:"s" %}selected{% endif %}>
                            {{ category.name }}
                        </option>
                    {% endfor %}
                </select>
            </div>

            <div class="col-md-2">
                <button class="btn btn-dark w-100">Apply</button>
            </div>
        </div>
    </div>
</form>

<div class="row g-4 mb-5">
    <div class="col-lg-7">
        <div class="card border-0 shadow-sm h-100">
            <div class="card-body">
                <h5 class="fw-bold mb-4">Articles by Category</h5>
                <canvas id="categoryChart"></canvas>
            </div>
        </div>
    </div>

    <div class="col-lg-5">
        <div class="card border-0 shadow-sm h-100">
            <div class="card-body">
                <h5 class="fw-bold mb-4">Top Articles</h5>

                <div class="table-responsive">
                    <table class="table align-middle">
                        <thead>
                            <tr>
                                <th>Title</th>
                                <th>Views</th>
                                <th>Likes</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for article in top_articles %}
                                <tr>
                                    <td>{{ article.title }}</td>
                                    <td>{{ article.views }}</td>
                                    <td>{{ article.likes }}</td>
                                </tr>
                            {% empty %}
                                <tr>
                                    <td colspan="3" class="text-muted">No articles found.</td>
                                </tr>
                            {% endfor %}
                        </tbody>
                    </table>
                </div>

            </div>
        </div>
    </div>
</div>

<div class="card border-0 shadow-sm">
    <div class="card-body">
        <h5 class="fw-bold mb-4">Recent Articles</h5>
        {% include "core/partials/article_table.html" %}
    </div>
</div>

{{ category_labels|json_script:"category-labels" }}
{{ category_data|json_script:"category-data" }}

{% endblock %}

{% block extra_js %}
<script>
    const labels = JSON.parse(document.getElementById("category-labels").textContent);
    const data = JSON.parse(document.getElementById("category-data").textContent);

    const ctx = document.getElementById("categoryChart");

    new Chart(ctx, {
        type: "bar",
        data: {
            labels: labels,
            datasets: [{
                label: "Articles",
                data: data
            }]
        }
    });
</script>
{% endblock %}

16. templates/core/partials/article_table.html

<div class="table-responsive">
    <table class="table align-middle table-hover">
        <thead>
            <tr>
                <th>Title</th>
                <th>Category</th>
                <th>Status</th>
                <th>Views</th>
                <th>Likes</th>
            </tr>
        </thead>

        <tbody>
            {% for article in articles %}
                <tr>
                    <td class="fw-semibold">{{ article.title }}</td>
                    <td>{{ article.category.name|default:"No category" }}</td>
                    <td>
                        {% if article.status == "published" %}
                            <span class="badge bg-success">Published</span>
                        {% else %}
                            <span class="badge bg-warning text-dark">Draft</span>
                        {% endif %}
                    </td>
                    <td>{{ article.views }}</td>
                    <td>{{ article.likes }}</td>
                </tr>
            {% empty %}
                <tr>
                    <td colspan="5" class="text-center text-muted py-4">
                        No articles found.
                    </td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
</div>

17. templates/core/article_list.html

{% extends "base.html" %}

{% block title %}Articles | SmartDash{% endblock %}

{% block content %}

<div class="d-flex justify-content-between align-items-center mb-4">
    <div>
        <h1 class="fw-bold mb-1">Articles</h1>
        <p class="text-muted mb-0">AJAX likes and HTMX filtering.</p>
    </div>
</div>

<div class="card border-0 shadow-sm mb-4">
    <div class="card-body">
        <h5 class="fw-bold mb-3">Filter Articles with HTMX</h5>

        <div class="row g-3">
            <div class="col-md-6">
                <select
                    name="status"
                    class="form-select"
                    hx-get="{% url 'article_filter_htmx' %}"
                    hx-target="#article-results"
                    hx-trigger="change"
                    hx-include="#category-filter">
                    <option value="">All Statuses</option>
                    <option value="published">Published</option>
                    <option value="draft">Draft</option>
                </select>
            </div>

            <div class="col-md-6">
                <select
                    id="category-filter"
                    name="category"
                    class="form-select"
                    hx-get="{% url 'article_filter_htmx' %}"
                    hx-target="#article-results"
                    hx-trigger="change">
                    <option value="">All Categories</option>
                    {% for category in categories %}
                        <option value="{{ category.id }}">{{ category.name }}</option>
                    {% endfor %}
                </select>
            </div>
        </div>
    </div>
</div>

<div id="article-results">
    {% include "core/partials/article_cards.html" %}
</div>

{% endblock %}

18. templates/core/partials/article_cards.html

<div class="row g-4">
    {% for article in articles %}
        <div class="col-md-6 col-lg-4">
            <div class="card border-0 shadow-sm h-100 article-card">
                <div class="card-body d-flex flex-column">
                    <div class="mb-2">
                        {% if article.status == "published" %}
                            <span class="badge bg-success">Published</span>
                        {% else %}
                            <span class="badge bg-warning text-dark">Draft</span>
                        {% endif %}
                    </div>

                    <h5 class="fw-bold">{{ article.title }}</h5>

                    <p class="text-muted">
                        {{ article.summary|truncatewords:22 }}
                    </p>

                    <p class="small text-secondary mt-auto">
                        Category: {{ article.category.name|default:"No category" }}
                    </p>

                    <div class="d-flex justify-content-between align-items-center mt-3">
                        <button
                            class="btn btn-outline-primary btn-sm like-btn"
                            data-id="{{ article.id }}">
                            Like
                        </button>

                        <span>
                            <strong id="like-count-{{ article.id }}">{{ article.likes }}</strong>
                            likes
                        </span>
                    </div>
                </div>
            </div>
        </div>
    {% empty %}
        <div class="col-12">
            <div class="alert alert-info">
                No articles found.
            </div>
        </div>
    {% endfor %}
</div>

19. templates/core/contact.html

{% extends "base.html" %}

{% block title %}Contact | SmartDash{% endblock %}

{% block content %}

<div class="row justify-content-center">
    <div class="col-lg-7">

        <div class="bg-white rounded-4 shadow-sm border p-4 p-md-5">
            <div class="text-center mb-4">
                <span class="badge bg-success mb-2">Improved Form UX</span>
                <h1 class="fw-bold">Contact Us</h1>
                <p class="text-muted">
                    This form uses improved labels, placeholders, validation, messages, and HTMX.
                </p>
            </div>

            <div id="contact-form-box">
                {% include "core/partials/contact_form.html" %}
            </div>
        </div>

    </div>
</div>

{% endblock %}

20. templates/core/partials/contact_form.html

{% if success_message %}
    <div class="alert alert-success shadow-sm">
        {{ success_message }}
    </div>
{% endif %}

{% if form.non_field_errors %}
    <div class="alert alert-danger">
        {% for error in form.non_field_errors %}
            <div>{{ error }}</div>
        {% endfor %}
    </div>
{% endif %}

<form
    method="post"
    hx-post="{% url 'contact_htmx' %}"
    hx-target="#contact-form-box"
    hx-swap="innerHTML"
    id="contact-form">

    {% csrf_token %}

    <div class="mb-3">
        <label class="form-label fw-semibold" for="{{ form.name.id_for_label }}">
            {{ form.name.label }}
        </label>

        {{ form.name }}

        {% if form.name.errors %}
            <div class="text-danger small mt-1">
                {% for error in form.name.errors %}
                    <div>{{ error }}</div>
                {% endfor %}
            </div>
        {% endif %}
    </div>

    <div class="mb-3">
        <label class="form-label fw-semibold" for="{{ form.email.id_for_label }}">
            {{ form.email.label }}
        </label>

        {{ form.email }}

        {% if form.email.errors %}
            <div class="text-danger small mt-1">
                {% for error in form.email.errors %}
                    <div>{{ error }}</div>
                {% endfor %}
            </div>
        {% endif %}
    </div>

    <div class="mb-3">
        <label class="form-label fw-semibold" for="{{ form.message.id_for_label }}">
            {{ form.message.label }}
        </label>

        {{ form.message }}

        {% if form.message.help_text %}
            <div class="form-text">{{ form.message.help_text }}</div>
        {% endif %}

        {% if form.message.errors %}
            <div class="text-danger small mt-1">
                {% for error in form.message.errors %}
                    <div>{{ error }}</div>
                {% endfor %}
            </div>
        {% endif %}
    </div>

    <button type="submit" class="btn btn-primary btn-lg w-100">
        Send Message
    </button>
</form>

21. static/css/style.css

body {
    min-height: 100vh;
}

.stat-card {
    border-radius: 1rem;
    transition: 0.2s ease;
}

.stat-card:hover {
    transform: translateY(-4px);
}

.article-card {
    border-radius: 1rem;
    transition: 0.2s ease;
}

.article-card:hover {
    transform: translateY(-4px);
}

.form-control,
.form-select {
    border-radius: 0.8rem;
}

.btn {
    border-radius: 0.8rem;
}

.card {
    border-radius: 1rem;
}

22. static/js/main.js

function getCookie(name) {
    let cookieValue = null;

    if (document.cookie && document.cookie !== "") {
        const cookies = document.cookie.split(";");

        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();

            if (cookie.substring(0, name.length + 1) === name + "=") {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }

    return cookieValue;
}

document.addEventListener("click", function (event) {
    if (!event.target.classList.contains("like-btn")) {
        return;
    }

    const button = event.target;
    const articleId = button.dataset.id;
    const csrftoken = getCookie("csrftoken");

    button.disabled = true;
    button.textContent = "Liking...";

    fetch(`/articles/${articleId}/like/`, {
        method: "POST",
        headers: {
            "X-CSRFToken": csrftoken,
            "X-Requested-With": "XMLHttpRequest",
        },
    })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                document.getElementById(`like-count-${articleId}`).textContent = data.likes;
            } else {
                alert(data.error || "Something went wrong.");
            }
        })
        .catch(error => {
            console.error("AJAX error:", error);
            alert("An error occurred.");
        })
        .finally(() => {
            button.disabled = false;
            button.textContent = "Like";
        });
});

23. Create database tables

python manage.py makemigrations
python manage.py migrate

24. Create superuser

python manage.py createsuperuser

25. Add test data

Run the server:

python manage.py runserver

Open:

http://127.0.0.1:8000/admin/

Add categories such as:

Django
Python
Web Development
Security
Frontend

Add several articles with different statuses, views, and likes.

26. Run the project

python manage.py runserver

Open:

http://127.0.0.1:8000/

Useful pages:

Home:        http://127.0.0.1:8000/
Dashboard:   http://127.0.0.1:8000/dashboard/
Articles:    http://127.0.0.1:8000/articles/
Contact:     http://127.0.0.1:8000/contact/
Admin:       http://127.0.0.1:8000/admin/

Important production note

In this tutorial project, Tailwind is loaded with CDN for simplicity:

<script src="https://cdn.tailwindcss.com"></script>

For a real production project, install Tailwind with Node.js and compile your CSS instead of using the CDN.

Final result

You now have a complete Django project that covers:

Bootstrap layout and dashboard design.

Tailwind-style modern UI utilities.

AJAX article likes without page reload.

HTMX article filtering and contact form submission.

Chart.js dashboard visualization.

Improved Django form UX with validation and user feedback.